[Tutorial] Interface Scripting

In this forum you will find and post information regarding the modding of Star Wars Battlefront 2. DO NOT POST MOD IDEAS/REQUESTS.

Moderator: Moderators

Post Reply
jedimoose32
Field Commander
Field Commander
Posts: 938
Joined: Thu Jan 24, 2008 12:41 am
Projects :: Engineering Degree
Location: The Flatlands of Canada

[Tutorial] Interface Scripting

Post by jedimoose32 »

Buckle up, this tutorial is not for the faint of heart.

First of all I need to extend mad street cred to Nedarb7, who first introduced me to the interface system. I would be completely clueless about this without his efforts and patient guidance. :o
Secondly it is important for you to know that this is largely based on a tutorial I wrote for Noobasaurus in a PM with reference to his Hide and Seek game mode. I don't think he ended up using much of what we discussed but the reason I mention this is just because some of the comments in my code will mention things like hide and seek, or blindness, etc. So just keep that in mind, though I've done my best to adapt everything for a more general audience. :)

What is the purpose of this tutorial?
There are two main topics that I am going to cover. The first is how to set up a brand new, fully customizable HUD element using the interface framework, and the second is how to construct a menu. I can already hear people protesting and saying that is totally backwards, we should learn how to make a menu first... well the way my examples are set up it will actually be easier for me to do it in the reverse order, so have a little faith and it will (hopefully) become clear soon.

Last thing before we begin: My examples are primarily from my Assassination game mode, so if you want to see how these things work in-game, you can download the test map HERE.

Let's begin!

Part 1: Preparation

Pretty much everything in this tutorial will be taking place inside data_ABC (or whatever your 3-letter mod code is). Assume that all directories mentioned in this tutorial have data_ABC as the root unless I say otherwise.

The very first thing you need to understand about making this kind of custom HUD element is that it uses the game's interface system, not the HUD. So it is essentially a screen much like the pause menu or video options screen, but overlaid on top of the game while the player has control over their unit rather than while the game is paused/frozen.
Custom interface scripts need to be loaded through the map's mission.lvl. However, simply adding the script's name to the mission.req file doesn't add it in. The file needs to be called by another script that automatically gets processed every time the map loads... which is ifs_pausemenu.lua. Note: this only applies if you are adding an in-game menu or a HUD overlay - if you are making a shell screen such as a new campaign menu or customizing the main menu, use the standard shell editing techniques for Part 6 of this tutorial!

So, open up common/mission.req and add "ifs_pausemenu" at the bottom of the "script" section.

Now we need to make a change to the pause menu script itself, so that it will tell the game to load our custom interface file (which we will create in another step) when the map loads. Open common/scripts/ifs_pausemenu.lua and put the following code at the top:

Code: Select all

ScriptCB_DoFile("ifs_blindness")
You may have guessed that ifs_blindness is the name of the .lua file we will be making later which will contain all our interface stuff. You can call it whatever you want, but I'll be using the name ifs_blindness throughout the rest of this tutorial. The ifs_ prefix is standard for all interface script names, but I don't think it's required.

We also need to load our (currently imaginary) interface script in mission.req, so go back to that file and add "ifs_blindness" anywhere above "ifs_pausemenu" in the script section. I'm pretty sure this order is important because we need ifs_blindness.lua to be already loaded in the game's memory when ifs_pausemenu.lua tries to execute it later.

So now we have the preliminary preparation work done - not too hard so far, right?

Part 2: Graphics

Obviously you can decide what kind of graphic will look best for the blindness effect, but here are a few guidelines:
  • It must be a .tga file
  • It must have an alpha channel, even if it's all white
  • I think it needs to have dimensions that are powers of two (e.g. 32x256, 512X512, etc)
  • The file needs to be saved to common/interface/

Once you have the graphic made, you'll need to make sure the game knows to load it. This should be done through a custom .lvl file. I use common.lvl for loading in all of my custom graphics. Open common/common.req, and if there is anything inside, delete it. Create a "texture" section and enter the name of your blindness effect .tga underneath (mine is called blackness.tga). So in this example mine would be:

Code: Select all

ucft
{
    REQN
    {
        "texture"
        "blackness"
    }
}
Now you need to make sure your custom .lvl gets loaded with your map, so enter this at the bottom of your addme.lua:

Code: Select all

ReadDataFile("..\\..\\addon\\ABC\\data\\_LVL_PC\\common.lvl")
And put this at the beginning of ScriptInit in your map's .lua (e.g. ABCc_con.lua):

Code: Select all

ReadDataFile("dc:common.lvl")
When you're done munging after this whole process is finished, remember that you might need to manually copy common.lvl over to your addon folder (I don't think you do but it never hurts to double check).

Part 3: What Is the Interface?


This is where things get really interesting.

