Skip to content

A quick guide to Script API

ivan-mogilko edited this page Apr 22, 2024 · 3 revisions

Engine is capable to export functions and variables to the script, thus letting gamedevs to access them. This is known as AGS Script API. In terms of scripting engine's exports behave like the ones created in script, but they are implemented as a real C++ code inside the engine. This article explains how this is done and how one can extend or otherwise modify Script API.

Script declarations

We begin with script declaration as this is the simpliest part. Engine's API is declared in a file called agsdefns.sh. This file is located in Editor/AGS.Editor/Resources and is embedded into the editor application as a resource. When the game scripts are compiled this file is copied into the temporary script header named _BuiltInScriptHeader.ash which is then included into every script module prior to any other header. The content of agsdefns.sh should be treated as a regular AGS script header in every aspect (syntax etc).

Engine exports

Engine features exports table implemented as SystemImports class (for some reason). The table is mapping script symbols (names) to actual objects and functions. SystemImports class is rarely used directly though. There's a group of helpers which register API functions and objects in the table. They are declared in script_runtime.h like this:

// Registers static function
bool ccAddExternalStaticFunction(const String &name, ScriptAPIFunction *pfn);
// Registers class member function (first argument is always pointer to the class's object).
bool ccAddExternalObjectFunction(const String &name, ScriptAPIObjectFunction *pfn);
// Registers plugin's function. Engine does not know return value and arguments this is why it's converted to void* pointer.
bool ccAddExternalPluginFunction(const String &name, void *pfn);
// Registers "static", unmanaged object, which is created in regular "memory" and is not reference-counted.
// Used only for global script variables like 'game' or 'player'.
bool ccAddExternalStaticObject(const String &name, void *ptr, ICCStaticObject *manager);
// Registers "static" global arrays, such as 'gui[]' or 'character[]'.
bool ccAddExternalStaticArray(const String &name, void *ptr, StaticArray *array_mgr);
// Registers "dynamic", managed objects that have predefined script names, such as gUIs, characters and so on.
bool ccAddExternalDynamicObject(const String &name, void *ptr, ICCDynamicObject *manager);
// Registers script module's export. This one is not interesting for us now.
bool ccAddExternalScriptSymbol(const String &name, const RuntimeScriptValue &prval, ccInstance *inst);

Additionally there is a specific variant for exporting engine functions to plugins:

bool ccAddExternalFunctionForPlugin(const String &name, void *pfn);

Plugins acquire unsafe void* pointers which they have to cast to the proper function pointer themselves, while script interpreter demands function pointers of exact prototype for its own use. This is why functions are registered twice: first time for our own script interpreter, using ccAddExternalStaticFunction or ccAddExternalObjectFunction, and second time for plugins, using ccAddExternalFunctionForPlugin.

We will examine all of these registration sorts below.

API exports must be registered during game's initialization after necessary objects are created in memory. It is essential to register everything before hooking up game plugins though, because these may try to acquire some of the script API at their own initialization. Currently game objects are usually registered on their own time as they are created. Script functions on other hand are exported by a call to setup_script_exports which in turn calls multiple subroutines each for one group of exports, like RegisterAudioChannelAPI, RegisterButtonAPI and so on. This is done purely in sake of organization.

Export may be conditional, depending on script API version or game's data format index, or other options (former is preferrable). Thus you may export one of the multiple variants of same script function depending on game setting.

Script function's registration

Functions are registered using ccAddExternalStaticFunction for static or global functions and ccAddExternalObjectFunction for class members.

There's a rule on the name under which the function has to be registered. First of all, this name must match the function declaration in built-in header. Optionally, the name may include a postfix ^X where X is a number of function arguments. For example:

  • "Character::Walk"
  • "Character::Walk^4"

Both are valid name variants. The benefit this postfix is granting is to be able to register more than one variant of a function with the same name but different number of arguments. This is useful when number of arguments has changed between API versions and you want to support both. Also this may come handy when (if) AGS Script will support function overriding.

Variadic functions have a "100" added to the number of arguments. Having a "^10N" postfix means there's a N explicit arguments followed by a variable list, for example:

  • "Overlay::SetText^104"
  • "String::Format^101"

For the struct attributes (properties) the name syntax also corresponds to general script rules:

  • "StructName::get_Attribute" - attribute's getter;
  • "StructName::set_Attribute" - attribute's setter;
  • "StructName::geti_Attribute" - array attribute's getter;
  • "StructName::seti_Attribute" - array attribute's setter;

Script function prototype and ScriptValue struct

Engine's script interpreter demands all registered functions to have very precise prototypes. There's one for "static" functions and one for "class members":

typedef RuntimeScriptValue ScriptAPIFunction(const RuntimeScriptValue *params, int32_t param_count);
typedef RuntimeScriptValue ScriptAPIObjectFunction(void *self, const RuntimeScriptValue *params, int32_t param_count);

