Jump to: navigation, search




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).


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:


If the node only contains subnodes:


If the node contains both properties and subnodes, we must use an unnamed block on the very top of the node definition:


A small example:

window employees {
	pic background {
		pos "0 0"
		size "1024 768"
		image "foo_image"

Main objects


The description of the UI is defined with a tree of nodes.

You can check the autogenerated documentation


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).




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".


We can use it to:

  1. split a big GUI into more simple sets of nodes;
  2. define a style for many nodes (button...);
  3. 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.


The body of the component instance can define new properties, new nodes, or override existing ones.


Some special nodes don't use properties, but a sequential language, and must be a leaf.


Functions are leaf nodes of a window, with the type func.

	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 are script functions that are defined from within the scripts. They are available as commands like all other script commands.

confunc uniqueID
	cmd "someCommand;"


This is not implemented. It should be an event call when a cvar value changes.



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".


You can call a function or a confunc with a call:



Expressions (created with tokens and operators) are used in setters and conditions.

  • 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.


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"
Reserved words
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


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 ) {
	} elif ( EXPRESSION ) {	
	} else {

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.


	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>;" }


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


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.


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.


  • We can think about merging bindui into UI script (a property with the key), and using keys.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.


Personal tools