In common/scripts/, make a new .lua file and name it the same as what you entered at the top of ifs_pausemenu.lua. In my example, it will be called ifs_blindness.lua.
The interface is made up of a bunch of objects that inherit properties and variables from other objects. If you go high enough up the chain you unfortunately run into the black box of the ZeroEngine's C API. This means that you can't make new kinds of objects within the interface, but you can do a ton of stuff with what's already available. Before I show you an example of an interface screen, I'll briefly explain the main elements you will see.
  • IFShellScreen - This is a table that defines the whole "screen". This is where you create buttons, images, text, etc. Right at the beginning a bunch of basic properties of the screen are defined. Important side note: in general, the shell uses 1 for yes/true and nil for no/false.
  • functions Enter, Update, Exit, Input_Back, Input_Accept - These are functions within the screen that are run at the point given by their name (e.g. function Enter runs when you first enter the screen; Input_Accept runs when the game receives a left-click or Enter key-press). You can put any code you want inside these.
  • IFContainer - A container is like a template that other objects can use to share common properties. Say I have 5 buttons and they all need to be 50 px wide and 10 px tall, I can make a container with the properties width=50 and height=10, and have all the buttons inherit from the container, instead of having to manually tell each one to use those dimensions.
  • IFText - A text box.
  • IFImage - An image.
  • function doSomething(this) - You can define and execute a function that does stuff not already defined in IFShellScreen if that meets your needs. You could theoretically leave the NewIfShellScreen table blank and define your whole screen through a function.
  • this - Refers to whatever screen is being defined.
  • AddIFScreen() - A function that adds your screen to the list of screens, I believe. It's definitely important.
Part 4: A Commented Example
Hidden/Spoiler:
[code]-- This script will create an interface overlay that simulates additional HUD elements

local screenWidth, screenHeight = ScriptCB_GetScreenInfo() -- get the screen dimensions, could come in handy later
local lock = 0
local txtLock = false

