View on GitHub

FOnline Engine

Flexible cross-platform isometric game engine

Scripting

Engine-owned documentation. This page describes reusable scripting runtime behavior in Source/Common/ScriptSystem.* and Source/Scripting/; concrete game scripts, quests, rules, and content policy belong to the embedding project.

Purpose

The scripting layer is the contract between the C++ engine runtime and game-authored behavior. It exposes engine entities, global services, events, remote calls, value types, collections, reflection helpers, and tool/frontend helpers to script code while keeping C++ ownership, metadata, nullability, persistence, networking, and validation in the engine.

Read this page together with:

Source paths inspected

Layer map

The scripting subsystem has four layers:

  1. Common runtime facadeSource/Common/ScriptSystem.h / .cpp define the backend-agnostic ScriptSystem, ScriptFuncDesc, ScriptFunc, FuncCallData, DataAccessor, native call adapters, init functions, loop callbacks, and type maps.
  2. Backend implementationSource/Scripting/AngelScript/ provides the current production backend. Mono and native scripting have placeholder/source roots, but AngelScript owns the implemented script compiler/runtime path in this tree.
  3. Script-visible native methodsSource/Scripting/*ScriptMethods.cpp files contain ///@ ExportMethod functions grouped by runtime side and receiver type. Codegen reads these annotations and emits method descriptors/wrappers.
  4. Core script library and game scriptsSource/Scripting/AngelScript/CoreScripts/*.fos provides engine-owned reusable script-side helpers. Embedding projects add their own .fos files and metadata through project configuration and resource/script baking.

The engine owns the reusable bridge. The embedding project owns game scripts and chooses which features are enabled through project configuration, build presets, and .fomain inputs.

ScriptSystem: backend-neutral dispatch

ScriptSystem is the C++ runtime facade used by client, server, mapper, tests, and script-aware tools. Its main jobs are:

ScriptFunc<TRet, Args...> normalizes native arguments into FuncCallData and catches script exceptions so callers can continue after a failed script callback. It retains return-value cleanup state only for non-void return types; void callbacks have no return storage to clean up when delayed callbacks are moved or destroyed during entity teardown. NativeDataProvider and NativeDataCaller adapt C++ arrays, dictionaries, entities, callbacks, value types, and mutable references to the generic call representation.

This boundary is also where generated nullability checks are inserted. NativeDataProvider::CheckArgNotNull() and CheckReturnNotNull() are called by codegen-generated MethodDesc::Call lambdas, not only by the AngelScript adapter. See Nullability.md for the full contract.

AngelScript runtime path

InitAngelScriptScripting() in Source/Scripting/AngelScript/AngelScriptScripting.cpp prepares the AngelScript runtime, creates an AngelScriptBackend, registers it at ScriptSystemBackend::ANGELSCRIPT_BACKEND_INDEX, and loads binary scripts from resources.

CompileAngelScript() is the compiler-side entry point used by tools/tests. It creates a standalone ScriptSystem, registers metadata, compiles text script files, and returns bytecode.

AngelScriptBackend owns the concrete engine instance and module lifecycle:

AngelScript is therefore used in two modes: compile-time tooling mode and runtime mode. The same metadata and type registration code must remain compatible with both.

Native methods registered through generated MethodDesc descriptors are invoked through ScriptGenericCall(). The unified FuncCallData slot for a mutable simple argument is the address of the caller’s variable — the value itself for primitives/enums/value types (int32&, mpos&, string&), the handle cell for object handles (Critter@&). Every AngelScript-side producer follows this contract: ScriptGenericCall() (classifying by the registration-time MethodDesc/EntityEventDesc argument descriptors — the same data that emitted the &/@& declaration) and the Invoke family resolve mutable arguments through asIScriptGeneric::GetArgAddress() (the pointer held on the stack), while ordinary input arguments use GetAddressOfArg(). Consumers rely on it symmetrically: NativeDataCaller::ConvertArg/ReturnArg read and write back through the slot, and the AngelScript-to- AngelScript branch of ScriptFuncCall() (script-fired events with by-ref args, Invoke targeting a script function) passes the slot straight to asIScriptContext::SetArgAddress(). Regression coverage: Test_CommonScriptMethods.cpp (TimePackingOperations, GameInvokeOperations/ByNameWithRefArgs) and Test_ScriptEntityOps.cpp (AdvancedServerOperations/CustomEntityEventRefArgs).

When asEP_ALLOW_UNSAFE_REFERENCES is enabled, AngelScript may defer releasing method receivers and arguments until an expression reaches a safe point. Short-circuit boolean compilation processes the left operand’s deferred parameters after materializing its primitive bool result and before merging the branch bytecode. Otherwise the right operand can reuse a temporary object slot and overwrite the retained receiver without releasing it. ScriptBuiltinsDeferredReceiverTemporaryIsReleased covers the property-accessor plus method-call form that exposed this during GUI shutdown.

AngelScript backend shutdown

~AngelScriptBackend() tears the runtime down in a fixed order: stop the debugger endpoint, run the registered cleanup callbacks, reset the context manager, then call asIScriptEngine::ShutDownAndRelease() while script modules, object types, behaviours, and backend links are still intact. The AngelScript shutdown path calls every module’s CallExit(), uninitializes global variables, runs repeated full GC passes until the live set is empty or no longer makes progress, discards modules, and reports any object that still cannot be destroyed. There is no fixed pass limit: script destructors may create another finite collectable graph that needs a subsequent pass. After the engine is released, the backend resets _meta / _scriptSys / _engine / _entityMngr and runs post-cleanup callbacks.

Global variables, delegates, script object handles, arrays, dictionaries, and GUI object graphs must be cleaned by module shutdown, destructors, ReleaseAllHandles, and the AngelScript GC. Embedding-project scripts should not add Game.OnFinish / EngineCallback_Finish cleanup just to silence shutdown diagnostics; if a graph survives shutdown, fix the owning native release/GC enumeration bug.

Entity deletion/unload clears the entity’s own event callbacks and time events from Entity::MarkAsDestroyed(), so embedding-project scripts should not keep central per-entity unsubscribe / StopTimeEvent registries for ordinary entity lifetime. Entity mutators and event/time-event entry points assert or verify when called after MarkAsDestroyed(), making accidental attempts to repopulate a destroyed entity show their stack trace at the offending call. During ServerEngine::Shutdown / ClientEngine::Shutdown, the engine also runs UnsubscribeAllEvents() + ClearAllTimeEvents() on the global engine entity and all live entities before DestroyAllEntities(). Embedding-project scripts should not hand-maintain unsubscribe / global-clear / StopTimeEvent cleanup in their Game.OnFinish handler purely to keep the GC quiet — only genuinely functional teardown belongs there.

Attributes, declarations, and metadata

Source/Scripting/AngelScript/AngelScriptAttributes.cpp parses engine-specific script attributes and declaration tags. Important contracts include:

These attributes are source-level contracts. AngelScript sees normalized declarations after preprocessing, while engine metadata and analyzers retain the higher-level FOnline-specific meaning.

Entities and properties in scripts

Source/Scripting/AngelScript/AngelScriptEntity.cpp registers script object types for engine entities, singleton-like components, property accessors, entity event types, and method dispatch. It bridges generated metadata with AngelScript registration calls so script code can work with engine entities through script-visible names such as critters, items, maps, locations, players, prototypes, abstracts, statics, holders, and property-backed components.

Entity lifetime is still owned by the engine runtime:

Use EntityModel.md for entity/property/prototype ownership and Persistence.md for database boundaries.

Remote calls and event callbacks

Source/Scripting/AngelScript/AngelScriptRemoteCalls.cpp registers remote caller object types such as RemoteCaller and CritterRemoteCaller. Remote-call declarations are metadata-backed, and runtime handling is split by side:

Events and remote calls are intentionally separate concepts. Events describe engine/runtime lifecycle and gameplay notifications; remote calls describe network-addressable script entry points. Both rely on metadata signatures, nullability contracts, and generated descriptors.

Native script method exports

Native script APIs are grouped by file name:

Each exported function is marked with ///@ ExportMethod and normally starts with a side/type prefix such as Server_Map_, Client_Game_, Common_ImGui_, or Mapper_Game_. Codegen turns these declarations into script-visible method descriptors and backend call wrappers. Trailing C++ default parameters are preserved in metadata and restored in the AngelScript registration declarations, with C++ value-type defaults such as fpos32 {} normalized to script expressions such as fpos(). Prefer a single exported method with defaults over duplicate overloads that only append optional arguments. See ScriptMethodsMap.md for the per-file map and counts.

For entity instance methods, the AngelScript dispatch layer validates the receiver before entering the native method body. Entity_MethodCall calls CheckScriptEntityAccessAndNonDestroyed, which checks server sync coverage and destroyed state for the self entity. Do not add an entry-only ValidateEntityAccess(self) or repeat the receiver check before ordinary receiver reads. Later in the body, validate entities only at real access/assert boundaries such as event dispatch or post-reentry continuation. When native code first needs to extend the current cover, use EnsureEntitySynced(...) rather than pairing it with a redundant immediate ValidateEntityAccess(...).

When adding a method, route it to the side that owns the state it mutates. For example, authoritative item creation belongs under server methods, while sprite/UI helpers belong under client/common frontend methods.

Client render helpers such as Game.DrawSprite, Game.DrawSpritePattern, and Game.DrawSpriteRegion are valid only during render-facing script callbacks (RenderIface / GUI draw callbacks). Game.DrawSpriteRegion(sprId, uv0, uv1, pos, size, color) draws a normalized [0, 1] sub-rectangle of the sprite’s original logical image into a destination rectangle; polygon-cropped atlas frames are remapped through their source offset and transparent cropped margins remain transparent in the destination. Game.DrawSpritePattern follows the same logical-image contract for every complete or partial tile. Region drawing is intended for reusable GUI composition such as script-side 9-slice panels, and returns false when the sprite cannot provide atlas-region drawing.

Core scripts

The engine-owned AngelScript core library lives in Source/Scripting/AngelScript/CoreScripts/ and includes reusable modules such as:

Treat these files as engine library code. Game-specific script modules should live in the embedding project instead of expanding the engine core script library with project policy.

Build and baking flow

BuildTools/cmake/stages/ScriptsAndBaking.cmake wires script compilation into the project build:

Script compilation and resource baking are adjacent but not identical. Script compilation produces bytecode/runtime inputs; baking packages resources and metadata for runtime consumption. See BakingPipeline.md for resource baking.

Mono and native scripting roots

Source/Scripting/Mono/ contains C# support files such as AssemblyInfo.cs, BasicTypes.cs, Entity.cs, Initializator.cs, MapSprite.cs, and Link.xml. BuildTools can wire Mono compilation when FO_MONO_SCRIPTING is enabled.

Source/Scripting/Native/ currently contains .keepalive, marking the source-root location for native scripting integration. Do not document Native or Mono as equivalent to the AngelScript runtime unless the implementation and tests are expanded.

Tests to inspect

Script behavior is covered by focused tests:

Use these tests as executable documentation when changing script registration, generated wrappers, method signatures, nullability, event declarations, or remote-call dispatch.

Change routing

Validation checklist

  1. If signatures or annotations changed, regenerate code and inspect generated metadata/wrapper diffs.
  2. Compile AngelScript through the embedding project’s CompileAngelScript target or equivalent AS compiler app.
  3. Run the smallest affected script tests, starting with Test_AngelScriptAttributes, Test_CommonScriptMethods, Test_ServerScriptMethods, Test_ScriptBuiltins, and Test_ScriptEntityOps as applicable.
  4. For nullable changes, run the nullability analyzers described in Nullability.md.
  5. For server/client/mapper method changes, validate the owning runtime path; do not rely only on compilation.
  6. Update ScriptMethodsMap.md when exported method files are added, removed, or materially regrouped.