UFO-Scripts/ui/*.ufo
General
Convention
All UIs (also called "menus") in UFO:AI are script based. It's a convention that all UI components (often a window) are placed in files with UINAME.ufo (where UINAME is to be replaced).
Syntax
Each UI description uses a tree of nodes, and the root must be a window
node. A node is an element of the UI which can contain some properties, according to is type (we call it behaviour); and node child.
If the node only contains properties:
NODETYPE NODENAME { PROPERTIES }
If the node only contains subnodes:
NODETYPE NODENAME { NODES }
If the node contains both properties and subnodes, we must use an unnamed block on the very top of the node definition:
NODETYPE NODENAME { { PROPERTIES } NODES }
A small example:
window employees { pic background { pos "0 0" size "1024 768" image "foo_image" } }
Main objects
Nodes
The description of the UI is defined with a tree of nodes.
You can check the autogenerated documentation
- UI node behaviours, the documentation
- an UML like diagram summary
Windows
A window is a node. A tree of nodes must start with a window. Every window needs a unique identifier.
window bases { [...] }
The identifier of this window is bases. You can push the window onto the stack by using the command mn_push bases
Window inheritance
It is possible to define a window that inherits content from another window. Use the "extends" keyword in the window identifier:
window hudinv extends inventory { [...] }
This defines a new window hudinv which inherits the content of a previously defined window inventory. This provides a basic means of re-using some window content. The inventory.ufo is a simple example which defines two soldier inventory windows (one for the HUD and another for the alt-HUD) that inherit most of their content from a generic inventory window.
A window that extends another can also override inherited content, simply by redefining nodes (see singleplayer.ufo for an example). Note: it is not yet possible for a window to extend more than one other window. However chains of window extension are possible (e.g. window C extends window B which in-turn extends window A).
Finally the order of windows is important... if window B extends window A then window A must be defined first (in general both windows should be in the same ufo file with window A defined at the top).
Options
...
Components
A component is a clonable set of nodes. It is internally a named tree of nodes, and it reuses the same syntax prefixed with "component".
component COMPONENT_NAME extends NODE_BEHAVIOUR|SUPER_COMPONENT_NAME { { NODE_BEHAVIOUR_PROPERTIES } SUBNODES }
We can use it to:
- split a big GUI into more simple sets of nodes;
- define a style for many nodes (button...);
- define a node structure we want to reuse
Furthermore it can (in cases 2 and 3):
- Reduce memory consumption (component instance reuse component strings without cloning strings)
- Reduce the parsing time (we remove a lot of duplication in the script)
We instantiate a component like any other node.
COMPONENT_NAME COMPONENT_INSTANCE_NAME { ... }
The body of the component instance can define new properties, new nodes, or override existing ones.
Actions
Some special nodes don't use properties, but a sequential language, and must be a leaf.
Functions
Functions are leaf nodes of a window, with the type func
.
func FUNCNAME { cmd "someCommandName;" }
If a function of a node uses the name of an event (of this node), the function will be linked into the event node. Currently, in this case, the func node continues to exist (and then we can call it), but we should remove and delete this node soon (without deleting the link into the event). This method is often used for onInit
, onClose
, onEvent
of a window node. See #Window for more information about this event.
Confunc
Confunc are script functions that are defined from within the scripts. They are available as commands like all other script commands.
confunc uniqueID { [..] cmd "someCommand;" [..] }
Cvarfunc
This is not implemented. It should be an event call when a cvar value changes.
Scripting
Command
You can call client command and confunc with a cmd
. Param of the command is the same syntax you can use in the console:
cmd "fooCommand;"
You can call more than one command:
cmd "fooCommand;barCommand;"
You must understand that all commands are put into a buffer and not called when we execute the function.
*cvar:a 0 cmd "set a 1;"
After executing this little code, if we test a
, a
is equal to "0", but in the next frame it will be equal to "1".
Call
You can call a function or a confunc with a call
:
call FUNCTIONNAME
Expressions
Expressions (created with tokens and operators) are used in setters and conditions.
- EXPRESSION := ( EXPRESSION BINARY_OPERATOR EXPRESSION ) | UNARY_OPERATOR EXPRESSION | VALUE
- VALUE := *node:PATH| *cvar:NAME | BOOLEAN | NUMBER | STRING
- BOOLEAN := true | false
- STRING := must be a quoted string
The expression parser is very simple. It is very important to use "()" every time we use a binary operator. Instead of "1 + 2 + 3", we should use "( ( 1 + 2 ) + 3 )". Patches are welcome :-)
We can use injection in string, number, cvar name, or node path.
Setter
Action blocks can use a setting command to edit a cvar or a node property.
To set a cvar, we must use the prefix cvar:
.
*cvar:foo = 1
For compatibility, we use 2 syntaxes to edit a node property. The older one allows us to edit a node relatively to the current window. This one is deprecated, we should remove it to clean up scripts and code; but at the moment we use it every where.
/* edit the child "foo" of the current window */ *foo@disabled true
The second one uses an absolute path to identify a node, and some reserved word allows us to use a relative path. We must use (for compatibility) the prefix node:
, but the idea is to remove this prefix when we no longer use the old syntax.
writing comment: move thing about node path into it's own section about path.
*node:foowindow.foonode.foonode@pos "10 10"
this |
the current node (the source of the event) |
parent |
the parent of the current node (node:parent ), or the parent of the current node into the path (node:foowindow.foonode.parent )
|
root |
the root (a window node) of the current node (node:root ), or the root of the current node into the path (node:foowindow.foonode.root )
|
button foo { /* display another button */ click { *node:this@invis true *node:this.parent.foo2@invis false } }
Conditions
An action block (function or event) can use conditions. It allows for the testing of cvars, node properties, and constant values in an if
block with facultative elif
and else
block. This syntax doesn't allow for the use of the command preprocessor in the condition (<...>
), but you can first set a cvar with injection and then test the cvar.
Syntax (else
block is facultative):
if ( EXPRESSION ) { ACTIONS } elif ( EXPRESSION ) { ACTIONS } else { ACTIONS }
An operand can be a cvar, constant, float, or string (according to the operator), and can't contain space characters. A cvar must use the prefix *cvar:
. No problem to check two constant values or two cvar.
To check a float you may use the operators: ==
, >=
, <=
, >
, <
, !=
:
if ( *cvar:mn_equipsoldierstate == 1 ) { cmd "mn_pop;mn_push aircraft;" }
To check a string you may use the operators: eq
, ne
:
if ( *cvar:mn_installation_type eq ufoyard ) { ... }
And we can check if a cvar exists:
if ( exists *cvar:mn_installation_type ) { ... }
We also can test some node properties, but only if they are numeric ones. We must use the prefix *node:
. We can use the full path of a node or a relative path, with the reserved words this
, parent
and root
to identify a node from the current one (the source of an event, or the function called).
confunc foo { if ( *node:foowindow.foonode@disabled == 1 ) { ... } } checkbox foo2 { ... change { if ( *node:this@current == 0 ) { ... } } }
Linking client and GUI
To call a specific function when a user clicks on a text control, you have two choices on how to integrate the script function.
- add an instruction in CL_InitLocal function, in
/src/client/cl_main.c
file. - use a confunc (see above)
First example will look like this:
Cmd_AddCommand("textname_click", FunctionName);
Example :
Cmd_AddCommand("ships_click", CL_SelectAircraft_f); Cmd_AddCommand("ships_rclick", CL_OpenAircraft_f);
Second one is only to define a confunc with the name textname_click Example:
confunc ships_click { *ships bgcolor "1 1 1 1" }
This example will change the bgcolor attribute of the node named ships to a V_COLOR value of 1 1 1 1 (white)
Command preprocessor
UFO:AI script uses a preprocessor for commands called from node event and from confunc. It is useful to have a bigger abstraction between the client code (C code) and the GUI code (script), providing both with a more generic interface. It provides the ability to manage more things script based.
- For events
- Generally used as a callback from script to client. It allows to inject property value of the source (the node that sent the event). For example, the spinner node uses it to provide to the client: on change, the difference of the change, or the current value...
- For confunc
- Generally used as a callback from client to script. It allows to pass parameters and update nodes. We can also inject confunc properties :-)
Param of confunc
/** * @param[in] <1> id of the item * @param[in] <2> quantity of the item in the base */ confunc buy_updateitem { *bt_buysell<1> current <2> }
If the client sends "buy_updateitem 3 2008
", the confunc will set the current
property of the node bt_buysell3
to 2008. All data types are not implemented. When it's needed, we can update the code to support more types of properties.
Properties
spinner bt_buysell2 { min 0 max 20 delta 1 current 10 change { cmd "mn_buysell 2 <lastdiff>;" } }
When the user clicks on the spinner, his status often changes (we or not in min or max, the delta is not null...) In this case, the node will execute the event command and call "mn_buysell 2 1
" (or -1 if we click on bottom).
We can think about injection every properties we want:
change { cmd "mn_buysell 2 <value> <name> <width>;" }
Cvar
Injection of a cvar value can be useful for reducing dependencies on the client code. They only receive a value, and the UI script can manage it's own local var in cvar.
<cvar:name of a cvar> | Allow to inject the string of a cvar |
Path
When we call a command from a node event, the command doesn't know the right window or the node calling it. That why we often use the current active window and a node name (deprecated because we use more and more popup) on the command, to compute the node source. Now we use path to identify a node, with the name of all parents from the root to the node. Problem is it very verbose, and dependent of the node path (if we rename the window, move node into another panel... we need to update all the code). That why we can inject some path. The current code allow only tree path injection.
<path:this> | Allow to inject the path of the current node |
<path:root> | Allow to inject the path of the current root (the name of the current window) |
<path:parent> | Allow to inject the path of the parent of the current node. |
This example comes from ufos/ui/checkcvars.ufo
. We can rename the window, or move this 2 nodes into another panel, the code will work.
optionlist select_language { ... whup { cmd "mn_active_vscrollbar <path:parent>.select_language_scroll 0;" } whdown { cmd "mn_active_vscrollbar <path:parent>.select_language_scroll 4;" } } vscrollbar select_language_scroll { ... }
Dynamic node name for setter
We can inject a value into the name of the node with the property setter syntax.
*bt_autosell<1> tooltip "_Item autosell disabled"
UFO:AI doesn't use any dynamic memory allocation, for the GUI, whatsoever. Everything is allocated at loading time. It's technically more difficult to set a value with random size (like text), but with this syntax we can set the text property of a random node to a constant text value. It's what we see on this example.
Binding
Bind descriptions are in the file "base/keys.cfg
". Some of command line use "bindui
", it is the current way to associate a key with a node, and then with the GUI. We use a separate file to allow the player to use his own bind.
A key associated with a node will only work when the window of the node is on top. The node and his parents must be visible and enabled. By typing the key, the associated node will be "activated", we will send to the node an event fake, a left mouse button click. It is often like calling onClick
event of the node, but for checkbox, or radiobutton, it will update the status and call onChange
if it need.
In the same way, we can bind func
or confunc
nodes to execute function body (without parameters). But it is more interesting to bind a graphical node, because a tooltip with the right key is displayed for the user.
Improvements:
- We can think about merging
bindui
into UI script (a property with the key), and usingkeys.cfg
to custom binds). - We can think about embedding key description into translation file to use a more natural key for each languages. Then, instead of using the bind key
DEL
, we can use_removearmor:DEL
. The syntax should be explicit for the translator, and should contain a default value.
Node events
Nodes have properties for callback events. The property type is V_UI_ACTION, you can find a list of all callbacks in the documentation UI node behaviours.
It allows us to script an event. It can be a common event, like a mouse event. But it can be a specific behaviour event, for example the container
node define a onSelect
event.
We often define a script event when we define the node.
pic aircraft_return { [...] onClick { cmd "echo click;" } onMouseLeave { cmd "echo mouse enter;" } onMouseEnter { cmd "echo mouse leave;" } }
But we can also update the script at runtime.
pic aircraft_return { [...] // we must click one time to receive echos onClick { *aircraft_return@onMouseEnter = { cmd "echo mouse enter;" } *aircraft_return@onMouseLeave = { cmd "echo mouse leave;" } } }
Then, we can change events into an event, but also any other attribute in the same way. You can change the size, color or whatever you like.
Node methods
Nodes have properties for very simple methods (no return value, and no parameters). The property type is V_UI_NODEMETHOD, you can find a list of all methods in the documentation UI node behaviours.
We can call it with the call
command.
// myText is a scrollable node, like a test node. // This function resets the scrollbar, we scroll the content on top. call myText@moveHome
We can also use a key binding in this method.