ifs_assnstars = NewIFShellScreen { -- my screen name here is ifs_assnstars, if this followed my example it would be ifs_blindness

bNohelptextPC = 1,
bNohelptext_back = 1, -- making this 1 would remove the default "back" button
bNohelptext_backPC = 1, -- PC version of variable, potentially useless
bNohelptext_accept = 1, -- Remove the default "accept" button
bg_texture = nil, -- Background texture name
movieBackground = nil, -- We don't have a movie background
movieIntro = nil, -- We don't have a movie intro
bDimBackdrop = nil, -- Dims backdrop if 1

Enter = function(this, bFwd) -- runs once on entering the screen
gIFShellScreenTemplate_fnEnter(this, bFwd) -- a generic function, does behind-the-scenes stuff, don't worry about this at all, ever

AnimationMgr_ClearAnimations(this.starUpdate) -- clears some animations for text that will be defined later
AnimationMgr_ClearAnimations(this.livesCounter)

IFObj_fnSetVis(this.starUpdate, nil) -- this makes the starUpdate text invisible

-- getting information from the Assassination gamemode is really easy because I set it up as a proper gamemode using Lua's limited object-oriented programming
-- capabilities... basically the game stores some basic global variables in the AssassinMode table (e.g. playerLives, below)
-- in my map/mission script (JERc_con) I make an instance of AssassinMode, like so:
-- "assassination = AssassinMode:New{...}" just like conquest, "conquest = ObjectiveConquest:New{...}"
-- you will likely need to implement a similar system for your mode, but I don't know how you've coded yours so maybe I'm wrong
local livesFd = assassination.playerLives
IFText_fnSetString(this.livesCounter, "game.assn.lives" .. livesFd) -- yep, you can change text on the fly this easily
AnimationMgr_AddAnimation(this.livesCounter, { fTotalTime = 3.0, fStartAlpha = 1, fEndAlpha = 0, }) -- the fade-out animation for "lives remaining"

end, -- since NewIfShellScreen is a table, always remember your commas

-- Function runs repeatedly (this = screen name, fDt = function delay time)
Update = function(this, fDt) -- fDt is a timer that elapses and resets every .125 seconds
gIFShellScreenTemplate_fnUpdate(this, fDt) -- another generic, behind-the-scenes function that you can ignore
ScriptCB_EnableCursor(nil) -- you can guess what this is for, really important for a custom HUD overlay

local lvl = assassination:DumpDefconLevel(false) -- get some more info from AssassinMode
if lock ~= lvl and lvl ~= nil then -- this section of code constantly checks to see if the player's stars have updated
lock = lvl
IFObj_fnSetVis(this.starUpdate, 1) -- remember we made this invisible before, we want to show it now
if lvl == 0 then
IFImage_fnSetTexture(this.starC.star1, "assn_star_blank") -- if the player's star level is 0,
IFImage_fnSetTexture(this.starC.star2, "assn_star_blank") -- make all 5 star slots empty
IFImage_fnSetTexture(this.starC.star3, "assn_star_blank")
IFImage_fnSetTexture(this.starC.star4, "assn_star_blank")
IFImage_fnSetTexture(this.starC.star5, "assn_star_blank")
IFText_fnSetString(this.starUpdate, "ANONYMOUS") -- look at us changing text on the fly again... remember this line when I discuss text boxes
else
for i=1, lvl do
IFImage_fnSetTexture(this.starC["star" .. i], "assn_star_active") -- if the star level is >=1 then show the correct number of stars
end
IFText_fnSetString(this.starUpdate, "game.assn." .. lvl .. "star") -- and some text to tell us how many
end
end

local txtFd = assassination.textFade -- these next 8 lines are a fade animation for the star update text
if txtLock ~= txtFd and txtFd ~= nil then
txtLock = txtFd
if txtFd == true then
AnimationMgr_AddAnimation(this.starUpdate, { fTotalTime = 5.0, fStartAlpha = 1, fEndAlpha = 0, })
assassination.textFade = false
end
end



end, -- again, never forget commas

-- Function runs on exiting the screen (this = screen name, bFwd = nil unless not backing out)
Exit = function(this, bFwd) -- in my case, nothing needed to happen on exiting the screen
end,

Input_Back = function(this) -- Input_Back is I think only bound to the Esc key
ScriptCB_PopScreen() -- so of course if the player presses escape we want to "pop" (hide) this screen so the player can see the pause menu
end,

-- Function runs on call, button select, or by the default "enter" button (this = screen name)
Input_Accept = function(this)
-- If base class handled this work, then we're done (not sure what this does)
if(gShellScreen_fnDefaultInputAccept(this)) then -- this is another of those things you can leave alone
return
end
--ifelm_shellscreen_fnPlaySound(this.acceptSound) -- you really don't want that jingly "accept button" sound playing every time the player left-clicks
end,

starC = NewIFContainer { -- at last, our first screen object, a container... I used this one for shared star properties (e.g. on-screen y coordinates)
ZPos = 10, -- ZPos is the object's "depth" on screen, 1 is front, 99 is back
ScreenRelativeX = 0.5, -- 0 is left, 1 is right, 0.5 is x-centered
ScreenRelativeY = 0.0, -- 0 is top, 1 is bottom (I'm pretty sure)
x = 0, -- fine-tuning... e.g. ScreenRelativeX=0.5 and x=-10 means the object would be 10 pixels left of center
y = (screenHeight/9.9), -- same here
},

starUpdate = NewIFText { -- new text box
string = "", -- the string field can contain the actual text you want (see "ANONYMOUS", further up) or a localization path (e.g. "level.ABC.mytext")
font = "gamefont_medium", -- tiny, small, medium, large
ZPos = 9,
ScreenRelativeX = 0.5,
ScreenRelativeY = 0.175,
halign = "hcenter", -- horizontal alignment, options are left, hcenter, right... there is also a valign property with options top, vcenter, bottom
textw = 150, -- width of the text box
x = -75,
y = 0,
textcolorr = 255, -- self explanatory
textcolorg = 255,
textcolorb = 255,
alpha = 1.0,
nocreatebackground = 1, -- for the love of all things good and holy please leave this as 1
},

livesCounter = NewIFText {
string = "", -- I should mention the reason I've left the string property blank in both my text objects is because they're dynamic so I define them...
font = "gamefont_medium", -- ...on the fly!
ZPos = 8,
ScreenRelativeX = 0.5,
ScreenRelativeY = 0.2,
halign = "hcenter",
textw = 150,
x = -75,
y = 0,
textcolorr = 255,
textcolorg = 255,
textcolorb = 255,
alpha = 1.0,
nocreatebackground = 1,
},

}

-- okay this is the doSomething function that I mentioned in Part 3... this all could have been defined in NewIFShellScreen
-- remember my container's name was starC, and also remember that "this" refers to the interface screen we're working on
-- so all of these lines inside the doStars function could read as ifs_assnstars.starC....
-- texture property for images cannot be handled by a container, I believe the same applies to the string property for text boxes
-- in our example, the texture would = "blackness"
-- localpos_l, _t, _r, _b (left, top, right, bottom) define how far from origin the image should extend
-- my star images were made 32x32 pixels so these properties extend the image 16 pixels in each direction
-- in your case you will probably want to use -(screenWidth/2), -(screenHeight/2), screenWidth/2, and screenHeight/2
function doStars(this)
this.starC["frame1"] = NewIFImage { x = -68, texture = "assn_star_frame", localpos_l = -16, localpos_t = -16, localpos_r = 16, localpos_b = 16, }
this.starC["frame2"] = NewIFImage { x = -34, texture = "assn_star_frame", localpos_l = -16, localpos_t = -16, localpos_r = 16, localpos_b = 16, }
this.starC["frame3"] = NewIFImage { x = 0, texture = "assn_star_frame", localpos_l = -16, localpos_t = -16, localpos_r = 16, localpos_b = 16, }
this.starC["frame4"] = NewIFImage { x = 34, texture = "assn_star_frame", localpos_l = -16, localpos_t = -16, localpos_r = 16, localpos_b = 16, }
this.starC["frame5"] = NewIFImage { x = 68, texture = "assn_star_frame", localpos_l = -16, localpos_t = -16, localpos_r = 16, localpos_b = 16, }

this.starC["star1"] = NewIFImage { ZPos = 9, x = -68, texture = "assn_star_blank", localpos_l = -16, localpos_t = -16, localpos_r = 16, localpos_b = 16, }
this.starC["star2"] = NewIFImage { ZPos = 9, x = -34, texture = "assn_star_blank", localpos_l = -16, localpos_t = -16, localpos_r = 16, localpos_b = 16, }
this.starC["star3"] = NewIFImage { ZPos = 9, x = 0, texture = "assn_star_blank", localpos_l = -16, localpos_t = -16, localpos_r = 16, localpos_b = 16, }
this.starC["star4"] = NewIFImage { ZPos = 9, x = 34, texture = "assn_star_blank", localpos_l = -16, localpos_t = -16, localpos_r = 16, localpos_b = 16, }
this.starC["star5"] = NewIFImage { ZPos = 9, x = 68, texture = "assn_star_blank", localpos_l = -16, localpos_t = -16, localpos_r = 16, localpos_b = 16, }
end

doStars(ifs_assnstars) -- can't forget to actually execute the doStars function
doStars = nil -- once it's done we can clear it from memory, it only needs to run once

AddIFScreen(ifs_assnstars, "ifs_assnstars") -- I don't know exactly how this works but just put ifs_blindness (or whatever you've named yours) in here twice[/code]
Part 5: Munge