As you can see, both accept an array of ScriptValues and return ScriptValue, the only difference is "self" void pointer. RuntimeScriptValue struct is used by interpreter as a universal variable. This struct describes one of several possible values, such as:

  • Integer
  • Float
  • String pointer
  • Managed object
  • Managed array
  • Function pointer

as well as few auxiliary ones used only internally by the interpreter. For the regular script function the integer, float, string and dynamic object are most common. Common value types are assigned simply as scval.SetInt32(number) and scval.SetFloat(number). Assigning booleans is done using function SetInt32AsBool (because bools are stored as ints).

Script objects must be accompanied by a manager that handles operations on it. For historical reasons some objects work as their own managers (refered to as "auto-objects" below), others use singleton managers. Please refer to following topic if you'd like further information: Script objects and Dynamic objects

The examples of assigning objects to ScriptValue: scval.SetDynamicObject(characterPtr, &ccDynamicCharacter), scval.SetDynamicObject(dynSpritePtr, dynSpritePtr), and so on. Managed strings are assigned as const char* pointers with myScriptStringImpl as a manager, e.g.: scval.SetDynamicObject(cstr, myScriptStringImpl);

Script function implementation

The script function's purpose is usually to cast these structs to expected types of values and pass them further into an actual method that implements game logic. If game logic returns a value that value should be cast back into the ScriptValue.

Let's assume engine declares Character.CanWalkTo which accepts a Character* pointer and 2 coordinates and returns a boolean. Following is a most verbose way to wrap the call to such function (for example's clarity):

RuntimeScriptValue Sc_Character_CanWalkTo(void *self, const RuntimeScriptValue *params, int32_t param_count)
{
    CharacterInfo* ch = static_cast< CharacterInfo* >(self); // character objects are represented as CharacterInfo struct
    int x = params[0].ToInt();
    int y = params[1].ToInt();
    bool result = Character_CanWalkTo(ch, x, y);
    // or ch->CanWalkTo(x, y) if function were a CharacterInfo's member
    RuntimeScriptValue rval;
    rval.SetInt32AsBool(result);
    return rval;
}

In practice writing this by hand each time may become tedious. When the script interpreter was refactored we had to wrap all the existing engine API in similar way. To achieve that we've introduced an extensive list of macros. These macros are declared in script/script_api.h and their names all have similar syntax:

API_[CALLTYPE]_[RETURNTYPE]_P[ARGTYPE1]_P[ARGTYPE2]_...

CALLTYPE can be either SCALL (static functions) or OBJCALL (member functions). RETURNTYPE or ARGTYPE can be INT, FLOAT, BOOL, OBJ (for dynamic objects) or OBJAUTO (for dynamic objects that do not require separate manager).

For example:

  • API_SCALL_VOID(FUNCTION) - static function that returns void. Use: API_SCALL_VOID(ClaimEvent);.
  • API_SCALL_VOID_PINT2(FUNCTION) - static function that takes 2 integers and returns void. Use: API_SCALL_VOID_PINT2(SetViewport);.
  • API_SCALL_INT_POBJ_PINT(FUNCTION, P1CLASS) - static function that takes an object pointer and integer argument and returns integer. Use: API_SCALL_INT_POBJ_PINT(GetTextWidth, const char);.
  • API_SCALL_OBJ(RET_CLASS, RET_MGR, FUNCTION) - static function with no arguments that returns object pointer. Use: API_SCALL_OBJ(const char, myScriptStringImpl, Game_GetName);.
  • API_OBJCALL_VOID_PINT2(CLASS, METHOD) - class method that takes two integer arguments. Use: API_OBJCALL_VOID_PINT2(CharacterInfo, Character_FaceDirection).
  • API_OBJCALL_OBJ_PINT_POBJ(CLASS, RET_CLASS, RET_MGR, METHOD, P1CLASS) - class method that takes an integer and object pointer and returns object pointer. Use: API_OBJCALL_OBJ_PINT_POBJ(GUIListBox, char, myScriptStringImpl, ListBox_GetItemText, char);

Exports to plugins

Engine should export same API function to both script and plugin, but because their call method is different they have to be registered separately, but under same name. For plugins we export regular functions directly, casting them to void* pointer.

For example, we may have a function Label_SetColor implementing game logic and a script wrapper Sc_Label_SetColor that converts between ScriptValues and normal variables. Then we register them for script interpreter and plugins:

ccAddExternalObjectFunction("Label::set_TextColor", Sc_Label_SetColor);
...
ccAddExternalFunctionForPlugin("Label::set_TextColor", (void*)Label_SetColor);

API versioning

The official API contents are defined by the builtin header agsdefns.sh, and like with user script there are ways to enable or disable parts using preprocessor commands and flags. The two common methods are API version switch and component switches.

API version switch

The most recommended method involves two options: "Script API version" and "Script compatibility level" and lets you group script API belonging to particular release of AGS.

API versions are registered as enum ScriptAPIVersion in Editor/AGS.Types/Enums/ScriptAPIVersion.cs. Every registered API version corresponds to a pair of preprocessor flags named SCRIPT_API_vXXX and SCRIPT_COMPAT_vXXX where XXX are version numbers (for example SCRIPT_API_v340 and SCRIPT_COMPAT_v340 are defined for ScriptAPIVersion.v340).

