Skip to content
Ivan Mogilko edited this page Mar 19, 2017 · 2 revisions

Overview

AGS script is being compiled into binary form, which consists of executed byte-code (instructions), tables of symbol imports/exports, and global data array. At runtime more memory storages are associated with an executed script. The script running logic in AGS is more or less simulating x86 processor architecture and assembly language, with certain things added on top at later stages (such as managed memory pool).

Runtime script memory

There are four kinds of memory storages created by the interpreter:

  • Static global memory
  • Registers
  • Function stack
  • Managed object pool (dynamic memory)

Static global memory

Static memory consists of arrays allocated one per each script module. Their sizes are always fixed.

This array of data is implemented as ccInstance::globaldata.

Currently AGS leaves no indication on actual value types nor sizes for objects written in this memory, only the addresses of their first byte (aka variable's offset). In theory it is may be possible to deduce the size of an object by comparing addresses of adjanced variables, but such method is not 100% failproof. Besides, even if the size is known, there is no way to tell the actual type of data. For this reason this memory is "unmanaged" one. The only safe checks we can do are: ensure that memory is accessed at correct offset (certain variable address), and that memory is not being read beyond the end of array.

The script module, when loaded, can export particular variables of its static memory. This is done by registering a new entry in the symbol imports table, with a variable's memory address. Since the script unit has to be existing for its export to stay valid, such variables may only be used when both exporter and user scripts are loaded at the same time. For that reason room scripts cannot use variables of each other.

All the global variables registered for the module are stored as ccInstance::globalvars map.

Static memory can be accessed for reading and writing at any time, assuming script module is loaded.

The contents of static memory of every script, that has been loaded at least once, is written into savedgame and loaded back when a save is restored.

Registers

Each execution thread of the interpreter has its own set of registers, but their meaning and purpose is always the same. This is a small array of integer variables, which are being used as a short-term storage for interim values when executing a script instruction.

They are implemented as ccInstance::registers array.

Their contents are never written to savedgame.

Function stack

Each execution thread of the interpreter has its own function stack.

Stack's maximal capacity is limited, but the limit is rather arbitrary, and may be changed if necessary.

In code stack is implemented as ccInstance::stack, and auxiliary ccInstance::stackdata array for allocating larger objects (of unmanaged user types).

Following data is pushed to stack:

  • Function local variables;
  • Function arguments and return values;

Stack is ... stack, which means new data entries can only be appended to the end, not inserted in the middle, and unneeded data is being removed only from the end too. However, stack allows to get or set data values freely for any variable it stores.

Due to the latest stack's implementation, unlike global memory, it is possible to know the general kind of data stored on stack; the only exception are the unmanaged user structs that have no means to describe their contents at runtime. When working with stack, the safe checks are being made to ensure that data is being accessed at valid offsets, that there is no reading or writing beyond stack's end, and that stack gets emptied correctly when existing the script function.

Stack is never written to savedgame, which (among other things) implies that a savedgame cannot be created while inside a script function.

Managed object pool

All the real game objects, created either at game start or by script's command, are kept in the managed object pool. This pool is a map, where the key is an integer value called "handle" and value is an object pointer. Manipulating managed objects in script is always done via handles, not real memory pointers or offsets, as opposed to global and local script variables. This makes these objects better protected from misuse. Along with that managed objects can describe themselves, telling information on their types, and sometimes contents.

In code managed pool is implemented as ManagedObjectPool class and the only global object of its type.

Managed objects are reference-counted. Whenever a handle is assigned to "reference" variable in script, the reference counter is incremented. Whenever such reference is lost for any reason (e.g. different reference assigned, or variable is deleted from memory), counter is decremented. When counter is 0, managed object is destroyed.

Note that there are "static" game objects, such as characters or inventory items, that exist until the program ends, even if there are no user-created references for them in script. This is because engine itself holds the "final reference" to them, declaring a read-only global reference variable to to such object, which cannot be overwritten.

Managed object pool's state is fully written to the savedgame. Not every type of object is saved this way though. Most game objects are saved outside of script data, in their own turn, and managed pool only writes information necessary to restore all references and bind them to actual objects. On contrary, limited number of types, such as dynamic arrays and Dynamic Sprites, are saved with the pool, because they do not exist without user-created reference to them.