Munge Common and then you are good to go! If you struggle with this part or run into any problems... yeah... 8)
I'm not going to cover the stuff you need to put in your game mode or map lua, but I will tell you that in order to see your new screen/HUD in-game you need to use ScriptCB_PushScreen("ifs_whatever") in your code. You can find the source files for Assassination Mode at my project directory website but really if you're this far along in the tutorial chances are you know what you need to do in order to set up global variables that can be read by ifs_yourifnamehere.lua so I'll let you take care of that yourself. :)

----------------------------------------------------------------------------------------------------------------

Part 6: Menus

There are so many different elements you can include in menus, some of which are very simple (buttons) and others which are needlessly complicated (looking at you, sliders). For this tutorial, I'm just going to show you a basic vertical button arrangement and a toggleable text box object. Remember when we're editing a menu in the shell (e.g. the single-player campaign menu or the video settings menu) we don't use the ifs_pausemenu hack, we use the regular shell editing procedure which is found in a different tutorial!
Hidden/Spoiler:
[code]-- This script will allow the player to set the difficulty options for Assassination mode in-game. This is my first try at anything shell or interface related.

local screenWidth, screenHeight = ScriptCB_GetScreenInfo() -- it's always good practice to do this at the start of all your ifs files

local CurPreset = 2 -- here I'm defining some local variables
local presetStrTable = {"Easy", "Normal", "Hard", "Brutal"} -- strings for the button labels
local losStrTable = {"Short", "Medium", "Far", "Very Far", "Very Far"}
local starStrTable = {"Long", "Average", "Short",}
local assnlos = 16 -- multipliers for difficulty settings...
local radmult = 3
local plives = 3
local evmult = 2
local isHelpUp = false -- is the help textbox visible?