When user chooses "Script API version" preprocessor defines all SCRIPT_API_vXXX flags starting from the lowest known and up to the corresponding version inclusively. When user chooses "Script compatibility level" preprocessor defines all SCRIPT_COMPAT_vXXX flags starting from the chosen API version and down to the chosen compatibility level inclusively.

The available script API is determined by two bounds: upper bound determines which new API parts will be enabled, and lower bound determines which obsolete API parts will be disabled. Upper bound depends on defined SCRIPT_API_vXXX macros. For every macro defined the corresponding API contents should be enabled. If certain macro is not defined, then those (newer) API contents stay disabled. Lower bound depends on defined SCRIPT_COMPAT_vXXX macros. For every macro defined the deprecated API contents that were still active in corresponding version are kept enabled; otherwise these are disabled.

For example, if user wants to use script API from AGS 3.5.0 and at the same time some deprecated functions from AGS 3.4.0, then API version is set to v350 and compatibility level to v340. In this case following flags will become defined:

SCRIPT_API_v321
SCRIPT_API_v330
SCRIPT_API_v334
SCRIPT_API_v335
SCRIPT_API_v340
SCRIPT_API_v341
SCRIPT_API_v350
SCRIPT_COMPAT_v350
SCRIPT_COMPAT_v341
SCRIPT_COMPAT_v340

API component switches

There are flags that switch certain API components. At the time of writing these are:

  • STRICT - defined if "Enforce Object Based Scripting" is enabled in General Settings. It was used to deprecate a big number of global functions in favor of OO-style ones.
  • STRICT_AUDIO - defined if "Enforce new-style audio scripting" is enabled. It was used to deprecate old audio API.
  • STRICT_STRINGS - defined if "Enforce new-style strings" is enabled. It was used to deprecate the use of old-style string type (limited to 200 chars) in favor of new String struct.

They are used very simply, like:

#ifndef STRICT_AUDIO
  /// Stops all currently playing sound effects.
  import static void   StopSound(bool includeAmbientSounds=false);
#endif

Above disables StopSound function when STRICT_AUDIO is enabled.

Similar flags may be introduced if you'd like to make one switch that toggles whole relevant group of API items. But the use cases may be limited since different functions in this group may rely on different versions of API.

A note on ifver/ifnver

There are two preprocessor commands that let user to switch code depending on the version of AGS Editor: #ifver and #ifnver. They are used like this:

#ifver 3.5.0
// do stuff for 3.5.0 and above
#endif
#ifnver 3.5.0
// do stuff for versions below 3.5.0
#endif

We strongly suggest you DON'T USE THEM for builtin API. The problem is that these commands refer to Editor's version, while usually you want same Editor to be able to support multiple API versions.

Typical changes: ammending API

When you simply add new items to script API make sure you put them under corresponding API version like this:

#ifdef SCRIPT_API_v350
  /// Gets/sets text alignment inside the button.
  import attribute Alignment TextAlignment;
#endif

This way such item will only be included if user set Script API version 3.5.0 or higher. It does not matter whether this is a global function, member method, global variable or a new type (struct), that will work in either case.

NOTE: it's recommended to put new API to the end of function list (in struct or related group of global functions): that helps finding them and also reduces number of #ifdef/#ifndef blocks.

Typical changes: deprecating API

When you want to deprecate some item, for example if you have provided new better variant, make sure you put them under compatibility level corresponding to the previous version (as the last version they were still in use) like this:

#ifdef SCRIPT_COMPAT_v350
/// Returns which walkable area is at the specified position on screen.
import int  GetWalkableAreaAt(int screenX, int screenY);
#endif

This way such item will only be included if user set compatibility level 3.5.0 or lower.

Typical changes: overriding API

Overriding means that new API contradicts with the old one and cannot coexist during same compilation. Examples of overriding are:

  • Changing values of constants;
  • Changing number of function parameters and return values (at the time of writing AGS script does not support function overriding);
  • Changing default values of function parameters;
  • Renaming a variable in struct (keeping both old and new variant will break the data layout).

When you are doing this you cannot simply combine ammending and deprecation, because user may at the same time have enabled new API and old compatibility level. Instead you need to tell preprocessor to include new variant if newer API version is enabled and include old variant if it is not (meaning - user set older API version). For example:

#ifdef SCRIPT_API_v350
  /// Locks the character to this view, and aligns it against one side of the existing sprite.
  import function LockViewAligned(int view, int loop, HorizontalAlignment, StopMovementStyle=eStopMoving);
#endif
#ifndef SCRIPT_API_v350
  /// Locks the character to this view, and aligns it against one side of the existing sprite.
  import function LockViewAligned(int view, int loop, Alignment, StopMovementStyle=eStopMoving);
#endif

The first function will be included if API version is 3.5.0 or higher, and the second if API version is lower than 3.5.0.