ifsassndiff_vbutton_layout = { -- you can call this whatever you want, it's just a framework for the vertical buttons but it doesn't get initialized until the end
ySpacing = 5, -- ...which is xxxxxxx_vbutton_layout, where xxxxxxx can be whatever you want
width = 260, -- ySpacing was the number of pixels between the buttons vertically, width is self-explanatory
font = gMenuButtonFont, -- gMenuButtonFont is a global variable defined in interface_util.lua which contains lots of globals
buttonlist = { -- alright, now we're getting somewhere. this is a table of buttons, and each button in this table is itself a table
{ tag = "presets", string = "PRESET: "..presetStrTable[CurPreset], }, -- important key-value pairs for buttons are: ...
{ tag = "addlives", string = "+ Player Lives ("..plives..")", }, -- tag, which is just the in-script name you're giving the button...
{ tag = "sublives", string = "- Player Lives ("..plives..")", }, -- and string, which is the in-game name for the button...
{ tag = "guardlos", string = "Guard LOS: "..losStrTable[(assnlos/8)], }, -- don't forget your commas, people,,,,,
{ tag = "guardradios", string = "Radio Distance: "..losStrTable[((radmult/2)+0.5)], },
{ tag = "evademult", string = "Star Delay: "..starStrTable[4-evmult], },
{ tag = "help", string = "Toggle Help", },
},
title = "Difficulty Settings", -- title is a string (which can be a localized string e.g. "level.ABC.blah", goes above the buttons
-- see the pause menu for a good example of what a similar vertical button layout looks like in-game
}

ifs_assndiff = NewIFShellScreen { -- we've seen this before

bNohelptextPC = nil, -- nothing new to see here, move along citizen
bNohelptext_back = nil,
bNohelptext_backPC = nil, -- oh wait this is new... we now have a default generated back button in the corner
bNohelptext_accept = nil,
bg_texture = nil,
movieBackground = nil,
movieIntro = nil,
bDimBackdrop = 1, -- we didn't do this last time, but now I want a dim backdrop so deal with it

Enter = function(this, bFwd)
gIFShellScreenTemplate_fnEnter(this, bFwd)

this.CurButton = ShowHideVerticalButtons(this.buttons, ifsassndiff_vbutton_layout) -- this makes it so the currently selected...
-- ...object is the vertical button list. this is more important than you may realize, so just do it okay?
IFObj_fnSetVis(this.helptext, nil)

end,

Update = function(this, fDt) -- remember the update function runs every .125 seconds, or 8 times per second
gIFShellScreenTemplate_fnUpdate(this, fDt)

plives, assnlos, radmult, evmult = assassination:DumpDiffScreenInfo(false) -- this is a method from AssassinMode.lua that gives...
-- ...me important info about the current difficulty multipliers and the amount of remaining lives the player has
RoundIFButtonLabel_fnSetString(this.buttons.addlives, "+ Player Lives ("..plives..")") -- this.buttons.<button tag name without quotes>
RoundIFButtonLabel_fnSetString(this.buttons.sublives, "- Player Lives ("..plives..")")
RoundIFButtonLabel_fnSetString(this.buttons.presets, "PRESET: "..presetStrTable[CurPreset])
RoundIFButtonLabel_fnSetString(this.buttons.guardlos, "Guard LOS: "..losStrTable[assnlos/8]) -- yep, we can do math in here too!
RoundIFButtonLabel_fnSetString(this.buttons.guardradios, "Radio Distance: "..losStrTable[((radmult/2)+0.5)])
RoundIFButtonLabel_fnSetString(this.buttons.evademult, "Star Delay: "..starStrTable[4-evmult]) -- the lesson here is the function...
-- RoundIFButtonLabel_fnSetString(button, string) is what you use to update a button's text on the fly

end,

Exit = function(this, bFwd) -- ignore this, I never ended up using it in the end, but you may want to
--assassination.loadHUD = true
end,

Input_Back = function(this) -- when the player presses escape...
ScriptCB_SndPlaySound("shell_menu_exit") -- play the fun BOOP BOOP noise
ScriptCB_PopScreen() -- and pop (i.e. kill) the top-most screen, which is this one
end,

Input_Accept = function(this) -- so this is where we make stuff happen whenever a button gets pressed
if(gShellScreen_fnDefaultInputAccept(this)) then
return -- wow, much cool, very excite, wow
end

if this.CurButton then -- check to see if a button was hovered/selected when the accept key was pressed
ifelm_shellscreen_fnPlaySound(this.acceptSound) -- if so, play the accept jingle sound
end -- this means that you can't get the lovely accept jingle sound by just clicking anywhere on the background

-- okay this is a really big if-elseif so get ready... I promise this is actually how they do it in the stock files too
if this.CurButton == "_back" then -- side note: _back is the default generated back button, remember bNohelptext_backPC = nil?
ScriptCB_PopScreen() -- pop it
elseif this.CurButton == "presets" then -- if the button that was pressed was presets then...

if CurPreset < 4 then -- you can skip down to the next comment, this is all boring and unimportant
CurPreset = CurPreset + 1
elseif CurPreset >= 4 then
CurPreset = 1
end

if CurPreset == 1 then
assassination.maxplayerLives = 5
assassination.playerLives = 5
assassination.guardLOS = 8
assassination.guardRadioMult = 1
assassination.starMult = 3
elseif CurPreset == 2 then
assassination.maxplayerLives = 3
assassination.playerLives = 3
assassination.guardLOS = 16
assassination.guardRadioMult = 3
assassination.starMult = 2
elseif CurPreset == 3 then
assassination.maxplayerLives = 1
assassination.playerLives = 1
assassination.guardLOS = 24
assassination.guardRadioMult = 5
assassination.starMult = 1
elseif CurPreset == 4 then
assassination.maxplayerLives = 1
assassination.playerLives = 1
assassination.guardLOS = 40
assassination.guardRadioMult = 9
assassination.starMult = 1
end

elseif this.CurButton == "addlives" then -- still boring, but just wanted to reinforce that this is a different button now

assassination.maxplayerLives = assassination.maxplayerLives + 1
assassination.playerLives = assassination.playerLives + 1

elseif this.CurButton == "sublives" then -- and yet another...

if plives > 1 then
assassination.maxplayerLives = assassination.maxplayerLives - 1
assassination.playerLives = assassination.playerLives - 1
end

elseif this.CurButton == "guardlos" then

if assnlos == 8 then
assassination.guardLOS = 16
elseif assnlos == 16 then
assassination.guardLOS = 24
elseif assnlos == 24 then
assassination.guardLOS = 40
elseif assnlos == 40 then
assassination.guardLOS = 8
end

elseif this.CurButton == "guardradios" then

if radmult == 1 then
assassination.guardRadioMult = 3
elseif radmult == 3 then
assassination.guardRadioMult = 5
elseif radmult == 5 then
assassination.guardRadioMult = 9
elseif radmult == 9 then
assassination.guardRadioMult = 1
end

elseif this.CurButton == "evademult" then

if evmult ==3 then
assassination.starMult = 2
elseif evmult == 2 then
assassination.starMult = 1
elseif evmult == 1 then
assassination.starMult = 3
end

elseif this.CurButton == "help" then -- okay let's jump in here

if isHelpUp == true then -- this button toggles the help text box
IFObj_fnSetVis(this.helptext, nil) -- if help is already displayed then hide it with IFObj_fnSetVis(object, nil)
isHelpUp = false -- and then change the local variable
elseif isHelpUp == false then -- otherwise...
IFObj_fnSetVis(this.helptext, 1) -- make it visible with IFObj_fnSetVis(object, 1)
isHelpUp = true -- cool
end

end

--ifelm_shellscreen_fnPlaySound(this.acceptSound) --we only want this on actual buttons though

end,

buttons = NewIFContainer { -- remember containers?
ScreenRelativeX = 0.5, -- I want my buttons centered on both axes
ScreenRelativeY = 0.5,
},

helptext = NewIFText { -- here's a text box
string = "+/- Player Lives -- The number of lives the player is allowed.\nGuard LOS -- The distance from which guards can detect the player.\nRadio Distance -- The range of the guards' radio calls for backup.\nStar Delay -- The time delay between gaining new stars.",
font = "gamefont_small",
ZPos = 1,
ScreenRelativeX = 0.5,
ScreenRelativeY = 0.78,
halign = "hcenter",
textw = 800,
texth = 300,
x = -400,
y = 0,
textcolorr = 255,
textcolorg = 255,
textcolorb = 255,
alpha = 1.0,
nocreatebackground = 1, -- seriously don't do it
},

}

function doDiffs(this) -- this ended up being redundant
--this.bg_texture = "BG_texture"
end

doDiffs(ifs_assndiff) -- soooooo redundant
doDiffs = nil

--okay pay attention here - this is where we actually go ahead and lay out the buttons that we defined at the beginning
ifs_assndiff.CurButton = AddVerticalButtons(ifs_assndiff.buttons,ifsassndiff_vbutton_layout)

AddIFScreen(ifs_assndiff, "ifs_assndiff") -- add this to the list of screens and we're done![/code]
Again, I'm not going to cover the details of how to get this screen called, but remember what I said in Part 5 about PushScreen, and apply that to a button or a key or something using the methods I showed here.

Conclusion

This is it for now. If it seems disorganized, let me know and I'll come back and edit things for clarity. I will also compile a list of the most helpful/commonly used global variables and functions involved in interface scripting and add that somewhere in here as well. Edit: See below, I've done it!

Helpful Functions, Callbacks, and Properties Directory

I just spent a couple hours putting together this wonderful list of some of the functions, callbacks, classes, properties, etc that I think are most relevant/helpful. This comprises probably 15% of the entirety of the interface library but the other 85% is a combination of helper functions that run in the background, and really confusing, niche stuff that has very few practical uses; I'll let you guys explore on your own if you find that my list is missing something you'd like to use.
Enjoy!
Hidden/Spoiler:
[code]
Objects & Properties

- common properties {type, x, y, width, height, alpha, ScreenRelativeX, ScreenRelativeY, ZPos, vis, ColorR, ColorG, ColorB}
- NewBorder {localpos_l, localpos_t, localpos_r, localpos_b, localpos_w, localpos_h}
- NewIFModel {scale, depth, lighting}
- NewIFText {string, font, halign, valign, textw, texth, style, flashy, startdelay, bg_width, bg_tail, bgleft, bgmid, bgright}
- NewIFImage {texture, localpos_l, localpos_t, localpos_r, localpos_b}
- NewPCIFButton {btnw, btnh, bg_width, hotspot_x/y, hotspot_width/height, <any IFText property>}
- NewEditBox {string, MaxChars, MaxLen, bPasswordMode}
- NewHSlider {thumbwidth, thumbposn, texturebg, texturefg}
- NewIFContainer {<same as common, and can be the parent for other types as well>}
- animation template stuff {fTotalTime, fStartAlpha/fEndAlpha, fStartX/fEndX, fStartY/fEndY, fStartW/fEndW, fStartH/fEndH}

Functions

- IFObj_fnSetZPos(object, position) //kinda obsolete but not really, see next one
- IFObj_fnSetPos(object,x,y,z) //pass nil for any argument to keep current value
- IFObj_fnSetVis(object, visibility) //remember, 1 or nil
- IFObj_fnGetVis(object) //returns a boolean
- IFObj_fnSetColor(object,r,g,b) //pass nil for any argument to keep current value
- IFObj_fnSetAlpha(object, alpha) //range is 0 to 1
- IFObj_fnCreateHotspot(object) //makes the object clickable (and maybe hoverable?)
- IFObj_fnTestHotSpot(object) //returns a boolean re: whether the hotspot was triggered

- IFText_fnSetString(object, string, case) //have never used case or seen in the wild
- IFText_fnSetFont(object, fontname) //e.g. "gamefont_medium"
- IFText_fnSetScale(object, horizontal, vertical) //may cause some ugly stretching
- IFText_fnSetLeading(object, leading) //leading is typography jargon for space between lines, default is 0 here
- IFText_fnGetExtent(object) //mostly used for testing and design, returns width of string in pixels
- IFText_fnGetDisplayRect(object) //returns left, top, right, and bottom extents from center of string
- IFText_fnSetTextBox(object, width, height) //creates a text within an object??
- IFText_fnSetTextBreak(object, type) //only break type found in the wild is "none"

- IFImage_fnSetTexturePos(object,l,t,r,b) //stretches left, top, right, bottom from center of image
- IFImage_fnSetTexture(object, texture, alpha) //alpha is optional

- IFModel_fnSetMsh(object, model) //name of the model referenced in your req/lvl file
- IFModel_fnSetOmegaY(object, omega) //I'm pretty sure this is frequency of rotation about the Y axis
- IFModel_fnSetScale(object, scale) //any number, default is obviously 1
- IFModel_fnSetDepth(object, position) //same as Z position
- IFModel_fnAttachModel(object, attachment, hardpoint) //if this actually works, cool
- IFModel_fnSetTranslation(object,x,y,z) //have not yet tested where this is relative to
- IFModel_fnSetRotation(object,s,x,y,z) //I don't know what s does, you can just nil it

- AnimationMgr_ClearAnimations(object) //clears every animation from this object
- AnimationMgr_AddAnimation(object, template) //template is a table that can be defined inside the argument or beforehand

- AddIFScreen(table, name) //this goes at the end of every ifs file, basically dumps the whole screen to game memory

Callbacks

- ScriptCB_GetFontHeight("fontname")
- ScriptCB_EnableCursor(toggle) //toggle can be 1 or nil
- ScriptCB_PushScreen("ifs_whatever")
- ScriptCB_PopScreen() //no argument, this will always just pop the current screen that is "on top"
- ScriptCB_GetScreenInfo() //returns several values (I think 5 or 6) but you only need the first two which are width & height
[/code]
Last edited by jedimoose32 on Fri Apr 01, 2016 7:45 pm, edited 2 times in total.
Marth8880
Resistance Leader
Posts: 5042
Joined: Tue Feb 09, 2010 8:43 pm
Projects :: DI2 + Psychosis
Games I'm Playing :: Silent Hill 2
xbox live or psn: Marth8880
Location: Edinburgh, UK
Contact:

Re: [Tutorial] Interface Scripting

Post by Marth8880 »

Thank you very much for this. I'll definitely add that using the Lua Development Tools makes UI scripting a whole lot easier thanks to its IntelliSense-like LuaDoc capabilities.
Last edited by Marth8880 on Tue Oct 04, 2016 6:29 pm, edited 1 time in total.
jedimoose32
Field Commander
Field Commander
Posts: 938
Joined: Thu Jan 24, 2008 12:41 am
Projects :: Engineering Degree
Location: The Flatlands of Canada

Re: [Tutorial] Interface Scripting

Post by jedimoose32 »

Yep! Lua Development Tools all day, every day.

Edit: Added a helpful functions/callbacks/properties list to the bottom of the original post. :)
User avatar
Anakin
Master of the Force
Master of the Force
Posts: 4817
Joined: Sat Sep 19, 2009 11:37 am
Projects :: RC Side Mod - Remastered - SWBF3 Legacy
Location: Mos Espa (germany)

Re: [Tutorial] Interface Scripting

Post by Anakin »

Thank you :D i'll read that today

==EDIT==

in the scripts i always find this: ifs_movietrans_PushScreen() instead of ScriptCB_PushScreen(). What's the differences between them??

==EDIT2==

where is ifs_movietrans_PushScreen defined?? I just tried to write a wrapper, but it seams to be unknown.
jedimoose32
Field Commander
Field Commander
Posts: 938
Joined: Thu Jan 24, 2008 12:41 am
Projects :: Engineering Degree
Location: The Flatlands of Canada

Re: [Tutorial] Interface Scripting

Post by jedimoose32 »

ifs_movietrans_PushScreen() is defined in shell/scripts/ifs_movietrans.lua. I know the stock scripts use it a fair amount but it's unnecessary unless you have a movie for the background of the next screen. Basically ifs_movietrans_PushScreen() gets the movie background and the music queued up and ready to play as the screen is transitioning. As far as I know this only applies to the console versions of the game when it comes to menus, but I think it can be used for campaign movie intros as well.
For your purposes you will probably only need to use ScriptCB_PushScreen(). Just keep ifs_movietrans_PushScreen() in the back of your mind in case you do need to transition to a movie.
User avatar
Anakin
Master of the Force
Master of the Force
Posts: 4817
Joined: Sat Sep 19, 2009 11:37 am
Projects :: RC Side Mod - Remastered - SWBF3 Legacy
Location: Mos Espa (germany)

Re: [Tutorial] Interface Scripting

Post by Anakin »

Ok. that explains why i cannot wrap ifs_movietrans_PushScreen. ifs_movietrans is loaded after the gc script happens ;)
Marth8880
Resistance Leader
Posts: 5042
Joined: Tue Feb 09, 2010 8:43 pm
Projects :: DI2 + Psychosis
Games I'm Playing :: Silent Hill 2
xbox live or psn: Marth8880
Location: Edinburgh, UK
Contact:

Re: [Tutorial] Interface Scripting

Post by Marth8880 »

jedimoose32 wrote:Yep! Lua Development Tools all day, every day.
Totally! :D Except for when it, you know, starts to use upwards of >4GB of RAM and eventually crashes because of it. :u
MileHighGuy
Jedi
Jedi
Posts: 1194
Joined: Fri Dec 19, 2008 7:58 pm

Re: [Tutorial] Interface Scripting

Post by MileHighGuy »

Sorry for the bump, but I thought I would post here since this topic is already on the FAQ

finally figured out some more details about how IF screens work. Some of you might have known, but its not explained anywhere yet.

all interface screens are stored on a stack, imagine a deck of cards. The screen you are seeing is always the top of the stack.

ScriptCB_PushScreen(screenName)
pushes to screen, once current execution finishes.
It is not like a return statement, if you call another function after calling this, it will finish the next function, then push the screen.
When you Push a screen, the screen.Exit() fucntion of you current screen is called, and then the screen.Enter() of the new screen is called.. PushScreen sets bFwd to 1 in the Exit and Enter functions.

ScriptCB_PopScreen()

Pops the current screen to the previous in the stack. bFwd is nil for the Exit function of the current screen and the Enter function of the previous screen.

ScriptCB_PopScreen(screenName)

this one is rarely used, but it works differently. It pops every card from the top of the stack until it arrives to screenName. bFwd is nil again

ScriptCB_SetIFScreen(screenName)

this one only changes the top of the stack. It Pops/Exits the current screen, and the Pushes/Enters to screenName. bFwd is 1.

ScriptCB_IsScreenInStack(screenName)
pretty simple, returns true/false if the screen is in the stack

in the code, interface screens typically are name starting with ifs_.... The main menu is an interface screen, mission select is one. they are created with the function NewIFScreen and are added to the game with the function AddIFScreen.

the ifs_freeform scripts are galactic conquest and ifs_campaign are the campaign. Its when you look at the galactic map

Its important to use the screen system as intended. If you only push to screens over and over, it might run out of screen memory. I believe it is around 40 screen, but not sure.


A common way of editing game screens is overriding the ScriptCB_DoFile function to redirect it to load your screen. Put this at the top of your mission script. for example:

Code: Select all

originalDoFile = ScriptCB_DoFile

ScriptCB_DoFile = function(filename)

    print("debug: DoFile loaded " .. tostring(filename))

    if(filename == "ifs_pausemenu") then
        originalDoFile("ifs_pausemenu_custom")
    else
        originalDoFile(filename)
    end
end
If you want to override only part of the screen, not the whole thing, you can override AddIFScreen. You would put this after ingame.lvl is loaded in your mission script, since thats when AddIFScreen is defined.

Code: Select all

if AddIFScreen then

    originalAddIFScreen = AddIFScreen

    AddIFScreen = function (screen, screenName)

        print("debug: AddIFScreen args " .. tostring(screen) .. " " .. tostring(screenName))

        if screenName == "ifs_pausemenu" then

            -- add new button without remaking the whole pause menu
            local newButton = { tag = "invincible", string = "cheats.invincible_on", }
            table.insert(ifspausemenu_vbutton_layout.buttonlist, newButton)
            ifs_pausemenu.CurButton = AddVerticalButtons(ifs_pausemenu.buttons,ifspausemenu_vbutton_layout)

            originalAddIFScreen(screen, screenName)
        end

        originalAddIFScreen(screen, screenName)
    end
else

    print("debug: function AddIFScreen not defined yet yet")

end
Now if you add a new button to the pause menu and want to make it do anything, you have to edit the ifs_pausemenu.Input_Accept function. Input_Accept is one of the functions that any IFScreen can have, like Enter and Exit. it gets called when you click while on the screen. the "tag" of the button sets "CurButton". so just add an if statement to check if the CurButton is your button, and then do whatever you want, like push to another screen.

As far as I know, you can't add any new buttons to a screen after it has been added (after AddIFScreen is called). You can only toggle their visibility.

If you want to edit screen from the main menu, campaign, or galactic conquest, you have to put the code in a custom_gc lvl which the v1.3 patch allows. this is because it would load that before anything else in the load order.

read more about Lua environments here: https://github.com/BAD-AL/SWBF2_Xbox_mo ... vironments
Post Reply