# FOnline Engine bounded documentation context This generated bundle contains public current Markdown owned by the standalone engine. Generated reference detail pages are represented by their indexes; complete JSON models remain linked from llms.txt. Documents omitted by the reviewed full-context policy remain discoverable through llms.txt and docs-manifest.json. Canonical site: https://fonline.ru Documentation version: master (rolling-branch) Documents: 110 Policy exclusions: buildtools-readme, documentation-site-publication, script-methods-map, tools ===== BEGIN DOCUMENT repository-home ===== Source: README.md Canonical URL: https://fonline.ru/ Content SHA-256: b43be26040d110c289f96b48b45b9ea19693c49d48c455f35028efffadd7fe38 --- layout: default title: FOnline Engine locale: en document_id: repository-home permalink: / --- # FOnline Engine [![License](https://img.shields.io/github/license/cvet/fonline.svg)](https://github.com/cvet/fonline/blob/master/LICENSE) [![GitHub](https://github.com/cvet/fonline/workflows/validate/badge.svg)](https://github.com/cvet/fonline/actions) [![Commit](https://img.shields.io/github/last-commit/cvet/fonline.svg)](https://github.com/cvet/fonline/commits/master) **FOnline** is an open-source (MIT) C++20 engine for building online multiplayer RPGs in the classic isometric style of Fallout 1/2/Tactics and Arcanum. One codebase gives you the authoritative server, the game client, the map editor, the content pipeline, and packaging for desktop, mobile, and the browser — you bring the game: content, scripts, and rules live in your own repository that embeds the engine. In continuous development since 2006, the engine powers community multiplayer RPGs; a current example is [Last Frontier](https://lastfrontier.ru/), a post-apocalyptic MMO built on it. ## Why FOnline? - **Multiplayer first.** Not a single-player engine with networking bolted on: an authoritative server, replicated entity state, and client/server separation are the core design, all the way down to the entity model. - **Complete vertical.** Server, client, mapper, editor, resource baker, script compiler, test runner, auto-updater — all built from the same sources by one CMake pipeline. - **Engine/game split that stays clean.** The engine is a reusable submodule; your game owns content, scripts, configuration, branding, and release policy. Engine updates don't drag game policy with them. - **Data-driven content.** Prototypes, maps, dialogs, localization, and GUI are authored as plain-text assets and baked into runtime packs — friendly to diffs, reviews, and tooling. - **Runs where players are.** Native Windows/Linux/macOS, Android and iOS, and a WebAssembly client that plays in the browser over WebSockets. ## Feature highlights ### Multiplayer core - Authoritative server runtime with entity managers, client validation, and hardened parsing of untrusted client input. - Shared entity/property/prototype model with generated type-safe property wrappers and automatic property replication to clients. - Pluggable network transports: TCP sockets (including an Asio-based server), WebSockets for browser play, an ordered-UDP channel, and an in-process transport for tests and embedded clients. - Pluggable persistence backends — JSON files, SQLite, MongoDB, or in-memory — behind one database facade with an async commit queue and recovery logs. - Built-in client auto-updater: a thin client host plus a replaceable runtime, resumable file transfer, and a server-side update backend. ### Scripting - AngelScript and Managed C# gameplay scripting over a backend-neutral script system; Native scripting remains a reserved placeholder. - The native API is exported to scripts by code generation from `///@` annotations — methods, properties, events, remote calls, and enums stay in sync with the C++ source automatically. - Nullability is enforced across the script/native boundary: script `T?` maps to native `ptr`/`nptr` contracts, checked by analyzers and runtime asserts. - Script debugging support alongside native debugging. ### Rendering and presentation - Renderer backends: OpenGL, Direct3D, Vulkan, and SDL_GPU, plus headless/null modes for servers and CI. - Effects are written once in GLSL and compiled through glslang to SPIR-V, then translated to each backend via SPIRV-Cross. - Sprite-based isometric worlds with 3D character models (FBX), particle effects, video playback, and audio in both modern (Ogg/Vorbis) and classic Fallout formats. - Windowed, borderless-fullscreen, and multi-client virtual-window modes with a consistent resolution/letterbox model; ImGui-powered developer overlay. ### World and maps - Hexagonal and square grid geometry modes with shared helpers for distance, direction, and neighborhoods. - Path finding, line tracing, movement contexts, and a blocking model designed for multiplayer server authority. ### Content pipeline and tools - A baking pipeline turns authored sources — prototypes, maps, dialogs, localized texts, effects, images, models, scripts — into versioned runtime resource packs. - Imports classic 2D asset formats (Fallout FRM, Arcanum ART, and other legacy formats) alongside PNG/TGA. - Interactive tools built on the engine itself: Mapper with map/content windows and SPARK editing, plus focused animation and baked-particle viewers. ### Engineering quality - Unit tests (Catch2) with generated per-suite targets, sanitizer runs, and code coverage. - Clang Thread Safety Analysis enforced as `-Werror` on every Clang toolchain; strict smart-pointer and nullability vocabularies audited across the codebase. - Always-on stack traces, deterministic exception-safety rules, and a terminate-on-OOM allocation model instead of half-mutated states. - Tracy profiler integration for client and server captures. ## Architecture at a glance ```text Your game repository FOnline engine (this repo, embedded as Engine/) ──────────────────── ──────────────────────────────────────────────── content: protos, maps, ┌──► Applications — client/server/tool entry points dialogs, texts, GUI │ Client & Server runtimes — views vs. authority AngelScript / C# game logic embeds │ Common model — entities, properties, protos, .fomain configuration ───────┤ maps, networking, config native extensions │ Frontend — windows, input, audio, renderers CMake presets, CI, │ Scripting — AngelScript + Managed C# bridges + generated API release policy └──► Tools & BuildTools — bakers, mapper, editor, CMake stages, codegen, packaging ``` The engine owns reusable technology; the game owns the product. A game repository adds the engine as an `Engine/` submodule, points the engine's staged CMake pipeline at its own configuration, and gets project-named build targets for every application. The full layer map is in [Engine Architecture](Docs/en/explanation/architecture/), and the embedding contract in [Embedding FOnline in a Game Project](Docs/en/how-to/build/embedding-project.md): ```text GameProject/ ├── Engine/ # this repository as a git submodule ├── CMakeLists.txt # project entry point that includes engine build logic ├── CMakePresets.json # project presets and platform variants ├── GameName.fomain # master project configuration ├── Scripts/ # game AngelScript or Managed C# modules ├── SourceExt/ # optional project-native C++ extensions ├── Critters/ Items/ Maps/ # game content and prototypes └── Dialogs/ Texts/ # dialogs and localization ``` ## Getting started - **Run the first engine-owned project:** [First FOnline Headless Project](Docs/en/tutorials/first-project.md) - configure, build, bake, start, verify, and stop the minimal headless server. - **Inspect the canonical scaffold:** [Examples/MinimalProject/README.md](Examples/MinimalProject/README.md) - the complete project and CI smoke contract. - **Inspect the executable content gallery:** [Examples/ContentShowcase/README.md](Examples/ContentShowcase/README.md) - source assets, baking, native/Web checks, provenance, budgets, and reproducible capture evidence. - **Build the first playable slice:** [first playable client](Docs/en/tutorials/first-client.md), [first content change](Docs/en/tutorials/first-content.md), and [first automated test](Docs/en/tutorials/first-test.md) - connect a client, change localized content, and extend executable checks. - **Configure and maintain a project:** [Project Configuration](Docs/en/how-to/build/project-configuration.md), [Generated Content Workflow](Docs/en/how-to/build/generated-content.md), and [Engine Upgrade Guide](Docs/en/how-to/migration/engine-upgrade.md) - own `.fomain`, resource packs, generated outputs, migrations, and Engine updates. - **Choose release targets truthfully:** [Support Matrix](Docs/en/reference/platforms/support-matrix.md) and its [generated matrix](Docs/en/reference/platforms/generated-matrix.md) - distinguish source capability, required builds, process smoke tests, and project/device qualification. - **Plan and validate public examples:** [Public Example Repositories](Docs/en/how-to/build/public-example-repositories.md) and its [generated registry](Docs/en/reference/public-examples/index.md) - ownership, exact Engine pins, compatibility lanes, repository template, support, and asset provenance. - **Browse the generated native API:** [Docs/en/reference/script-api/index.md](Docs/en/reference/script-api/index.md) - methods, properties, events, types, settings, migrations, and source links. - **Author prototypes:** [Prototype Format](Docs/en/how-to/content/prototype-format.md) and its [generated reference](Docs/en/reference/prototype-format/index.md) - exact syntax, inheritance, built-in properties, references, migrations, and validation. - **Author maps:** [Docs/en/how-to/content/map-format.md](Docs/en/how-to/content/map-format.md) and its [generated reference](Docs/en/reference/map-format/index.md) - `.fomap` sections, placement IDs, ownership, mapper normalization, side-specific baking, and runtime loading. - **Author localized text:** [Text and Localization](Docs/en/how-to/content/text-and-localization.md) and its [generated reference](Docs/en/reference/text-format/index.md) - `.fotxt` syntax, language normalization, prototype `$Text`, runtime lookup, color tags, and the game-formatting boundary. - **Author images and sprite sheets:** [Image and sprite formats](Docs/en/how-to/content/image-format.md) and the [generated reference](Docs/en/reference/image-format/index.md) - PNG/TGA and legacy import, FOFRM composition, baking, runtime factories, atlases, caches, and validation. - **Author shader effects:** [Effect Format](Docs/en/how-to/content/effect-format.md) and its [generated reference](Docs/en/reference/effect-format/index.md) - `.fofx` sections, passes, render state, shader resources, backend outputs, runtime selection, and script values. - **Author particles:** [Particle Format And Runtime](Docs/en/how-to/content/particle-format.md) and its [generated reference](Docs/en/reference/particle-format/index.md) - optional SPARK/Effekseer selection, `.spark`/`.efkproj` authoring, `.spk`/`.efk` baking, Mapper tools, runtime routes, and model/script integration. - **Author bitmap fonts and lay out text:** [font formats and text layout](Docs/en/how-to/content/font-format.md) and its [generated reference](Docs/en/reference/font-format/index.md) - FOFNT/BMFont descriptors, slot binding, scaling, measurement, wrapping, rendering flags, colors, and validation. - **Inspect project integration contracts:** [CMake reference](Docs/en/reference/cmake/index.md), [BuildTools CLI reference](Docs/en/reference/buildtools/index.md), [helper CLI reference](Docs/en/reference/helper-cli/index.md), [native-extension reference](Docs/en/reference/native-extension/index.md), [package reference](Docs/en/reference/packages/index.md), and [public-example registry](Docs/en/reference/public-examples/index.md) - exact CMake, main/helper BuildTools, native-extension, packaging, and example-program surfaces. - **Add project-native C++ safely:** [Native Extensions](Docs/en/how-to/native-extensions.md) - source roles, hooks, script exports, state ownership, compatibility, and executable validation. - **Browse the generated CMake interface:** [Docs/en/reference/cmake/index.md](Docs/en/reference/cmake/index.md) - project options, strict stages and hooks, selected helpers, defaults, and source links. - **Measure client and server performance:** [Profiling](Docs/en/how-to/quality/profiling.md) - Tracy build modes, isolated capture boundaries, reproducible workloads, and comparable result analysis. - **Publishing documentation:** [documentation site publication](Docs/en/contributing/documentation/site-publication.md) - generated navigation/search/route data, rolling version and locale policy, local Jekyll preview, rendered-route/accessibility validation, CI artifacts, and the existing `fonline.ru` GitHub Pages route. - **AI and offline documentation:** [llms.txt](llms.txt), [llms-full.txt](llms-full.txt), and [docs-manifest.json](docs-manifest.json) - generated routes, bounded context, canonical/source URLs, provenance, and content hashes from the same Markdown manifest. - **New to the engine:** [Getting Started](Docs/en/tutorials/getting-started.md) - the first route: what to read, what to build, what belongs where. - **Starting or inspecting a game project:** [Embedding Project](Docs/en/how-to/build/embedding-project.md) — expected repository shape and ownership rules. - **Building:** [Build Workflow](Docs/en/how-to/build/) — prerequisites, presets, and validation strategy. Builds are normally driven from the embedding game repository, not from the engine checkout. - **AI-maintainer instructions:** [AGENTS.md](AGENTS.md) — read before changing engine code or docs. FOnline has build profiles for Windows, Linux, macOS, Android, iOS, and WebAssembly, but support is evidence-scoped. Consult the [support matrix](Docs/en/reference/platforms/support-matrix.md); a cross-build or headless smoke does not qualify a renderer, device, package, service, or store route. ## Repository layout - [`Source/`](Source/) — engine source: `Applications/` entry points, `Client/` and `Server/` runtimes, `Common/` shared model, `Frontend/` platform/render layer, `Scripting/` bridge, `Tools/` bakers and editors, `Essentials/` low-level core, `Tests/` unit tests. - [`BuildTools/`](BuildTools/) — staged CMake pipeline, code generation, platform toolchains, workspace and package preparation. - [`Resources/`](Resources/) — engine-owned runtime and build resources. - [`ThirdParty/`](ThirdParty/) — vendored dependencies (SDL, AngelScript, Asio, ImGui, glslang, SPIRV-Cross, Tracy, and more); maintenance workflow in [ThirdParty Maintenance](Docs/en/contributing/third-party/). - [`Docs/`](Docs/) — maintained engine documentation. ## Documentation The maintained index is [Docs/en/index.md](Docs/en/index.md); the Russian mirror is [Docs/ru/index.md](Docs/ru/index.md). Deep dives by theme: | Theme | Docs | |-------|------| | Architecture & navigation | [Architecture](Docs/en/explanation/architecture/) · [SourceTree](Docs/en/contributing/source-tree/) · [Applications](Docs/en/reference/applications.md) · [Essentials](Docs/en/reference/native/essentials.md) | | Runtime model | [Entity Model](Docs/en/explanation/entity-and-property-model/) · [Maps and Movement](Docs/en/explanation/maps-and-movement.md) · [Networking](Docs/en/explanation/authority-and-networking/) · [Persistence](Docs/en/explanation/persistence/) | | Client & server | [Client Runtime](Docs/en/explanation/runtime/client.md) · [Server Runtime](Docs/en/explanation/runtime/server.md) · [Frontend and Rendering](Docs/en/explanation/rendering/) · [Client Updater](Docs/en/explanation/runtime/client-updater.md) | | Scripting | [Scripting](Docs/en/explanation/scripting-runtime/) · [Managed C#](Docs/en/how-to/scripting/managed-csharp.md) · [LifecycleAndConcurrency](Docs/en/how-to/scripting/lifecycle-and-concurrency.md) · [AngelScriptStyle](Docs/en/how-to/scripting/style-and-refactoring.md) · [RemoteCalls](Docs/en/reference/scripting/remote-calls.md) · [ScriptMethodsMap](Docs/en/reference/script-api/method-ownership.md) · [Nullability](Docs/en/contributing/coding-contracts/nullability.md) · [GeneratedApiAndMetadata](Docs/en/reference/metadata/index.md) · [ContractChangeManagement](Docs/en/contributing/contract-change-management.md) | | Build & content pipeline | [BuildWorkflow](Docs/en/how-to/build/) · [ProjectConfiguration](Docs/en/how-to/build/project-configuration.md) · [GeneratedContentWorkflow](Docs/en/how-to/build/generated-content.md) · [EngineUpgradeGuide](Docs/en/how-to/migration/engine-upgrade.md) · [SupportMatrix](Docs/en/reference/platforms/support-matrix.md) · [BuildToolsPipeline](Docs/en/reference/cmake-and-buildtools/pipeline.md) · [BakingPipeline](Docs/en/explanation/content-pipeline/baking.md) · [ConfigurationAndDataSources](Docs/en/reference/settings/configuration-and-data-sources.md) | | Tools | [Tools](Docs/en/reference/tools/) · [Mapper Tools](Docs/en/how-to/tools/mapper.md) · [Mapper Interactive Manual](Docs/en/how-to/tools/mapper-interactive.md) · [Animation and Particle Viewers](Docs/en/how-to/tools/animation-particle-viewers.md) | | Quality & conventions | [Testing](Docs/en/contributing/testing/index.md) · [Profiling](Docs/en/how-to/quality/profiling.md) · [ExceptionSafety](Docs/en/contributing/coding-contracts/exception-safety.md) · [SmartPointers](Docs/en/contributing/coding-contracts/smart-pointers.md) · [ThreadSafetyAnalysis](Docs/en/contributing/coding-contracts/thread-safety-analysis.md) | | Platform debugging | [Debugging](Docs/en/troubleshooting/debugging.md) · [Web](Docs/en/how-to/platforms/web-debugging.md) · [Android](Docs/en/how-to/platforms/android-debugging.md) | When behavior changes in a noticeable way, the owning document is updated in the same change — the docs are maintained as a source-grounded reference, not an afterthought. ## Project and community - Site: - GitHub: - License: [MIT](LICENSE) ===== END DOCUMENT repository-home ===== ===== BEGIN DOCUMENT documentation-home ===== Source: Docs/en/index.md Canonical URL: https://fonline.ru/Docs/en/ Content SHA-256: 7161150c42f6987c0309cc3f45ab0e8c8f1a71309525c5cfcaec9eedec376673 --- layout: default title: FOnline Engine Documentation locale: en document_id: documentation-home permalink: /Docs/en/ --- # FOnline Engine Documentation This is the human documentation entry point for the reusable FOnline engine. It is written for game developers, tool authors, release operators, and engine contributors working from an Engine checkout without another game's documentation. ## Start here - [Getting Started](tutorials/getting-started.md) routes a new developer through the repository, supported workflows, and the engine/project ownership split. - [First FOnline Headless Project](tutorials/first-project.md) configures, builds, and runs the smallest tested server milestone. - [First Playable Client](tutorials/first-client.md) adds a connected desktop client, map loading, and a server-authoritative interaction. - [First Content Change](tutorials/first-content.md) follows localized prototype data through baking and runtime lookup. - [First Automated Test](tutorials/first-test.md) adds metadata, server-content, and client-visible checks. - [Minimal Project](../../Examples/MinimalProject/README.md) and [Minimal Multiplayer](../../Examples/MinimalMultiplayer/README.md) are the engine-owned runnable sources behind the tutorials. ## Build and ship a game - [Embedding Project](how-to/build/embedding-project.md) defines the repository boundary between a game and its pinned Engine revision. - [Project Configuration](how-to/build/project-configuration.md) covers `.fomain`, resource packs, sub-configs, overrides, and validation. - [Build Workflow](how-to/build/index.md) covers presets, prerequisites, target selection, and the normal configure/build loop. - [Generated Content Workflow](how-to/build/generated-content.md) gives the dependency order for codegen, scripts, resources, metadata, docs, and site artifacts. - [Support Matrix](reference/platforms/support-matrix.md) distinguishes verified, smoke-gated, source-capable, experimental, and unsupported combinations. - [Packaging and Release](how-to/release/packaging.md) owns package declarations, payloads, provenance, signing boundaries, acceptance, publication, and rollback. - [Security and Secrets](how-to/release/security-and-secrets.md), [Release Operations](how-to/release/operations.md), and [Backup and Recovery](how-to/release/backup-and-recovery.md) cover the reusable operational boundaries around a release. - [Engine Upgrade Guide](how-to/migration/engine-upgrade.md) defines full-range revision reconciliation, compatibility review, regeneration, and rollback. ## Author content - [Prototype Format](how-to/content/prototype-format.md) - [Map Format](how-to/content/map-format.md) - [Model Format](how-to/content/model-format.md) and [Model Animation](how-to/content/model-animation.md) - [Image and Sprite Formats](how-to/content/image-format.md) and [Sprite Root Motion](how-to/content/sprite-root-motion.md) - [Text and Localization](how-to/content/text-and-localization.md) - [Effect Format](how-to/content/effect-format.md) - [Particle Format and Runtime](how-to/content/particle-format.md) - [Font Formats and Text Layout](how-to/content/font-format.md) - [Audio](how-to/content/audio.md) and [Video](how-to/content/video.md) Each guide links to its generated reference when the engine owns a declarative grammar or machine-readable contract. A game owns its concrete catalogs, balance, quests, dialog content, visual policy, and localization policy. ## Understand the runtime - [Engine Architecture](explanation/architecture/index.md) and [Source Tree](contributing/source-tree/index.md) explain where behavior belongs. - [Entity and Property Model](explanation/entity-and-property-model/index.md), [Maps, Movement, and Geometry](explanation/maps-and-movement.md), [Authority and Networking](explanation/authority-and-networking/index.md), and [Persistence](explanation/persistence/index.md) — the database facade, commit queue, backend-consistent snapshots, and recovery logs — describe the reusable world model. - [Client Runtime](explanation/runtime/client.md), [Server Runtime](explanation/runtime/server.md), [Frontend and Rendering](explanation/rendering/index.md), and [Client Runtime Split and Updater](explanation/runtime/client-updater.md) cover the process and presentation layers. - [Scripting Runtime](explanation/scripting-runtime/index.md), [Managed C# Scripting](how-to/scripting/managed-csharp.md), [Script Lifecycle and Concurrency](how-to/scripting/lifecycle-and-concurrency.md), [AngelScript Style and Refactoring](how-to/scripting/style-and-refactoring.md), and [Remote Calls](reference/scripting/remote-calls.md) define the reusable scripting contract. - [Frontend and Rendering](explanation/rendering/index.md) covers Engine-native rendering and input primitives; high-level GUI libraries are project-owned. ## Use tools and validate changes - [Mapper Interactive Manual](how-to/tools/mapper-interactive.md) covers daily map editing, history, save discipline, and visible review. - [Mapper Tools](how-to/tools/mapper.md) covers mapper lifecycle, script automation, deterministic screenshots, and headless integration. - [Particle Authoring Tools](how-to/tools/particle-authoring.md) and [Animation and Particle Viewers](how-to/tools/animation-particle-viewers.md) cover focused visual inspection. - [Gameplay and Integration Testing](how-to/testing/gameplay-and-integration.md), [Testing](contributing/testing/index.md), [Debugging](troubleshooting/debugging.md), and [Profiling](how-to/quality/profiling.md) route changes to appropriate evidence. - [AiControl Protocol](how-to/ai-control-protocol.md) and the [runnable protocol sample](../../Examples/AiControlSample/README.md) define the project-neutral automation transport and its security boundary. ## Reference - [Applications](reference/applications.md) lists executable and library entry points. - [CMake Project Interface](reference/cmake/index.md) contains exact options, stages, hooks, and project-facing helpers. - [BuildTools CLI](reference/buildtools/index.md) and [Helper CLI](reference/helper-cli/index.md) contain parser-backed commands, arguments, defaults, choices, and exact help output. - [Generated API and Metadata](reference/metadata/index.md) explains the source-annotation, code generation, generated-model, and publication pipeline. - [Essentials](reference/native/essentials.md) defines the native foundation layer, its strict include order, allocation vocabulary, and subsystem map. - [Public Contract Index](reference/public-contract/index.md) routes all generated contract domains and their stability labels. - [Script API Method Ownership](reference/script-api/method-ownership.md) maps native script exports by runtime side and receiver family. - [Native Extension Reference](reference/native-extension/index.md) covers roles, hooks, fallbacks, and generated bindings. - [Platform Support Matrix](reference/platforms/support-matrix.md) records the evidence behind support claims. Canonical machine-readable models live under [`Docs/generated/`](../generated/). AI clients should start with [`llms.txt`](../../llms.txt), use [`docs-manifest.json`](../../docs-manifest.json) for stable document metadata, and load [`llms-full.txt`](../../llms-full.txt) only when a bounded standalone corpus is appropriate. ## Maintain the engine and documentation - [Documentation Maintenance](contributing/documentation/index.md) defines source ownership, revision reconciliation, regeneration order, and review evidence. - [Translation Workflow](contributing/documentation/translation.md) defines the English source, Russian mirror, glossary, freshness hashes, and code parity. - [Site Publication](contributing/documentation/site-publication.md) defines the GitHub Pages/Jekyll route, local preview, rendered artifact, and `fonline.ru` production checks. - [Contract Change Management](contributing/contract-change-management.md) classifies generated-model changes and breaking-change dispositions. - [Documentation Snippet Validation](contributing/documentation/snippets.md) and [AI Documentation Evaluation](contributing/documentation/ai-evaluation.md) define executable examples and retrieval/evidence quality gates. - [Native Coding Contracts](contributing/coding-contracts/) and [Third-Party Maintenance](contributing/third-party/) own low-level contributor conventions. The active production roadmap and verification history remain in [`Docs/ProductionDocumentationPlan.md`](https://github.com/cvet/fonline/blob/master/Docs/ProductionDocumentationPlan.md), [`Docs/_meta/DocumentationBacklog.md`](https://github.com/cvet/fonline/blob/master/Docs/_meta/DocumentationBacklog.md), and [`Docs/_meta/DocumentationVerificationReport.md`](https://github.com/cvet/fonline/blob/master/Docs/_meta/DocumentationVerificationReport.md) until their planned `_meta/` migration has durable redirects. ## Ownership boundary Engine documentation owns reusable runtime behavior, tools, build and platform contracts, formats, generated APIs, and native/script conventions. An embedding project owns concrete game content, product rules, deployment policy, service credentials, and project-specific commands. Normative Engine procedures must be executable from an Engine checkout and must not depend on Last Frontier, TLA, or another project's files. External projects may provide version-pinned evidence, but reusable helpers and regressions cited as the source of an Engine guarantee belong in this repository. ===== END DOCUMENT documentation-home ===== ===== BEGIN DOCUMENT legacy-public-api-entry ===== Source: Docs/en/reference/public-contract/index.md Canonical URL: https://fonline.ru/Docs/en/reference/public-contract/ Content SHA-256: fb1f17deb0487281984b8d00bfbb4c6650d06eba2b9b7924fc715eb2a3d8833a --- title: FOnline Public Contract Index document_id: legacy-public-api-entry locale: en generated: true --- # FOnline Public Contract Index This page is the revision-pinned entry point to the reusable interfaces and data formats exposed by the FOnline engine. It is generated from the same machine-readable models used by contract-diff checks. A reachable symbol or accepted file is not automatically a compatibility promise; the stability label on its owning contract is authoritative. ## Contract decision Resolve every disagreement in this exact four-level order: (1) owning Engine source and its metadata define behavior and stability; (2) the same-revision machine model defines exact inventory and drives diffs; (3) the generated human reference provides navigation; (4) handwritten guides provide workflow and context but do not override the first three levels. Reachability alone is not a compatibility promise. This index does not define annotation spelling or an embedding project's pin mechanism; do not invent either when the owning source or project does not supply it. The native script API is a revision-pinned experimental inventory: the current validated inventory-pinned scope classifies the complete inventory, so do not require an individual tag on every symbol. If that scope is absent or fails validation, unannotated native symbols remain internal. Pin the exact Engine revision, but when the embedding project does not document pin storage, do not propose a dedicated file, CI variable, config field, `FO_ENGINE_ROOT`, `FO_WORKSPACE`, or another mechanism. On upgrade, record the exact commit, compare all generated contract domains, and follow the Engine Upgrade Guide before moving the pin. ## Contract surfaces | Domain | Contract surface | Current stability | Human reference | Machine model | | --- | --- | --- | --- | --- | | Native script API | `engine-native-codegen` | `experimental` | [Guide](../script-api/index.md) | [api.json](../../../generated/api.json) | | CMake project interface | `cmake-project-interface` | `experimental` | [Guide](../cmake/index.md) | [cmake.json](../../../generated/cmake.json) | | BuildTools CLI | `buildtools-cli` | `internal` | [Guide](../buildtools/index.md) | [cli.json](../../../generated/cli.json) | | Package interface | `package-interface` | `internal` | [Guide](../packages/index.md) | [package.json](../../../generated/package.json) | | Helper CLIs | `helper-cli` | `internal` | [Guide](../helper-cli/index.md) | [helper-cli.json](../../../generated/helper-cli.json) | | Native extensions | `native-extension-interface` | `experimental` | [Guide](../native-extension/index.md) | [native-extension.json](../../../generated/native-extension.json) | | Prototype format | `prototype-format` | `experimental` | [Guide](../prototype-format/index.md) | [prototype-format.json](../../../generated/prototype-format.json) | | Map format | `map-format` | `experimental` | [Guide](../map-format/index.md) | [map-format.json](../../../generated/map-format.json) | | Model format | `model-format` | `experimental` | [Guide](../model-format/index.md) | [model-format.json](../../../generated/model-format.json) | | Text and localization format | `text-format` | `experimental` | [Guide](../text-format/index.md) | [text-format.json](../../../generated/text-format.json) | | Effect format | `effect-format` | `experimental` | [Guide](../effect-format/index.md) | [effect-format.json](../../../generated/effect-format.json) | | Image format | `image-format` | `experimental` | [Guide](../image-format/index.md) | [image-format.json](../../../generated/image-format.json) | | Particle format | `particle-format` | `experimental` | [Guide](../particle-format/index.md) | [particle-format.json](../../../generated/particle-format.json) | | Font format | `font-format` | `experimental` | [Guide](../font-format/index.md) | [font-format.json](../../../generated/font-format.json) | | Audio | `audio` | `experimental` | [Guide](../audio/index.md) | [audio.json](../../../generated/audio.json) | | Video | `video` | `experimental` | [Guide](../video/index.md) | [video.json](../../../generated/video.json) | | AiControl protocol | `ai-control-protocol` | `experimental` | [Guide](../ai-control-protocol/index.md) | [ai-control-protocol.json](../../../generated/ai-control-protocol.json) | The current revision contains **17** modeled contract domains: `experimental` 14, `internal` 3. ## Native script API status - Discovered symbols: **2555** - Symbols with source-backed descriptions: **2555** - Symbols without descriptions: **0** - Explicitly classified symbols: **2555** - Symbols inheriting the default `internal` classification: **0** The generated native reference is complete as an inventory of the modeled code-generation surface. Its current inventory-pinned scope is explicitly `experimental` and requires an exact Engine revision pin; it is not a broad `stable` compatibility promise. If the scope is absent or fails validation, unannotated native symbols remain `internal`. ## Stability vocabulary - `stable`: maintained compatibility contract; incompatible changes require the documented migration process. - `experimental`: usable with an exact Engine revision pin; compatibility may change with documented migration notes. - `deprecated`: retained temporarily for migration and scheduled for removal through the change-management process. - `internal`: implementation detail with no downstream compatibility promise. See [ADR-0002](../../contributing/decisions/0002-public-api-stability-contract.md) for classification rules and [Contract Change Management](../../contributing/contract-change-management.md) for review, diff, deprecation, and migration requirements. ## Source-of-truth order 1. The owning Engine source and its explicit contract metadata define behavior and stability. 2. The machine model records the extracted contract for the pinned revision and drives automated diffs. 3. The generated human reference explains the modeled surface and routes to focused manuals. 4. Handwritten guides provide workflows and examples but do not silently widen compatibility guarantees. When these layers disagree, treat the claim as unverified, fix the source metadata or generator, regenerate the affected model and reference, and update migration guidance in the same change. ## Project-owned boundaries The Engine contract does not define a game's remote-call schema, concrete prototypes, content identifiers, gameplay rules, deployment topology, service credentials, signing policy, or release acceptance matrix. Embedding projects must document and validate those boundaries in their own repositories. Engine manuals may use project evidence as non-normative context, but must remain usable without that project. ## Revision pinning and upgrades Pin the Engine by an exact commit in every game repository and public example. Before moving the pin, compare all generated contract domains, review breaking and behavioral changes, follow the [Engine Upgrade Guide](../../how-to/migration/engine-upgrade.md), and re-run the embedding project's build, bake, test, packaging, and documentation checks. Platform qualification is tracked separately in the [Support Matrix](../platforms/support-matrix.md). ===== END DOCUMENT legacy-public-api-entry ===== ===== BEGIN DOCUMENT getting-started ===== Source: Docs/en/tutorials/getting-started.md Canonical URL: https://fonline.ru/Docs/en/tutorials/getting-started.html Content SHA-256: 2773347b8ab1cf3e2888aeafb00141e47b3d3749437622ab26cfa3f347d5f6d6 --- layout: default title: Getting Started with FOnline Engine locale: en document_id: getting-started permalink: /Docs/en/tutorials/getting-started.html --- # Getting Started with FOnline Engine This guide is the first route for a developer who opens the engine repository and wants to understand what to read, what to build, and what belongs to the engine versus the game. ## Mental model FOnline is not a complete game by itself. It is a reusable engine that is normally checked out as an `Engine/` submodule inside a game repository. The split is: - **Engine repository:** runtime, tools, build modules, resource pipeline, scripting bridge, platform packaging, third-party code, and engine documentation. - **Game repository:** content, scripts, project configuration, presets, branding, native game extensions, deployment choices, and game-specific documentation. If a question is about reusable runtime behavior, engine tools, platform build mechanics, or script/native contracts, document it here in `Engine/Docs/`. If a question is about a concrete game's content, balance, quests, text, maps, or release policy, document it in that game's docs. ## First reading path 1. Read the repository overview in [README.md](../../../README.md). 2. Complete the tested [first headless project tutorial](first-project.md). 3. Run [First Playable Client](first-client.md), then make the [first content change](first-content.md) and add the [first automated test](first-test.md). 4. Read [Embedding FOnline](../how-to/build/embedding-project.md) to understand how a game project composes the engine. 5. Read [Build Workflow](../how-to/build/) before running other CMake or platform package steps. 6. Open [Source/README.md](../../../Source/README.md) when you need source-tree orientation. 7. Open [BuildTools/README.md](../../../BuildTools/README.md) when touching generated files, CMake stages, packaging, or platform workspaces. ## Common tasks ### I want to create or inspect a game project Run [First FOnline Headless Project](first-project.md), inspect the complete [minimal project](../../../Examples/MinimalProject/README.md), and continue through the playable [Minimal Multiplayer](../../../Examples/MinimalMultiplayer/README.md) lessons. Then read [Embedding FOnline](../how-to/build/embedding-project.md). The game repository should own its root CMake files, `.fomain`, content folders, scripts, and release-specific settings. The engine should stay reusable. ### I want to build or run tests Start with [Build Workflow](../how-to/build/). Prefer the embedding project's presets and tasks. Engine-only assumptions are easy to get wrong because actual target names, package names, and generated API files are project-dependent. ### I want to work on gameplay scripts Start with [Scripting](../explanation/scripting-runtime/) for the shared lifecycle and backend matrix. Use [AngelScript Style and Refactoring](../how-to/scripting/style-and-refactoring.md) for `.fos` modules or [Managed C# Scripting](../how-to/scripting/managed-csharp.md) for `.cs` assemblies, async/cover analysis, build, bake, runtime, and packaging. Native scripting is a reserved placeholder, not an implemented gameplay backend. ### I want to debug native code Use [Native, AngelScript, and Managed Debugging](../troubleshooting/debugging.md). It covers symbols, mixed stacks, crash diagnostics, native debugger behavior, AngelScript live attach, Managed diagnostics, and validation boundaries. ### I want to work on Web or Android Use the platform docs: - [Web Build, Packaging, and Browser Debugging](../how-to/platforms/web-debugging.md) - [Android Build, Packaging, and Device Debugging](../how-to/platforms/android-debugging.md) ### I want to work on updater/runtime split Use [Client Runtime Split and Updater](../explanation/runtime/client-updater.md). The client host/runtime ABI and updater protocol are subtle enough that they should not be reconstructed from code alone. ### I want to change script/native nullability Use [Nullability.md](../../Nullability.md). Keep C++ annotations, generated AngelScript/Managed types, runtime checks, and analyzers aligned. ## Documentation rule Keep docs close to ownership: - Engine-wide reusable behavior -> `Engine/Docs/`. - Engine source-tree or build-tool entry points -> `Engine/Source/README.md`, `Engine/BuildTools/README.md`, or focused files under `Engine/Docs/`. - Game-specific behavior -> the embedding project's `Docs/`. When changing behavior, update the owning doc in the same change. Do not leave a reader to infer new rules from code. ===== END DOCUMENT getting-started ===== ===== BEGIN DOCUMENT first-client-tutorial ===== Source: Docs/en/tutorials/first-client.md Canonical URL: https://fonline.ru/Docs/en/tutorials/first-client.html Content SHA-256: c96b9ffeaf641a1f2dc5af3b1d520d649740af3814044c84026f85cdde0f66c0 --- layout: default title: First Playable Client locale: en document_id: first-client-tutorial permalink: /Docs/en/tutorials/first-client.html --- # First Playable Client Build and run a desktop client connected to the engine-owned [Minimal Multiplayer](../../../Examples/MinimalMultiplayer/README.md) server. ## Prerequisites Complete the headless tutorial first. A standalone checkout keeps FOnline as the `Engine/` submodule beside the example's `CMakeLists.txt`. From the standalone example root, run: ```powershell python validate.py ``` On Linux, use `python3 validate.py`. The command configures and builds the desktop client, headless client, headless server, and baker, then runs the full automated lesson. Engine maintainers can exercise the checked-in source without materializing a separate repository: ```powershell cd Examples\MinimalMultiplayer python validate.py ``` Use the same `python3 validate.py` entry point from `Examples/MinimalMultiplayer` on Linux. ## Start the visible client After `validate.py` succeeds, open two terminals in the standalone example root. Start the server first: ```powershell Build\windows\Binaries\Server-Windows-win64\FOMM_ServerHeadless.exe -ApplyConfig FOnlineMinimalMultiplayer.fomain ``` Then start the desktop client: ```powershell Build\windows\Binaries\Client-Windows-win64\FOMM_Client.exe -ApplyConfig FOnlineMinimalMultiplayer.fomain ``` The generated platform directory may differ with the host and generator. The client connects to `127.0.0.1:4010`, logs in through `Tutorial::EnterWorld()`, and loads `TutorialMap`. Press Space after the map appears. The client calls `CollectSupply()`, the server increments `SuppliesCollected`, and the client displays the synchronized value. Expected log milestones include: ```text tutorial_client_connected tutorial_server_world_ready tutorial_client_map_loaded tutorial_server_supply_collected=1 tutorial_client_supply_collected=1 ``` ## Read the client/server boundary The complete behavior is in [`Scripts/Tutorial.fos`](../../../Examples/MinimalMultiplayer/Scripts/Tutorial.fos): 1. Client `Game.OnStart` binds the Engine font and calls `Game.Connect()`. 2. Client `Game.OnConnected` invokes the server remote call `EnterWorld()`. 3. The server creates the location, map, critters, and item, logs in the player, then calls `WorldReady()`. 4. Client `Game.OnMapLoaded` enables input and renders the map plus a small interface overlay. 5. Space sends only an intent. The server owns item creation and the replicated property mutation. The client owns presentation and intent; the server owns authoritative world and persistent state. ## Configuration contract [`FOnlineMinimalMultiplayer.fomain`](../../../Examples/MinimalMultiplayer/FOnlineMinimalMultiplayer.fomain) is generated from current Engine setting declarations because distribution config baking requires a complete server/client setting set. Project choices remain explicit in `generate_config.py`; edit its `OVERRIDES` or `PROJECT_SECTIONS`, regenerate the `.fomain`, and keep `--check` green. Ordinary runtime startup applies settings in this order: 1. Engine defaults; 2. project config or packaged internal config; 3. selected sub-configs; 4. the writable local-config cache; 5. command-line overrides; 6. derived auto settings. The desktop target stages `FOMM_BakerLib` beside the host so an unpackaged launch can prebake changed sources. The stricter distribution `Config` baker requires every saved project setting to be initialized; `CheckTutorialConfig` rejects drift before baking or packaging. ## Recovery - `Connection refused`: start the server first and confirm port `4010` is free. - `Config file not found`: run from the example root or pass the full path to `-ApplyConfig`. - Missing `FOMM_ClientLib.dll` or `FOMM_BakerLib.dll`: rebuild the `windows-check` preset; do not copy a DLL from another project. - A map or script change is ignored: run `cmake --build --preset windows-check` to bake and replay the complete route. Continue with [First Content Change](first-content.md). ===== END DOCUMENT first-client-tutorial ===== ===== BEGIN DOCUMENT first-content-tutorial ===== Source: Docs/en/tutorials/first-content.md Canonical URL: https://fonline.ru/Docs/en/tutorials/first-content.html Content SHA-256: 670fd7312d9b562cec599887b320c340e04b5c83f1cf3a931cc310a78daafc27 --- layout: default title: First Content Change locale: en document_id: first-content-tutorial permalink: /Docs/en/tutorials/first-content.html --- # First Content Change Change localized prototype text and prove the baked result in the [Minimal Multiplayer](../../../Examples/MinimalMultiplayer/README.md) example. ## Locate the owning sources The lesson deliberately separates authored data from behavior: | Source | Responsibility | |---|---| | `Content/StarterContent.fopro` | player, NPC, item, and location prototypes plus English/Russian text | | `Maps/TutorialMap.fomap` | map identity, size, and work hex | | `Scripts/Tutorial.fos` | world creation, interaction, and client presentation | | `FOnlineMinimalMultiplayer.fomain` | generated baker inputs, language order, and runtime settings | Do not edit `Build/*`, `Baking/*`, generated metadata, or a packaged resource to author content. Those are outputs. Project-owned configuration choices live in `generate_config.py`; its generated `.fomain` must remain reproducible. ## Rename the tutorial item Open [`Content/StarterContent.fopro`](../../../Examples/MinimalMultiplayer/Content/StarterContent.fopro) and replace both `$Text` values on `TutorialSupply`. Keep the stable prototype name unchanged: ```ini [ProtoItem] $Name = TutorialSupply $Text engl = "Emergency cache" $Text russ = "Аварийный контейнер" ``` `Baking.BakeLanguages = engl russ` makes English the normalization base for this example. Both locale values are authored together so the content contract does not silently rely on fallback. The checked smoke and packaged-runtime scenarios intentionally treat the visible English name as a public expectation. Update the client marker in both `tutorial-smoke.json` and `package-smoke.json` in the same change: ```json "tutorial_client_content=Emergency cache" ``` Keeping the assertion with the content revision distinguishes an intentional rename from missing or stale baked text. ## Bake and verify Run the complete check from a standalone example checkout: ```powershell python validate.py ``` Expected semantic evidence includes: ```text [tutorial-smoke] remote calls and replicated persistent property verified [gameplay-test] scenario content-test: passed tutorial_client_content=Emergency cache tutorial_server_supply_collected=1 [gameplay-test] summary: suite=minimal-multiplayer-tutorial status=passed scenarios=2 passed=2 failed=0 ``` Process-output lines may have runner labels around the markers. The markers and final passed summary are the contract. For an Engine checkout, run `python validate.py` from `Examples/MinimalMultiplayer`; the script selects the supported host preset. To inspect Russian text visibly, set the `Client.Language` override to `russ` in `generate_config.py`, regenerate the checked `.fomain`, rebuild the check preset, and launch the normal desktop client. Restore or deliberately commit the selected default language as project policy; language selection does not belong in Engine source. ## Add content safely For a new prototype or map: 1. Add the authored section under a directory consumed by the appropriate `ResourcePack`. 2. Give it a stable `$Name`; changing persisted identities later is a migration, not a cosmetic rename. 3. Author every locale in `Baking.BakeLanguages`. 4. Reference it through script metadata or a validated `hstring`, according to the owning API. 5. Extend the content test, then bake and run the smallest consuming route. Use [Prototype Format](../how-to/content/prototype-format.md), [Map Format](../how-to/content/map-format.md), and [Text and Localization](../how-to/content/text-and-localization.md) for the complete contracts. ## Recovery - `Unknown prototype`: confirm the extension is in `Baking.ProtoFileExtensions` and the containing directory is mounted by the `Protos` resource pack. - The old name remains: do not patch generated or packaged outputs; rerun the checked bake and inspect `Baking/Baking.report.json` for the owning pack. - The content test passes but the client marker fails: inspect the `Texts` resource pack and runtime `Game.GetText()` lookup, not only prototype existence. Continue with [First Automated Test](first-test.md). ===== END DOCUMENT first-content-tutorial ===== ===== BEGIN DOCUMENT first-test-tutorial ===== Source: Docs/en/tutorials/first-test.md Canonical URL: https://fonline.ru/Docs/en/tutorials/first-test.html Content SHA-256: a6a04fc38189630334396b16f1cc9e37436fbb7de3b9f8aa74e57a1c62a23f0d --- layout: default title: First Automated Test locale: en document_id: first-test-tutorial permalink: /Docs/en/tutorials/first-test.html --- # First Automated Test Extend the three-layer executable checks in [Minimal Multiplayer](../../../Examples/MinimalMultiplayer/README.md). ## Test layers `RunTutorialChecks` runs these layers in order: 1. **Baked metadata check.** `run_tutorial_smoke.py` decodes server and client metadata, requires symmetric remote-call declarations, and confirms that `SuppliesCollected` exists on both sides. 2. **Server content test.** The `TutorialContentTest` sub-config starts an isolated in-memory server with networking disabled. Script assertions create the location/map, add the NPC and item, and validate map invariants. 3. **Client-visible smoke.** A headless client connects to a real listening server, logs in, loads the map, collects the item, observes the synchronized count, and requests clean shutdown. The headless client proves connection, baking, map load, localization, remote calls, and replication. It does not prove pixels, input ergonomics, audio, or GPU behavior; keep a visible pass for those claims. ## Run all layers From a standalone example checkout: ```powershell cmake --preset windows cmake --build --preset windows-check ``` Use `linux` and `linux-check` on Linux. `python validate.py` selects these commands for the current host. Engine CI uses the equivalent reusable route: ```powershell cd Examples\MinimalMultiplayer python validate.py ``` ## Add one content invariant Suppose the project adds an item with stable name `TutorialBeacon`. Add its prototype first, then extend `RunContentTest()` in [`Scripts/Tutorial.fos`](../../../Examples/MinimalMultiplayer/Scripts/Tutorial.fos): ```angelscript const hstring BeaconPid = "TutorialBeacon".hstr(); void RunContentTest() { verify(Game.CheckProtoItem(BeaconPid), "Tutorial beacon prototype is missing"); // Keep the existing assertions and clean shutdown. } ``` Keep the constant with the other stable IDs and preserve the existing body; the abbreviated function above only shows the new assertion. Run the complete check. Then temporarily misspell the ID and confirm the content-test layer fails before the multiplayer client starts. Restore the valid ID and rerun. This fail-then-pass replay proves the assertion is active. A test that has only ever been observed green is weaker evidence. ## Marker and timeout policy Markers are a small public contract between the example scripts and runner: - use stable, machine-searchable lowercase names; - emit a marker only after the asserted state exists; - require process exit code zero and every marker; - bound every process wait; - terminate both processes on failure; - label captured server/client output and reject every missing marker. Do not replace state assertions with log assertions. Script `verify(...)` checks the world invariant; the marker tells the external runner which verified milestone completed. ## Persistence scope `SuppliesCollected` and `TutorialAccount` are declared `Persistent`, and the baked metadata test protects those declarations. The tutorial uses `Server.DbStorage = Memory`, so it does not claim persistence across server process restarts. A game that claims restart durability must run an additional test against its supported database backend and verify the restored entity state. ## Recovery - Metadata fails before startup: compare both baked sides and remote-call signatures; do not weaken the decoder. - Client connects but never loads the map: inspect server world creation and player/critter switching before adding longer sleeps. - A test is flaky: wait on semantic milestones with a bounded timeout; avoid fixed delays as success criteria. For reusable fixture, marker, timeout, cleanup, and process-report semantics, continue with [Gameplay and Integration Testing](../../GameplayTesting.md). For native Catch2 ownership and coverage, use [Testing](../../Testing.md). ===== END DOCUMENT first-test-tutorial ===== ===== BEGIN DOCUMENT embedding-project ===== Source: Docs/en/how-to/build/embedding-project.md Canonical URL: https://fonline.ru/Docs/en/how-to/build/embedding-project.html Content SHA-256: 0acb2ae65debb2493c031a7054a88ab552d1cae52581979b098ea8fa81034b23 --- layout: default title: Embedding FOnline in a Game Project locale: en document_id: embedding-project permalink: /Docs/en/how-to/build/embedding-project.html --- # Embedding FOnline in a Game Project FOnline is designed to be embedded as a source submodule. The engine repository supplies reusable technology; the game repository supplies the concrete product. ## Source paths inspected - `BuildTools/Init.cmake` - `BuildTools/cmake/ProjectInterface.json` - `BuildTools/cmake/helpers/Build.cmake` - `BuildTools/cmake/stages/ScriptsAndBaking.cmake` - `Examples/MinimalProject/CMakeLists.txt` - `Examples/MinimalProject/FOnlineStarter.fomain` - `Examples/MinimalProject/README.md` - `Examples/MinimalMultiplayer/CMakeLists.txt` - `Examples/MinimalMultiplayer/FOnlineMinimalMultiplayer.fomain` - `Examples/PublicRepositories.json` - `Source/Applications` - `Source/Tools` The engine-owned [minimal project](../../../../Examples/MinimalProject/README.md) is the canonical executable example of this boundary. It is also the source for the [first headless project tutorial](../../tutorials/first-project.md) and the planned `fonline-project-template`. Ownership, exact-revision rules, CI lanes, and publication gates for that repository and later examples are defined in [Public Example Repositories](public-example-repositories.md). ## Expected repository shape A typical game repository looks like this: ```text GameProject/ ├── Engine/ # git submodule pointing to this repository ├── CMakeLists.txt # project entry point that includes engine build logic ├── CMakePresets.json # project presets and platform variants ├── GameName.fomain # master project configuration ├── Scripts/ # game AngelScript (.fos) or Managed C# (.cs) modules ├── SourceExt/ # optional project-native C++ extensions ├── Critters/ Items/ Maps/ # game content and prototypes ├── ProjectDialogs/ Texts/ # optional project-defined dialogs and localization └── Docs/ # game-specific documentation ``` Folder names vary by project, but the ownership rule should stay stable: reusable engine machinery lives under `Engine/`, while concrete game content and project policy live in the parent repository. The folder name above is deliberately generic. FOnline does not currently ship a built-in dialog-tree schema, `.fodlg` parser, dialog baker, runtime, or visual editor. A game may implement dialogs in scripts, through project-native extensions and bakers, or through a separately versioned companion. Document the chosen format and validation in the game repository, and do not assume that another embedding project has the same dialog API or file layout. ## What belongs in the engine Keep these in the engine repository: - Runtime systems shared by multiple games. - BuildTools and CMake stages used to compose projects. - Platform package/workspace generation logic. - Engine resources and reusable tools. - Public/native API definitions and generated scripting API machinery. - Documentation about engine behavior, platform mechanics, and reusable contracts. ## What belongs in the game project Keep these in the embedding project: - Game rules, content, maps, prototypes, dialogs, localization, and GUI definitions. - Game-specific AngelScript or Managed C# modules, with exactly the baker/runtime backend(s) enabled and packaged by the project. - Project-level native extension implementations and dependencies; [Native Extensions](../../../NativeExtensions.md) owns composition, hooks, and bindings, while [Project Dependencies](../../../ProjectDependencies.md) owns library/SDK selection, role-scoped linking, package delivery, and updates. - Project-level AI observations, game actions, MCP tools, and listener shipping policy; [AiControl Protocol](../ai-control-protocol.md) owns only the reusable transport, command lifecycle, threat boundary, reference client, and protocol evidence. - Project presets, product identifiers, package names, signing/deployment choices, and CI policy. - Game design and content workflow documentation. ## Project-Owned Game-System Formats A project may define authored formats for game systems that are not part of the Engine contract. Dialog trees are a common example. Keep such a system project-owned until its reusable implementation, tests, fixtures, compatibility policy, and documentation have moved into Engine or a versioned companion. A complete project-owned format should identify: 1. the parser and authoritative grammar; 2. the baker or other generated outputs; 3. runtime consumers and authority boundaries; 4. editor/formatter behavior and round-trip expectations; 5. source-level, compiled, and runtime validation; 6. compatibility and migration policy across Engine pins; 7. the exact ownership label in project documentation. Engine documentation may describe the native-extension and baking primitives used to implement the format. It must not publish the project format as a stock FOnline capability. ## Build composition The game repository should drive the build. In practice this means: 1. Configure from the game repository root, not from inside `Engine/`, unless a specific engine-only workflow says otherwise. 2. Use the game repository's `CMakePresets.json` and tasks so generated paths, target names, and package metadata match the product. 3. Let engine `BuildTools` provide reusable stages and helper functions. 4. Keep generated files out of hand-authored docs unless the generation process is part of the topic. Use `Examples/MinimalProject/CMakeLists.txt` as the smallest current composition example. It also proves a server-only `INTERFACE` dependency through the revision-pinned `FO_SERVER_LIBS` list; expand it by adding project-owned modules and targets without copying unrelated wiring from a large game. ### Add a project-specific baking target The standard pipeline creates `BakeResources` and `ForceBakeResources` with subconfig `NONE`. If the game defines another configuration slice for a public, test, or release resource set, create its target immediately after the scripts-and-baking stage: ```cmake SetupScriptsAndBaking() AddBakingTarget(Game_PublicResources SUB_CONFIG PublicGame COMMENT "Bake public resources") BuildPackages() ``` Add `FORCE` only when that target must always request a full bake. The helper retains the standard `ForceCodeGeneration` dependency, `FO_OUTPUT_PATH` working directory, main-config argument, and resource build-hash update. Keep the target name, subconfig contents, and downstream CI/package policy in the game repository. ## Documentation composition Use this routing: - Link from game docs into `Engine/Docs/...` for reusable mechanics such as Web/Android debugging, nullability, updater protocol, mapper automation, and native debugging. - Keep local links in engine docs inside the engine repository. Cross-project examples must use stable HTTPS links to tagged public revisions. - Use only examples whose generated registry status is `published`; a planned repository name is not a valid public source link. - Avoid duplicating long engine explanations in the game docs. Prefer a short project-specific note plus a link to the owning engine document. ## Validation principle Validate engine changes through a real embedding project whenever possible. The engine-owned minimal project provides the baseline host-aware `Examples/MinimalProject/validate.py` route; larger projects remain necessary for client, content, packaging, and gameplay contracts. The example validator is opt-in and is not a required Engine workflow lane. A reusable engine change may compile in isolation but still break generated APIs, project packaging, scripts, or content baking. Choose the narrowest project target that exercises the changed layer. ===== END DOCUMENT embedding-project ===== ===== BEGIN DOCUMENT engine-architecture ===== Source: Docs/en/explanation/architecture/index.md Canonical URL: https://fonline.ru/Docs/en/explanation/architecture/ Content SHA-256: ed29a41d3c3a6b2ccfa73bd8a892142f2657683129e26e7b2d7137403110ad61 --- layout: default title: Engine Architecture locale: en document_id: engine-architecture permalink: /Docs/en/explanation/architecture/ --- # Engine Architecture This document gives a source-grounded map of the FOnline engine layers. Use it when deciding where a behavior belongs before opening subsystem-specific docs. ## Ownership decision Route a change through these four decisions: 1. Put behavior that must work the same for multiple games in the Engine source layer and the corresponding Engine subsystem guide. Put game rules, authored content, product configuration, acceptance thresholds, and release policy in the embedding project. 2. Use this architecture page when behavior crosses several Engine layers or the Engine/project boundary. Use [Source Tree](../../contributing/source-tree/) when the question is where code lives or which directory to inspect first. 3. Keep generated Engine contracts tied to their owning Engine source, machine model, and generator. Project inputs and generated project outputs do not become reusable Engine authority merely because an Engine tool consumes or emits them. 4. Link from the non-owning page to the owner instead of restating the contract. A source-directory inventory is not an architecture decision, and a project integration is not proof of generic Engine behavior. A complete boundary answer names both the owner and the documentation route: use this page for architecture-wide behavior, Source Tree for source navigation, and the owning subsystem guide for its detailed contract. ## Big picture FOnline is organized around a reusable engine embedded by a game project. The game project owns content, scripts, product configuration, and release policy; the engine owns reusable runtime systems, tools, generated API infrastructure, platform frontends, and build composition.
Diagram showing the reusable FOnline engine on the left and an embedding game project on the right. The engine provides runtime systems, tools, code generation, and platform applications. The game provides project configuration, scripts, content, tests, and release policy. Generated contracts and extension hooks connect the two sides.
Engine owns reusable runtime and tooling; the embedding project owns game rules, content, product configuration, validation, and release policy. Dependencies cross only through declared configuration, generated contracts, and extension hooks.
The main layers are: - **Applications** — executable and library entry points in `Source/Applications/`. - **Essentials** — low-level platform, memory, filesystem, logging, serialization, sockets, and utilities in `Source/Essentials/`. - **Common runtime** — shared engine model in `Source/Common/`: entities, properties, prototypes, maps, networking primitives, config, scripts, and engine base services. - **Client runtime** — presentation/resource/network-client side in `Source/Client/`. - **Server runtime** — authoritative world, managers, database backends, network-server side, and updater backend in `Source/Server/`. - **Frontend** — application/window/rendering abstraction in `Source/Frontend/`. - **Scripting** — implemented AngelScript and Managed C# backends, the reserved Native placeholder, and script method registration in `Source/Scripting/`. - **Tools** — baker, Mapper-centered editing, asset processors, and related developer tooling in `Source/Tools/`. - **BuildTools** — CMake stages, helpers, toolchains, platform project generation, package layout, and validation support in `BuildTools/`. ## Application layer `Source/Applications/` is the practical entry-point directory. It contains app wrappers such as: - `ClientApp.cpp` and `ClientLib.cpp` for client host/runtime flows. - `ServerApp.cpp`, `ServerDaemonApp.cpp`, `ServerHeadlessApp.cpp`, and `ServerServiceApp.cpp` for server variants. - `MapperApp.cpp` for the central interactive editing tool. - `BakerApp.cpp` and `ASCompilerApp.cpp` for generation/build support. - `TestingApp.cpp` for test execution. `BuildTools/cmake/stages/Applications.cmake` wires these files into project-specific targets based on build options such as client/server/tool/platform/library modes. Avoid hard-coding target names in engine docs: target names are often derived from the embedding project's `FO_DEV_NAME` and presets. See [Applications](../../reference/applications.md) for the application map. ## Source paths inspected - `Source/Applications/` - `Source/Common/EngineBase.h` - `Source/Common/EngineBase.cpp` - `Source/Common/Entity.h` - `Source/Common/Entity.cpp` - `Source/Common/ScriptSystem.h` - `Source/Common/ScriptSystem.cpp` - `Source/Client/Client.h` - `Source/Server/Server.h` - `Source/Frontend/Application.h` - `Source/Frontend/ApplicationInit.cpp` - `BuildTools/cmake/stages/Applications.cmake` ## Common runtime layer `Source/Common/` holds shared concepts used by client, server, tools, and scripts. Important entry points include: - `EngineBase.h` / `EngineBase.cpp` — base engine services and shared runtime state. - `Entity.h` / `Entity.cpp` — exported entity concepts shared across runtime sides. - `Properties.h`, `EntityProperties.h`, `EntityProtos.h`, `ProtoManager.h` — property/prototype model. - `ScriptSystem.h` / `ScriptSystem.cpp` — script engine abstraction used by runtime sides and tools. - `Geometry.h`, `Movement.h`, `PathFinding.h`, `MapLoader.h` — reusable map and movement primitives. - `NetBuffer.h`, `NetworkUdp.h` — common networking primitives. - `ConfigFile.h`, `DataSource.h`, `FileSystem.h`, `CacheStorage.h` — config and data access support. - `ImageWriter.h` — PNG encoders for screenshots, render-target and atlas dumps. `WritePng` writes RGBA to disk; `EncodeCompactPng` produces filtered opaque RGB bytes for transport. `Game.CaptureScreenshot(maxSide)` reads the last complete frame, applies integer-factor downscaling when requested (`0` keeps full size), and refuses a render-callback capture of a partial frame. This layer should stay reusable. Game rules should generally be expressed through content/scripts or project-native extensions, not by embedding one project's policy into common engine code. ### Runtime random state `random_generator` owns its own state: `capture_state()` returns the four 64-bit words that define the sequence, and `restore_state()` puts them back, rejecting the all-zero state because xoshiro256++ sits at a fixed point there. `BaseEngine::CaptureRandomState()` and `RestoreRandomState()` delegate to the generator under the mutex it shares with random draws. There is no engine-level serialization format; a caller that needs to store the state serializes the four words itself. This API is a persistence primitive, not a complete snapshot boundary. An authoritative server must still stop gameplay mutation before it captures the generator together with the corresponding world, time, event, and storage state. Client presentation, transport, and subsystem-specific generators are independent and are not included in this state. The server-side `RunInQuiescence()` operation supplies that reusable in-process stop boundary: new connection admission closes, main/worker gameplay execution drains, frame/synchronized time and delayed-job scheduling freeze, the live entity graph is covered, and synchronized-time/RNG state is captured before a callback runs. `ServerEngine::CreateSnapshot()` composes the stable subset: it rejects counted runtime-only script/delayed/time-event/movement blockers, flushes exact time/id, and returns the database payload bytes together with the state that describes them. Fresh construction takes that pair back, restores the random state before startup jobs, loads the payload into storage, and validates its time/id before gameplay hooks. Atomic slot publication, persistent time-event/movement forms, project eligibility, UI policy, integrity/rotation, and coordinated client reload remain embedding-layer work. See [Server Runtime](../runtime/server.md) and [Persistence](../persistence/index.md) for the exact guarantees and exclusions. ## Client and server layers `Source/Client/Client.h` includes the client-side composition points: application integration, resource/cache access, views for critters/items/locations/maps, effects, rendering-facing structures, and client connection code. `Source/Server/Server.h` includes authoritative runtime pieces: entities, managers, database, geometry, scripting-facing server objects, client validation, networking, and updater backend support. Treat client and server docs as separate because their ownership differs: - The **client** presents local views, resources, UI-facing objects, and network-client behavior. - The **server** owns authoritative world state, persistence, entity managers, validation, and network-server behavior. ## Frontend layer `Source/Frontend/Application.h` and the related `Application*.cpp` / `Rendering*.cpp` files abstract platform app startup and rendering backends. This layer is where headless/stub/native frontend differences belong, not in game docs. Platform workflow docs: - [Web build, packaging, and browser debugging](../../how-to/platforms/web-debugging.md) - [Android build, packaging, and device debugging](../../how-to/platforms/android-debugging.md) - [Native, AngelScript, and Managed C# Debugging](../../troubleshooting/debugging.md) ## Scripting layer `Source/Common/ScriptSystem.*` defines the common script-system abstraction. `Source/Scripting/` provides runtime-specific method registration and integration folders: - `Source/Scripting/AngelScript/` - `Source/Scripting/Native/` - `Source/Scripting/Managed/` — implemented Managed C# backend, CoreScripts bridge, analyzers, load-context host, and tests - `Source/Scripting/*ScriptMethods.cpp` The engine owns the reusable script/native bridge. A game project owns concrete game script modules and gameplay logic. ## Build and generation layer `BuildTools/cmake/stages/` is the staged CMake pipeline. Current stage files include: - `Init.cmake` - `ProjectOptions.cmake` - `CoreLibs.cmake` - `ThirdParty.cmake` - `EngineSources.cmake` - `Codegen.cmake` - `Applications.cmake` - `ScriptsAndBaking.cmake` - `Packages.cmake` - `Finalize.cmake` These stages compose engine code with embedding-project configuration. Read [Build Workflow](../../how-to/build/) before changing build behavior. ## Typical runtime flow A normal embedding-project workflow looks like this: 1. The game repository configures CMake from the project root. 2. BuildTools loads project options and engine sources. 3. Codegen and baking steps prepare generated API/resources/scripts. 4. Applications are built from `Source/Applications/` entry points. 5. Runtime starts through the selected app: client, server, mapper, baker, test app, or platform package. 6. Client/server/tools use common runtime services and call into game-owned scripts/content where appropriate. ## Where to document changes - Architecture-wide behavior: this file. - Source navigation: [Source Tree Guide](../../contributing/source-tree/). - App entry points: [Applications](../../reference/applications.md). - Build workflow: [Build Workflow](../../how-to/build/) and [BuildTools Pipeline](../../reference/cmake-and-buildtools/pipeline.md). - Script/native boundary: [Nullability](../../../Nullability.md) and [Scripting](../../../Scripting.md). - Platform debugging: [Web](../../how-to/platforms/web-debugging.md), [Android](../../how-to/platforms/android-debugging.md), and [native debugging](../../troubleshooting/debugging.md). ===== END DOCUMENT engine-architecture ===== ===== BEGIN DOCUMENT build-workflow ===== Source: Docs/en/how-to/build/index.md Canonical URL: https://fonline.ru/Docs/en/how-to/build/ Content SHA-256: 91f5eb4f89c9119fe681f12cd3df4355e4915d7ae8f935d792512f7c2170a7e8 --- layout: default title: Build Workflow locale: en document_id: build-workflow permalink: /Docs/en/how-to/build/ --- # Build Workflow This document explains how to approach FOnline builds without hard-coding assumptions from one project into another. ## Source paths inspected - `../BuildTools/README.md` - `../BuildTools/Init.cmake` - `../BuildTools/validate.sh` - `../BuildTools/validate.cmd` - `../BuildTools/buildtools.py` - `../BuildTools/docs_cli.py` - `Docs/en/reference/buildtools/index.md` - `../BuildTools/PackageInterface.json` - `../BuildTools/docs_package.py` - `en/reference/packages/index.md` - `../Examples/MinimalProject/` - `../Examples/MinimalMultiplayer/` - `../BuildTools/cmake/stages/Init.cmake` - `../BuildTools/cmake/stages/ProjectOptions.cmake` - `../BuildTools/cmake/stages/EngineSources.cmake` - `../BuildTools/cmake/stages/Codegen.cmake` - `../BuildTools/cmake/stages/ScriptsAndBaking.cmake` - `../BuildTools/cmake/stages/Applications.cmake` - `../BuildTools/cmake/stages/Packages.cmake` - `../BuildTools/cmake/stages/Finalize.cmake` - `../BuildTools/cmake/helpers/*.cmake` - `../Source/Applications/TestingApp.cpp` - `../Source/Tests/README.md` ## Use the embedding project as the build root FOnline is normally built through a game repository that embeds the engine as `Engine/`. Configure and build from the game root unless a focused engine-only command explicitly says otherwise. Reasons: - Target names are project-defined. - `.fomain` controls game-specific configuration. - Generated scripting APIs are project-dependent. - Package names, signing, resources, and deployment settings belong to the product. - Platform presets usually live in the embedding project's `CMakePresets.json`. ## Typical workflow 1. Open the game repository root. 2. Inspect available presets with CMake or the IDE integration used by the project. 3. Configure the smallest preset that covers your change. 4. Build the narrowest relevant target. 5. Run the corresponding test, package, or launch target. 6. Update documentation if the workflow or behavior changed. ## Engine-owned first build The repository includes one stable exception to project-specific target naming: [Examples/MinimalProject](../../../../Examples/MinimalProject/README.md). It proves a clean headless embedding path without Last Frontier, TLA, or another game checkout. From the engine root, use the host-specific validation target: ```powershell cd Examples\MinimalProject python validate.py ``` ```bash cd Examples/MinimalProject python3 validate.py ``` Both routes configure and build the baker plus headless server, bake the minimal AngelScript project, run the server with networking disabled and an in-memory database, and require the lifecycle markers documented in [First FOnline Headless Project](../../tutorials/first-project.md). Pinned Windows and Linux CI lanes are verified. The next Engine-owned route builds the desktop client, headless client, headless server, and baker, then tests metadata, content, login, map loading, localized text, remote calls, and replicated state: ```powershell cd Examples\MinimalMultiplayer python validate.py ``` ```bash cd Examples/MinimalMultiplayer python3 validate.py ``` The source and manual launch path are documented in [Minimal Multiplayer](../../../../Examples/MinimalMultiplayer/README.md) and [First Playable Client](../../tutorials/first-client.md). ## Prerequisites Use the [Support Matrix](../../reference/platforms/support-matrix.md) before turning a build profile into a release claim. The generated matrix distinguishes required compilation, executable smoke evidence, and source-only profiles; device, renderer, package, service, and store acceptance remain project-owned. The exact list depends on host OS and target platform, but common tools include: - Git - CMake - Python 3 - A C++20-capable compiler/toolchain - Platform SDKs for the targets you build - Visual Studio or Build Tools on Windows-oriented workflows - Emscripten and Node.js for Web builds - JDK and Android NDK for Android builds Prefer the embedding project's documented setup because it may pin specific SDK/tool versions. ### Windows 7 compatibility lane The `win32-win7` and `win64-win7` build-platform keys are native-Windows MSVC lanes pinned to toolset `v143,version=14.44`; they fail early on a non-Windows host. `FO_BINARY_OUTPUT_POSTFIX` is an independent build identity, not an implication of the `-win7` platform name. When a project builds with a value such as `Win7`, its matching package declaration must use the same value on that one entry: `BINARY Client Windows win32-win7 Raw+Zip+Wix POSTFIX Win7`. Before packaging or publishing that lane, inspect every linked EXE and DLL: ```powershell python BuildTools/check_windows7_imports.py --require-large-address-aware ``` The check parses PE imports and rejects the curated Windows 8+ exports, absent libraries, and unsupported API-set contracts described in [Testing](../../contributing/testing/). This includes imports from a statically linked managed runtime. A passing static check is not a live Windows 7 SP1 startup test. The embedding project owns the concrete toolset installation, binary paths, package matrix, CI gate, and live-host acceptance. ## Windows x86 address space `AddExecutableApplication` in `BuildTools/cmake/helpers/Build.cmake` links every 32-bit Windows engine executable with `/LARGEADDRESSAWARE`, including client hosts, headless applications, servers and tools. The decision follows platform and pointer size, not a project target name or binary postfix; shared libraries do not set the process limit. On 64-bit Windows, this permits an x86 user address space of up to 4 GB rather than 2 GB. On 32-bit Windows 7 the default remains 2 GB; the flag adds no physical RAM. `check_windows7_imports.py --require-large-address-aware` checks the finished EXE's PE flag alongside import compatibility and does not impose that flag on DLLs. Neither gate proves representative map loading, sustained memory behavior or acceptance on an actual Windows 7 host. ## Fetching through a mirror of your own `prepare-workspace` downloads the toolset, the Android SDK/NDK, the MSVC SDK and the LLVM sources from whoever publishes them. Each of those is a machine you do not run, and a dropped connection costs the job that is waiting on it. An embedding project may put a host of its own in front of them; the engine only needs to be told where it is, so nothing about that host is compiled in and everything travels in the environment: | variable | what it configures | |---|---| | `FO_DOWNLOAD_MIRROR` | Base URL of a pull-through mirror. `https://host/path` is fetched as `/host/path` instead. | | `FO_WORKSPACE_CACHE` | Base URL for prepared workspaces. Emscripten is keyed by SDK version, host OS, and architecture; the MSVC SDK tree is keyed by xwin version and contained architectures. Each complete tree is built once and downloaded whole afterwards. | | `FO_CI_TOKEN` | Bearer token for the two addresses above. It is sent **only** to their own scheme and host, never to an upstream one. | | `FO_CI_CA` | Extra trust anchors, added to the system store rather than replacing it, for a machine whose root store cannot be repaired. | Unset, every one of them leaves the download path exactly as it was. Two behaviours are deliberate. A download is checked against the upstream `Content-Length`, because a dropped connection ends the read instead of raising and an archive cut in half unpacks into a failure far from its cause. And a workspace cache that is empty, unreachable or refusing is only a **miss**: it exists to make the build faster and independent of other people's servers, not to become another way for it to fail. `emsdk` and `xwin` fetch their own packages, so mirroring the engine's direct downloads does not cover them. Their complete prepared results are therefore what the workspace cache holds. A corrupt or incomplete Emscripten cache object is discarded and rebuilt locally; cache creation and upload remain best-effort. Cached trees are extracted through the standard data-only tar filter after a path-boundary check. Extraction lands in a temporary sibling first, and only the named complete SDK directory is promoted, so an archive cannot overwrite another prepared workspace tree. The xwin cache follows the same restore rule. ## Where build logic lives Use the generated [BuildTools CLI reference](../../reference/buildtools/index.md) for the exact main commands, arguments, defaults, choices, and executable help output. Use the generated [package interface reference](../../reference/packages/index.md) for `DefinePackage` grammar, accepted targets/platforms/architectures, pack-token compatibility, payload layouts, and output artifacts. Follow [Packaging and Release](../release/packaging.md) for the build/bake/package order, platform procedures, artifact evidence, signing, acceptance, and recovery boundaries. Keep a game's concrete package matrix in that embedding project's documentation. For a library, SDK, framework, or runtime payload owned by the game repository, follow [Project-Local Dependencies](../../../ProjectDependencies.md). Create a project CMake target, append it to the narrowest consumed `FO_*_LIBS` list supported by the pinned revision, and validate both its compiled feature state and packaged runtime state. - [BuildTools overview](../../../../BuildTools/README.md). - [BuildTools Pipeline](../../reference/cmake-and-buildtools/pipeline.md) — staged CMake pipeline and change routing. - `../BuildTools/cmake/` — reusable CMake modules and staged generation/build/package logic. - `../BuildTools/Init.cmake` — project-facing CMake entry point and strict stage dispatcher. - Embedding project root — product-level presets, configuration, and target selection. ## Validation by change type When `BuildTools/buildtools.py::create_parser()` changes, regenerate and check the CLI model/pages before validating the affected command in an embedding project. When package declarations or payload behavior change, update `BuildTools/PackageInterface.json`, regenerate/check its model/pages, run `validate_package_interface.cmake` and `test_packaging_matrix.py`, then build `RunPackagingChecks`, `RunTutorialPackageChecks`, or the narrower affected product package target from the owning example/project. These example targets are opt-in and are not part of the required Engine validation registry. The Engine fixtures prove native raw/archive/config/updater mechanics; they do not replace a game's signing, install, deployment, or rollback lane. - **Runtime C++:** build and run the project unit-test target; use [Testing](../../contributing/testing/) to choose focused suites and understand generated test targets. - **CMake/BuildTools:** reconfigure from a clean or relevant build directory and run the affected build/package target; use [BuildTools Pipeline](../../reference/cmake-and-buildtools/pipeline.md) for stage ownership. - **Generated API:** rebuild generation targets, verify scripts compile, and consult [Generated API and Metadata](../../reference/metadata/index.md). - **Project config/resource packs:** follow [Configure a Game Project](project-configuration.md), compile scripts, bake normally, force-bake when the input graph changed, and execute the consuming sub-config. - **Generated outputs:** follow [Generated Content Workflow](generated-content.md) in dependency order instead of editing build-tree, baked, or documentation artifacts. - **Engine pin:** follow the [Engine Upgrade Guide](../migration/engine-upgrade.md), including complete-range audit, generated contract comparison, persistence/network/updater review, and documentation reconciliation. - **Resource baking:** run the relevant normal/forced bake path and consult [Baking Pipeline](../../explanation/content-pipeline/baking.md). - **Updater:** follow [Client Updater](../../explanation/runtime/client-updater.md). - **Web:** follow [Web Build, Packaging, and Browser Debugging](../platforms/web-debugging.md). - **Android:** follow [Android Build, Packaging, and Device Debugging](../platforms/android-debugging.md). - **Mapper/tooling:** follow [Tools](../../../Tools.md) and [Mapper Tools](../tools/mapper.md). - **AngelScript source/refactor:** follow [AngelScript Style and Refactoring](../scripting/style-and-refactoring.md), run the Engine or project formatter wrapper, compile every affected side warning-free, and execute the narrowest behavior or contract test. - **Managed C# source/refactor:** follow [Managed C# Scripting](../scripting/managed-csharp.md), run the configured formatter and Roslyn analyzer, build `CompileManagedScripts`, then bake and execute the narrowest affected callback, async, synchronization, or packaging test. - **Nullability/script boundary:** follow [Scripting](../../explanation/scripting-runtime/), [Script Methods Map](../../reference/script-api/method-ownership.md), and [Nullability](../../contributing/coding-contracts/nullability.md). - **Configuration/resources:** follow [Configuration and Data Sources](../../reference/settings/configuration-and-data-sources.md) and [Baking Pipeline](../../explanation/content-pipeline/baking.md). - **Essentials/low-level utilities:** follow [Essentials](../../reference/native/essentials.md) and run the matching essentials tests from [Testing](../../contributing/testing/). ## Keep build docs maintainable Do not copy a full preset list into engine docs. Presets change per game and per branch. Instead, explain ownership and link to the concrete project document that owns exact commands. ## Validation checklist 1. Confirm the command or preset belongs to the embedding project before documenting exact names in engine docs. 2. For BuildTools changes, reconfigure the smallest affected preset and run the generated target that exercises the changed stage. 3. For runtime changes, run focused tests first and then the project `RunUnitTests` target when practical. 4. For package/platform changes, validate the owning package/debug doc in the same change. 5. Update [BuildTools Pipeline](../../reference/cmake-and-buildtools/pipeline.md), [Testing](../../contributing/testing/), or platform docs when the build workflow itself changes. 6. Run the matching starter smoke when a BuildTools, baking, scripting, application-startup, or embedding-boundary change can affect the canonical minimal project. 7. Regenerate affected contract models and run the aggregate [generated contract diff](../../contributing/contract-change-management.md) for project-facing API, CMake, CLI, or package changes. ===== END DOCUMENT build-workflow ===== ===== BEGIN DOCUMENT packaging-and-release ===== Source: Docs/en/how-to/release/packaging.md Canonical URL: https://fonline.ru/Docs/en/how-to/release/packaging.html Content SHA-256: a9708815dd34f5364cb6a145d225f48cd8b7ed7796e4c7a5dfe7dea08709238b --- layout: default title: Packaging and Release locale: en document_id: packaging-and-release permalink: /Docs/en/how-to/release/packaging.html --- # Packaging and Release This guide turns an embedding project's compiled FOnline applications and baked resources into reviewable release artifacts. It owns the reusable Engine procedure and its evidence boundaries. A game repository still owns its package matrix, product configuration, credentials, deployment topology, databases, stores, rollout, and rollback decision. Use the generated [package interface](../../reference/packages/index.md) for the exact current grammar, target/platform compatibility, pack tokens, payloads, and command-line arguments. Use the [Support Matrix](../../reference/platforms/support-matrix.md) before calling any resulting artifact supported. ## Package decision For each declared application variant, build the exact target, run `ForceBakeResources` for its release configuration, then invoke the generated `MakePackage` target and inspect the isolated artifact. The current output-producing packs are `Raw`, `Zip`, `SingleZip`, `Tar`, `TarGz`, `Root`, `Wix`, and `Apk`; valid packs still depend on target and platform. An implemented payload or pack is capability, not a support claim. The embedding project owns its package matrix, release policy, signing credentials, distribution, acceptance, rollout, and rollback. Engine package capability proves none of those project decisions. In particular, `Apk` copies the selected signed **or debug** APK; the pack token does not prove release signing. Describe a tested capability and its evidence, not an Engine guarantee for every host or project. Do not say the output packs can be combined with any target/platform: use only an implemented compatible row from the generated matrix. `Debug+Apk` selects Gradle `assembleDebug`, whose development artifact is signed with the Gradle debug key; it is debug-signed, not unsigned and not production release-signed. The current accepted targets are `Server`, `Client`, `Mapper`, `Baker`, `AnimationViewer`, and `ParticleViewer`. Payload packaging is implemented for `Windows`, `Linux`, `Android`, and `Web`; `macOS` and `iOS` are accepted parser dimensions whose package implementations are `unsupported`. The implemented payloads are native PE plus companion files, native ELF plus companion files, an Android Gradle client project with ABI libraries and assets, and a browser JavaScript/Wasm client with preloaded resources. Name the artifact produced by the chosen compatible output pack: `Raw` retains the staged directory; `Zip` emits a ZIP; `SingleZip` appends to one package-wide ZIP; `Tar` emits a tar archive; `TarGz` emits a gzip-compressed tar archive; `Root` merges the staged directory into the package output root; `Wix` emits an MSI; and `Apk` copies the selected signed or debug APK. These are tested packager capabilities, not universal Engine guarantees or project release evidence. ## Know what the Engine proves Keep four claims separate: 1. **Build capability**: an application target can compile for a host/target profile. 2. **Package capability**: `BuildTools/package.py` can assemble that target/platform/payload combination. 3. **Project qualification**: the embedding game repeatedly installs, starts, exercises, and diagnoses the artifact in its release environment. 4. **Published release**: an identified artifact passed project approval, signing, distribution, rollout, and recovery gates. The current packager implements Windows, Linux, Android, and Web payloads. It rejects macOS and iOS packaging. The required workflow nevertheless build-gates narrower macOS and iOS client inputs. A successful Apple build is therefore not an Engine-produced application bundle, signed archive, notarized package, device pass, or store submission. The packager can emit a Windows service binary or Linux daemon beside the ordinary server. It does not install the service, create an operating-system account, provision a database, write a systemd or recovery policy, open firewall ports, rotate logs, or prove long-running operation. ## Prepare a release-owned package matrix Record one row for every artifact the game intends to ship. Do not infer rows from all parser choices. | Field | Example | Required evidence | |---|---|---| | Package ID | `ReleaseWindows` | Stable `DefinePackage` name | | Engine revision | full commit SHA | Clean, recursively initialized checkout | | Game revision | full commit SHA or signed tag | Source used for the artifact | | Package config | `PublicRelease` | Baked client/server config exists | | Target | `Client`, `Server`, or tool | Application binary was built | | Platform and architecture | `Windows win64` | Compatible package capability | | Pack tokens | `Raw+Zip+Wix` | Valid generated-interface combination | | Build host/toolchain | pinned runner image and versions | Reproducible environment record | | Acceptance lane | named CI job or reviewed procedure | Install/start/runtime result | | Distribution | archive, installer, store, container, depot | Project-owned publication route | | Signing identity | certificate/key alias without secret data | Signature verification result | | Rollback unit | previous immutable artifact/config/data set | Rehearsed restore route | Treat the exact game and Engine revisions, package declaration, package config, dependency pins, SDK/tool versions, and build image as one input set. Changing any member creates a different release candidate. ## Declare packages Call `DefinePackage(...)` after project sources are registered and before `BuildPackages()`. Keep separate package IDs when their build hosts, credentials, acceptance lanes, or publication destinations differ. ```cmake DefinePackage(ReleaseWindows CONFIG PublicRelease BINARY Client Windows win64 Raw+Zip+Wix BINARY Server Windows win64 Headless+Service+Raw+Zip) DefinePackage(ReleaseLinux CONFIG PublicRelease BINARY Server Linux x64 Headless+Daemon+Raw+TarGz) DefinePackage(ReleaseWeb CONFIG PublicRelease BINARY Client Web wasm Raw+Zip+WebServer) DefinePackage(ReleaseAndroidArm64 CONFIG PublicRelease BINARY Client Android arm64 Raw+Apk) ``` This is a grammar example, not a support claim or a universal production matrix. Remove every row that the game does not build and qualify. Each `BINARY` clause has this shape: ```text BINARY [POSTFIX ] ``` `CONFIG` selects a baked sub-config. For resource-bearing client and server packages, baking must have produced `Baking/Configs/.fomain-client` or `...-server` through the `Config` baker. The package target does not silently create missing application binaries or baked resources. Use `POSTFIX` when a separately built binary variant has `FO_BINARY_OUTPUT_POSTFIX`. The declaration value must match the build output. This keeps package input selection, packaged runtime identity, and server-staged updater payload names aligned. Do not use a package name as an implicit variant selector. Use `INCLUDE ` only for reviewed, distributable files already present under the build output. The packager rejects escaping paths and updates the package root plus `SingleZip` when present. License notices, attribution, and third-party payload approval remain project responsibilities. ## Build, bake, then package Run the stages explicitly and stop on the first failure: 1. Start from a clean, recursively initialized game checkout at the release revision and verify the exact Engine SHA. 2. Prepare the host using the pinned Engine workspace command and the embedding project's preset or CI image. 3. Configure the project with release-owned cache values. Do not reuse an unexplained developer cache. 4. Build every application variant named by the package declaration. 5. Force-bake the release resources and configs when the candidate must not depend on incremental state. 6. Invoke `MakePackage-` only after its binary and baking inputs exist. 7. Preserve the complete package log and fail on assertions, warnings treated as errors, signing failures, missing symbols, missing configs, or missing resource packs. The project chooses concrete target names. A typical multi-config sequence is: ```bash cmake --preset release-host cmake --build Build/release-host --config Release --target cmake --build Build/release-host --config Release --target ForceBakeResources cmake --build Build/release-host --config Release --target MakePackage-ReleaseWindows ``` Do not run several platform entries from one package ID unless all of their compiled inputs are available in the same `FO_OUTPUT_PATH`. Separate package IDs make cross-build ownership and failures easier to audit. The packager patches reserved data regions after linking. It embeds resources and the selected baked config, writes the packaged build name, and may adjust PE PDB paths. It does not generate or execute code in those reserved regions. Signing, when configured, happens after patching and before archives or installers are emitted. Each embedded-data or configuration patch locates the first matching marker, checks the payload size and reservation bounds, and writes only that fixed-size region in place. The binary length and surrounding bytes stay unchanged; missing markers, oversized payloads and truncated reservations fail before that field is modified. This is a per-field guard, not an atomic transaction across all package patches. The packaged-name field uses its own fixed-size write and NUL padding. `BuildTools/tests/test_package_internal_config.py` checks the combined three-field result, variant configuration, first-marker selection and invalid fields that leave the input intact. ## Run the Engine packaging fixture `Examples/PackagingMatrix` is the executable Engine-owned baseline for native package mechanics. It is intentionally separate from the readable starter and multiplayer tutorials because `ConfigBaker` requires every server/client runtime setting to be initialized. Its checked-in `FOnlinePackagingMatrix.fomain` is deterministically generated from `Source/Common/Settings.inc`; `generate_config.py --check` fails when settings and the fixture diverge. Both this fixture and `Examples/MinimalMultiplayer` derive defaults from the current `SETTING(...)` declarations. After changing the settings schema, regenerate both example configurations before running their freshness checks. In a standalone checkout of `Examples/PackagingMatrix` with its `Engine` submodule initialized, configure the host build and build the fixture-owned `RunPackagingChecks` target. This target is not registered as a required Engine `BuildTools validate` lane. ```bash cmake --build --config Release --target RunPackagingChecks ``` Each route builds the client, headless client, server, headless server, host service/daemon role, and baker; force-bakes resources plus server/client `PackageSmoke` configs; creates raw payloads and ZIP or TAR.GZ archives; compares archive members with staged payloads; and starts the packaged headless client against the packaged server through the real updater handshake. Both processes must observe `Common.Packaged`, consume the embedded fixture setting, emit success markers, and stop with code zero. The verifier writes `FOPKG-PackageSmoke/packaging-manifest.json` with the exact Engine revision, archive hashes and sizes, complete payload inventories, role presence, and runtime results. Preserve the manifest and archives in the embedding project's release lane when this evidence is required; the current Engine workflow does not publish them. This fixture provides opt-in evidence for the Windows x64 or Ubuntu/Linux x64 Engine package path on the host where it is run. It does not qualify another game's package declaration, signing, installer, store, deployment host, database, renderer, or rollback. Copy the evidence pattern into the embedding project's required release lane and keep its concrete acceptance there. ### Public multiplayer package acceptance `Examples/MinimalMultiplayer` applies that pattern to a readable game source. Its `Tutorial` package force-bakes the automated gameplay configuration and emits native raw plus ZIP/tar.gz client/server payloads. The example-owned verifier checks archive/payload parity, records SHA-256 for every archive and payload file, and runs the packaged headless server/client map and item interaction through the shared gameplay process runner. The checked-in `.fomain` is generated from current `Settings.inc` defaults plus reviewed tutorial overrides and sections. `CheckTutorialConfig` runs before baking, so a new or changed saved setting fails on stale source instead of appearing later as an incomplete packaged config. The `windows-package` and `linux-package` presets in `Examples/MinimalMultiplayer` build the fixture-owned `RunTutorialPackageChecks` target on demand. Preserve the archives, package manifest, and runtime report in a project workflow when they are required; the current Engine workflow does not run these presets. This evidence remains narrower than a product release: the archives are unsigned, headless, audio-disabled fixtures with no installer, store, public deployment, durable backend, upgrade, or rollback claim. The Windows x64 lane passed locally on Engine `fac978a67`: two archives matched their raw payloads, the 28-file client and 37-file server inventories were hashed, and the packaged interaction scenario passed. This is local host evidence only. Linux support and immutable example-release evidence require a green landed job and a reviewed external repository commit/tag. ## Select artifacts by platform ### Windows client - `Raw` retains the staged portable directory. - `Zip` emits a portable archive from that directory. - `Wix` emits a per-user MSI. On Windows, prepare the Engine-pinned portable WiX v3 toolset with `buildtools.py prepare-workspace wix`; `package.py` finds `FO_WIX_ROOT`, a sibling `wix3` workspace, or finally `candle`/`light` on `PATH`. It passes the selected directory to `createmsi.py --wix-dir`; direct invocations accept the same option, including paths with spaces as one argument. Omitting it uses the tools on `PATH`. `light` runs ICE validation first and retries once with `-sval` only when the Windows Installer service is unavailable; every other linker/ICE failure and a failed fallback remain fatal. POSIX package hosts require `wixl` 0.102 or newer. - `OGL` adds the separately built OpenGL runtime variant. - `Lib` selects the library form where the target supports it. - `POSTFIX` keeps independently built variants, such as a depot-specific client, from colliding. The generated MSI uses `InstallScope="perUser"`. Its Start Menu and Desktop shortcut components use separate `HKCU` key paths, PATH registration is per-user (`System="no"`), and generated directory components carry uninstall cleanup. These choices make the same installer description buildable with pinned WiX on Windows or `wixl` on Linux without requiring machine-wide registration. An MSI is not proof that the client is signed, trusted by endpoint protection, upgrade-compatible, or accepted by a distribution channel. Verify those properties on the final emitted artifact. The installation-directory and folder-browser dialogs list a push button before text or path controls. WiX and `wixl` derive different tab loops from control order; a loop that omits `Control_First` makes `msiexec` abort before the first screen with internal error 2834. The generated XML is checked for a closed tab loop under both linker rules. On `wixl`, path controls remain editable with the mouse and through the folder browser even though they are outside its tab loop. Validate an actual installer on each supported host. The installation-directory dialog must run after `CostFinalize`, when Windows Installer has resolved `INSTALLDIR`. `wixl` can otherwise place a dialog constrained only to run before `ProgressDlg` ahead of costing, depending on dependency iteration order; `msiexec` then aborts with internal error 2343 because the path is empty. The generator anchors it `After="CostFinalize"` for both WiX and `wixl`. The [MSI creator guide](../../../../BuildTools/msicreator/readme.md) and regression tests describe the linker-specific sequencing checks. A successfully linked MSI does not replace a visible install test on the supported host. ### Linux client or server - `Raw`, `Zip`, `Tar`, and `TarGz` are available output forms. - `Headless` includes the headless variant in addition to the ordinary target. - `Daemon` includes the Linux daemon server variant. - `TotalProfiling` and `OnDemandProfiling` add separately compiled profiling variants where valid. The packager assigns logical `0755` modes to target executables independently of the packaging host and writes those modes into ZIP and TAR metadata. Raw or Root output also produces the package-root `.lf-package-modes.json` handoff: a versioned map of normalized POSIX-relative payload paths to the only accepted logical modes, `0644` and `0755`. A publisher that copies or repacks raw trees must validate and apply that handoff, then omit it from the public payload; unsafe, escaping, drive-qualified, or backslash paths are rejected. Qualify the actual Linux distribution, runtime libraries, filesystem paths, process account, signals, logs, and service manager used by the game. ### Managed C# payload When `FO_MANAGED_SCRIPTING` is enabled, the `Managed` baker puts target-specific assemblies and a prepared `ManagedRuntime/` class-library payload into the selected resource pack. Native client packages consume the payload built for that exact application target; Web and Android carry it in their resource assets. A server package preparing client updates stages one target-specific pack under `PlatformBinaries//` and `-expect-client-runtime Platform:arch[:postfix]` makes a missing requested payload fail packaging. If several native variants share that updater target, the least-qualified matching binary entry, normally default Release, supplies the pack; equivalent independently built CoreLib payloads need not be byte-identical. Inspect the assembly target, `runtime.manifest`, content hash, and packaged startup separately; see [Managed C# Scripting](../scripting/managed-csharp.md). ### Web client The Web client payload contains JavaScript, patched Wasm, an HTML shell, preloaded `Resources.data` / `Resources.js`, target-specific Managed assemblies/runtime resources when enabled, and optionally the local `WebServer` helper. The Engine-owned Content Showcase command `python validate.py --web-runtime` can provide opt-in evidence for native-host baking, exact raw/ZIP package inventory, localhost HTTP delivery, a native server connection, required lifecycle markers, a real WebGL 2 context, and compositor pixels for one deterministic fixture under pinned Chromium. It is not a required Engine workflow lane and does not prove an embedding game's public browser deployment. Follow [Web Build, Packaging, and Browser Debugging](../platforms/web-debugging.md) for local staging. A release lane must additionally verify HTTPS hosting, MIME types, cache policy, cross-origin isolation or other required headers, WebSocket reachability, browser compatibility, storage persistence, audio activation, loading failure UX, and at least one visible representative scene. ### Android client The Android payload is a generated Gradle project with one `libmain.so` per selected ABI and baked resources under application assets; a Managed build keeps its target assemblies and prepared runtime in those assets. `Apk` runs the Gradle assembly and copies the resulting APK beside the staged project. Follow [Android Build, Packaging, and Device Debugging](../platforms/android-debugging.md) for the pinned SDK/NDK workspace, ABI mapping, device connection, resource staging, and configuration fields. Android ARM32 and ARM64 are build-gated; Android x86 remains source-capable. A game must own emulator/device gates, GPU/input/audio/network/background behavior, signing identity, versioning, store policy, and rollout. An APK produced without release keystore settings uses the Gradle development key and is not a production release artifact. ### macOS and iOS `package.py` currently aborts for both `macOS` and `iOS`. Do not add `DefinePackage` rows for them. The support matrix build-gates client inputs at narrower scopes, but the embedding project must supply and maintain application-bundle assembly, resources, entitlements, provisioning, signing, notarization where applicable, device/simulator checks, store metadata, and delivery. Until that route exists and is repeatable, describe Apple targets as build-gated inputs rather than packaged or release-supported products. ### Client runtime variants A client package copies its selected host/runtime pairs explicitly. The generic companion-library pass excludes the regular and headless Engine client runtime input and alias names on Windows and Linux, because all of those files can coexist in one build-output directory. Consequently an ordinary package cannot inherit a stale headless runtime merely because another job built it; requesting the `Headless` pack token still adds the headless host/runtime under their packaged names. Inspect Raw and archive inventories for both the required pair and absence of unrequested sibling names before signing or publishing. ### Server, service, and daemon A server package includes server resources and client update resource packs. When matching client runtime libraries are available, it also stages platform runtime payloads used by the updater. Review [Client Runtime Split and Updater](../../explanation/runtime/client-updater.md) before publishing a server whose clients self-update. `Service` and `Daemon` are binary variants, not deployment systems. The game must version and test: - process arguments and environment; - least-privilege account and filesystem permissions; - service-manager definition and restart limits; - network exposure and TLS termination; - database schema, credentials, migration, backup, and restore; - logs, metrics, crash reports, health checks, and alerting; - graceful drain/shutdown and rollback to a compatible binary/config/data set. Keep those product and infrastructure details in the embedding project. [Release Operations](operations.md) supplies the reusable process, readiness, rollout, shutdown, and rollback runbook without claiming that infrastructure as Engine-owned. ## Reproducibility and provenance Native host builds provide the optional `FOnlineResourcePackHash` library under an input root's `Binaries/BuildTools--/`. The packager discovers it in input order; `-resource-pack-hash-library ` explicitly selects one. Without a discovered library, Python hashes remain the fallback. A present but unloadable library or a failed known-vector/streaming-seed check is an error, not a fallback. On Windows, loading temporarily enables `SEM_FAILCRITICALERRORS` on the calling thread, preserving its other flags and restoring the previous mode after either success or failure. An invalid DLL raises `OSError` without an interactive loader dialog; the process error mode is unchanged. Failure to set or restore the thread mode is also an error. Each packager loads its own backend, including spawned workers. Both backends use the same streaming FNV-1a 64 and retain header, physical, decoded-payload and logical content validation, including cache hits. Archive bytes, compression settings and cache keys do not depend on this accelerator. It is host tooling, not game payload. `test_resource_pack_hash.py` builds the real library, compares Python/native and serial/parallel Raw results, and rejects corrupt payloads. Valid and invalid native loads run in subprocesses with bounded timeouts; Windows checks also verify thread-flag restoration and preservation of the process mode. Resource archives are sequential by default. `package.py -resource-pack-jobs N` overrides `FO_RESOURCE_PACK_JOBS` (default `1`); both accept only positive integers. Independent `.fores` work runs in at most `N` spawned processes on every host. Tasks sharing a pack name or physical destination stay ordered on one worker, preserving local reuse and preventing concurrent writes. Groups are balanced by source bytes, largest first. Every worker retains the complete writer, cache protocol and payload validation; optional-cache unavailability persists within its batch, bounding failed probes by the worker count. The parent adopts validated archive identities and waits for workers before runtime-specific pack rewriting or failure cleanup. Embedded resources and final distribution bundles are outside this worker lane. Choose the limit for available CPU and memory; it is not a format change or a measured speedup guarantee. FOnline writes each non-Embedded resource pack as a deterministic `.fores` base from sorted normalized paths, without serialized timestamps. The [Resource Pack Format](../../../ResourcePackFormat.md) specifies the version-2 header, content and physical hashes, complete catalog, optional writable patch, and validation rules. `Baking.ResourcePackCompressLevel` and `Baking.ResourcePackMinCompressGain` govern per-blob deflate; `Baking.BundleCompressLevel` governs outer distribution bundles and the executable's Embedded ZIP. `package.py` accepts separate `-resource-pack-compress-level` and `-bundle-compress-level` overrides. It verifies every produced resource payload, including decoded length and content hash, before packaging succeeds. Embedded ZIP remains sorted with fixed timestamps and permissions and is CRC-checked from its in-memory bytes before binary patching. Outer ZIP and TAR packages use the target's logical file modes rather than the host filesystem's modes, so a Windows packaging host still emits executable Linux targets. Raw package parts merge their mode records into one package-root `.lf-package-modes.json` handoff instead of losing earlier parts. These are package-construction gates, not proof that the installer, delivery channel, publication step, or installed filesystem preserved the result. The package declaration parser and generated contract are deterministic and checked in CI. `FO_RESOURCE_ARCHIVE_CACHE_HELPER` may name a project-owned Python helper implementing `restore|store|release --key --archive `. The key covers the deterministic entry names and contents plus resource compression level and minimum gain. A hit still passes full `.fores` payload validation; a miss or explicitly unavailable optional cache falls back to local construction, while malformed hits and unexpected helper failures stop packaging. The helper's storage, credentials, claims, retention, and service availability remain project-owned. That does not make every complete release bit-for-bit reproducible. Linked binaries, debug symbols, top-level archives, MSI/APK toolchains, signing timestamps, included files, and external SDKs may carry host- or time-dependent data. State the narrower guarantee you have actually tested. For every candidate, emit or retain an artifact manifest containing at least: - game and Engine full SHAs; - dirty-tree status; - submodule revisions; - host image, compiler/linker, Python, CMake, SDK/NDK/Gradle/WiX versions; - package ID, config, target, platform, architecture, and pack tokens; - ordered artifact paths, sizes, and SHA-256 hashes; - signing subject/key alias, signature verification result, and timestamp status without credentials; - test job/run identifiers and acceptance result; - third-party license/provenance inventory; - updater generation/runtime ABI when the artifact participates in native updates. Generate hashes from the files that will actually be published, after signing and final packaging. Store immutable manifests beside immutable artifacts. Comparing two hashes is meaningful only when their declared input and signing policies match. ## Signing and secret boundaries Windows signing is optional and off by default. `Packaging.CodeSigningHook` points to a project-owned executable through a directly usable non-secret path in project config; the packager does not resolve target directives for it. The packager invokes the hook once per staged `.exe` and `.dll` after binary patching and before archive/MSI creation. A nonzero hook exit fails packaging. The hook owns the signing provider, certificate, timestamp service, retry policy, credential environment, and verification. Android release fields come from `Android.Keystore`, `Android.KeystorePassword`, `Android.KeyAlias`, and `Android.KeyPassword` in the baked effective target config. Password strings are passed to Gradle through dedicated environment variables rather than written into the generated project, but they have already traversed baked config and target directives are not resolved. The current Engine therefore has no host-only Android signing-secret handoff; keep production credentials out of Engine config and use a protected project-owned signing stage. Never commit a private key, token, password, keystore password, signing session, production endpoint secret, or decrypted credential to a `.fomain`, package include, log, test fixture, documentation page, artifact manifest, or CI artifact. Use the project's secret manager, limit credentials to the packaging job, redact command output, and verify that fork/untrusted jobs cannot request them. Record identities and verification results, not secret values. [Security and Secrets](security-and-secrets.md) owns substitution timing, the narrow `Common.SecretSettingTokens` masking boundary, the current package-secret limitations, CI isolation, rotation/revocation, incident routing, and secret-free artifact verification. Signing proves artifact integrity and publisher identity. It does not prove gameplay correctness, malware absence, store acceptance, updater compatibility, or safe rollback. ## Acceptance matrix Run the narrow Engine checks first, then the game-owned release checks. A green build alone is insufficient. | Layer | Minimum release evidence | |---|---| | Contract | Package model/check tests and no undeclared grammar drift | | Clean inputs | Exact clean game/Engine revisions and initialized submodules | | Build | Clean release configure and all declared binary variants | | Bake | Fresh resources, scripts, metadata, and selected client/server config | | Package | Expected payload tree and every requested artifact emitted | | Contents | Allowlist/denylist, symbols policy, licenses, no secrets, no stale files | | Integrity | Final SHA-256 manifest and signature verification where required | | Install/start | Real archive/installer/APK/site/service route on representative target | | Runtime | Login/connect, representative map/content/UI, save/persistence, clean shutdown | | Platform | Renderer, input, audio, networking, lifecycle, permissions, update behavior | | Server operations | Service/daemon lifecycle, database migration, backup/restore, observability | | Compatibility | Network, save/schema, updater generation/runtime ABI, old-client policy | | Recovery | Previous artifact/config/data restoration under a rehearsed time objective | Promote a package row to project-qualified only when the named lane is versioned, repeatable, and required for release. Record a failure as a failed candidate; do not overwrite an earlier immutable artifact under the same version. ## Release checklist 1. Freeze the game revision, exact Engine revision, dependency graph, package declarations, and release config. 2. Confirm the target rows are package-capable and their support labels are current. 3. Start from a clean workspace and record toolchain/SDK versions. 4. Build every declared application variant with zero warnings. 5. force-bake and validate the exact release configs and content. 6. Package each host-owned package ID separately and retain complete logs. 7. Audit payload contents, license/provenance records, writable-path behavior, symbols, and secret absence. 8. Sign where required, verify signatures, then hash final artifacts and write the manifest. 9. Install or deploy the emitted artifact through the real distribution path and run the declared acceptance lane. 10. Validate updater, network, save/database, and rollback compatibility against the versions the game still supports. 11. Rehearse or verify [backup/restore](backup-and-recovery.md) and previous-release rollback before broad rollout. 12. Publish immutable artifacts and manifests, monitor the staged rollout, and retain the previous compatible release until acceptance completes. ## Failure routing | Symptom | Inspect first | |---|---| | `Config file not found` | `Config` baker, package `CONFIG`, `Baking/Configs`, and fresh bake output | | Missing binary input | built target/variant, platform architecture key, `POSTFIX`, and shared `FO_OUTPUT_PATH` | | Unknown or invalid pack token | generated package matrix rather than remembered combinations | | Package succeeds but requested artifact is absent | output-producing pack token and final package output path | | Signing was skipped | project config resolution and `Packaging.CodeSigningHook` or Android keystore fields | | Signing fails | isolated signing hook/Gradle job, credentials, timestamp service, and final-byte order | | Web files load but the game does not connect | HTTP/browser diagnostics, WebSocket endpoint, and embedded release config | | APK installs but resources are missing | generated assets, runtime staging, app update/version behavior, and storage logs | | Service or daemon exits immediately | package payload first, then project-owned account/config/database/network/log policy | | Update loops or loads the wrong runtime | package `POSTFIX`, staged runtime name, updater generation/ABI, and server payload inventory | | Same sources produce different hashes | compare the full input/tool/signing manifest before calling the packager nondeterministic | ## Source paths inspected - `BuildTools/PackageInterface.json` - `BuildTools/package.py` - `BuildTools/cmake/helpers/Build.cmake` - `BuildTools/cmake/stages/Packages.cmake` - `BuildTools/tests/test_docs_package.py` - `BuildTools/tests/validate_package_interface.cmake` - `BuildTools/tests/test_package_include.py` - `BuildTools/tests/test_package_security.py` - `BuildTools/tests/test_package_resource_pack.py` - `BuildTools/tests/test_package_zip_helpers.py` - `BuildTools/tests/test_packaging_matrix.py` - `BuildTools/tests/test_minimal_multiplayer_package.py` - `BuildTools/msicreator/createmsi.py` - `BuildTools/check_windows7_imports.py` - `BuildTools/SupportMatrix.json` - `.github/workflows/validate.yml` - `Examples/PackagingMatrix/` - `Examples/MinimalProject/` - `Examples/MinimalMultiplayer/` ## See also - [Generated Package Interface](../../reference/packages/index.md) - [Build Workflow](../build/) - [Project-Local Dependencies](../../../ProjectDependencies.md) - [Support Matrix](../../reference/platforms/support-matrix.md) - [Project Configuration](../build/project-configuration.md) - [Generated Content Workflow](../build/generated-content.md) - [Web Build, Packaging, and Browser Debugging](../platforms/web-debugging.md) - [Android Build, Packaging, and Device Debugging](../platforms/android-debugging.md) - [Client Runtime Split and Updater](../../explanation/runtime/client-updater.md) - [Release Operations](operations.md) - [Engine Upgrade Guide](../migration/engine-upgrade.md) ===== END DOCUMENT packaging-and-release ===== ===== BEGIN DOCUMENT release-operations ===== Source: Docs/en/how-to/release/operations.md Canonical URL: https://fonline.ru/Docs/en/how-to/release/operations.html Content SHA-256: e477cc4cbfab3218cb4dcd3d3b80050ff7fec9505aaa29942ee85889c9934388 --- layout: default title: Release Operations locale: en document_id: release-operations permalink: /Docs/en/how-to/release/operations.html --- # Release Operations This guide covers reusable FOnline server lifecycle, readiness, rollout, shutdown, and rollback. [Packaging and Release](packaging.md) owns artifacts; [Persistence](../../explanation/persistence/) owns database mechanics; [Backup and Recovery](backup-and-recovery.md) owns the provider-neutral recovery procedure; the game owns infrastructure, data policy, objectives, and incidents. ## Rollout decision Run the foreground headless binary under a real supervisor. A detached daemon's parent exits before the child completes startup, so its successful parent exit is not readiness evidence. Readiness requires `Start server complete!` and a project-owned functional probe; a merely live process is explicitly not ready. Stop gracefully with `SIGTERM` on POSIX or `SERVICE_CONTROL_STOP` for the Windows service and wait for `Server stopped!`. The worker drain setting bounds only the first worker-pool drain, not the total stop timeout. Rollback is a new controlled deployment of one data-compatible immutable artifact/config/data unit through the same readiness gate. Never mix binaries, resources, config, or data from incompatible release units. ## Establish the operating boundary The Engine emits binaries, not service accounts, supervisors, containers, traffic/TLS/firewall policy, database clusters, backups, alerts, or on-call policy. Version those in the project. An immutable release unit contains the accepted server binary, baked resources/config, client packs, native updater payloads, and manifest. Keep writable state outside it when atomic replacement or rollback retention requires that split. The optional health file uses an executable-derived name below the same writable root as the log. The read-only `Common.UserWritablePath` is resolved before the log or config is opened, from `--UserWritablePath` or the executable's `INSTALLED` marker; when non-empty it roots the log, health file, cache, resource overlay, self-updated binaries, and server database. Set the working directory and any writable-path override explicitly; launch environments need not choose the same locations. ## Choose the server process | Binary | Runtime behavior | Operational use | |---|---|---| | `_Server` | Windowed server with the application frontend | Local development and attended diagnosis | | `_ServerHeadless` | Foreground, no rendering; waits until quit or startup failure | Preferred process under an external supervisor or container | | `_ServerService` | Windows-only SCM integration; reports `RUNNING` after `ServerEngine::IsStarted()`, stops cleanly on startup failure, and handles `SERVICE_CONTROL_STOP` | A project-qualified native Windows service route | | `_ServerDaemon` | Non-Windows process calls `fork()`, closes standard streams and calls `setsid()` in the child; the current app ignores the returned failure flag and can continue in the original process when `fork()` fails | Legacy detached launch requiring project qualification, PID ownership, and child monitoring | Prefer the foreground headless binary under a supervisor. The daemon's parent exits before the child completes startup, so successful launch exit is not readiness evidence; it writes no PID file and implements no manager protocol. The Windows helper registers fixed SCM name `FOnlineServer` as demand-start. Running the service executable without a recognized control flag registers or updates a command composed from the quoted executable path, the current command line, and `--server-service`; `--server-service-delete` removes the registration, while `--server-service-start` enters the SCM dispatcher. The registered command does not itself use that start flag, so treat the current helper as requiring Windows qualification rather than assuming registration proves service startup. It configures no working directory, account, dependencies, recovery actions, or product-specific name. ## Define readiness and health Process existence is liveness, not readiness. A reusable readiness gate requires all of these observations: 1. The process remains alive and has not reported startup failure. 2. The log reaches `Start server complete!` after runtime, world, scripts, and initial commit succeed. 3. A project-owned functional probe, such as a compatible client handshake/login, succeeds through the real network route. With `Server.WriteHealthFile = True`, startup writes `Starting...` to `_Health.txt`; after `_started`, the periodic writer replaces it with version, time, load, connection, entity, job, rejection, and database metrics. Therefore: - `Starting...` is explicitly not ready; - require the expected version and compatibility version, a parseable body, and a recent modification time; - treat a stale file as unknown or unhealthy, not as proof that the process is dead; - set `Server.HealthFilePeriodMs` to a positive project-qualified interval; - expose the local file through a project-owned probe or sidecar when an orchestrator needs a network endpoint. The Engine provides no HTTP health/drain endpoint or alert. Do not publish the file to an untrusted network. ## Prepare a deployment Before changing a running environment: 1. Identify the exact game and Engine SHAs, package/config ID, artifact hashes, signing result, updater generation/runtime ABI, and database/schema compatibility. 2. Verify the artifact from its immutable store, scan its inventory for secrets, and stage it without modifying the active release. 3. Resolve target-time configuration and credentials on the target host according to [Security and Secrets](security-and-secrets.md). 4. Validate executable, resource, writable-directory, port, firewall, database, and service-account permissions without printing secrets. 5. For durable-state changes, follow [Backup and Recovery](backup-and-recovery.md): capture a backend-consistent backup/snapshot, preserve both recovery oplogs, and prove an isolated semantic restore. An Engine operation log is not a substitute for a backup. 6. Confirm the previous compatible artifact/config/data unit is still available and name the operator authorized to roll back. 7. Run the same start/readiness/smoke/stop sequence in a representative non-production environment. Never point old and new servers at the same writable database concurrently unless the game's persistence design and migration tests explicitly prove that topology. ## Roll out and verify Use a staged rollout even when the final topology has one server: 1. Remove or isolate the instance from new traffic through project infrastructure. FOnline has no built-in drain protocol. 2. Request a graceful stop and wait for the process to exit; do not overwrite files used by a live process. 3. Activate the immutable release directory or versioned image and its matching configuration. 4. Start under a bounded restart policy; repeated startup failure is an incident. 5. Require the complete readiness gate. A Windows `SERVICE_RUNNING` state is useful first-party evidence, but still pair it with a functional probe. 6. Restore traffic gradually; watch restarts, health freshness, connections/rejections, jobs, database/updater errors, and game indicators. 7. Record manifest, environment/config, run identity, timestamps, evidence, and disposition. Before traffic returns, check [Client Runtime Split and Updater](../../explanation/runtime/client-updater.md). Unsupported updater generations require a new full client package. ## Stop safely On Linux/macOS, send `SIGTERM` or `SIGINT` to headless or the daemon child; the loop converts the latched signal to quit. On Windows service use `SERVICE_CONTROL_STOP`. Forced termination bypasses Engine shutdown. `ServerEngine::Shutdown()` stops networking/jobs, fires `OnFinish`, destroys entities/backends, flushes identity/time, waits for database commits, disconnects players, and logs `Server stopped!`. `Server.ShutdownGraceMs` bounds only the first worker-pool drain. Then parked lock waiters are aborted and an unbounded wait resumes; scripts, `OnFinish`, backends, and commits can extend stop. Size manager timeout from measured worst case; forced kill may lose pending state. Accept a graceful stop only when `Server stopped!` is present and the process exits successfully. Preserve logs and investigate a timeout or crash before replacing its evidence. ## Roll back Rollback is a new controlled deployment, not a file copy over a running process: 1. Freeze rollout and remove the affected instance from traffic. 2. Preserve logs, health/crash evidence, release/config identity, and needed database state. 3. Stop the process gracefully and verify exit. 4. Decide whether binary/config rollback is data-compatible. If not, execute the tested [backup/restore](backup-and-recovery.md) or game-owned forward-fix procedure before starting the old server. 5. Activate the previous artifact with matching config/updater payloads and repeat readiness probes. 6. Restore traffic gradually, monitor, and record the incident and final release state. Restore data only under project consistency/data-loss policy. Never mix binaries, baked resources, config, client packs, native updater payloads, or data from incompatible release units. ## Failure routing | Observation | Route | |---|---| | Process exits before readiness or logs `Server startup failed, shutting down` | Keep traffic closed; inspect the first startup exception, config, resources, database, and permissions | | Health file remains `Starting...` | Startup is incomplete; use logs and process state, not the file as a ready signal | | Health file is stale after readiness | Check permissions/scheduling/writer diagnostics; keep state unknown until a functional probe passes | | Daemon launch command exits zero but no ready child appears | Inspect the child log/process; parent exit is expected and is not startup evidence | | Graceful stop exceeds the supervisor timeout | Keep evidence, inspect the last `Shutdown stage:` marker, database availability, and game `OnFinish`; do not assume `ShutdownGraceMs` is a total bound | | New server is ready but clients cannot update or connect | Hold rollout and reconcile network, compatibility, updater generation/runtime ABI, and packaged client payloads | | Rollback binary cannot read current durable state | Do not start it against production data; follow the game-owned migration/restore decision | ## Validate the runbook Automate exact-package install, premature-readiness rejection, ready log/health transition, real handshake/login, graceful stop, `Server stopped!`, and successful exit. Inject startup and unavailable-database failures. Run [Examples/PackagingMatrix](../../../../Examples/PackagingMatrix/) plus project infrastructure/persistence lanes. Rehearse rollback with immutable artifacts and synthetic data. ## Source paths inspected `Source/Applications/Server{App,HeadlessApp,ServiceApp,DaemonApp}.cpp`, `Source/Frontend/Application{Init,Headless}.cpp`, `Source/Essentials/Platform.cpp`, `Source/Server/Server.cpp`, `Source/Common/Settings.inc`, `BuildTools/cmake/stages/Applications.cmake`, `BuildTools/package.py`, `BuildTools/tests/test_docs_release_operations.py`, and `Examples/PackagingMatrix`. ===== END DOCUMENT release-operations ===== ===== BEGIN DOCUMENT backup-and-recovery ===== Source: Docs/en/how-to/release/backup-and-recovery.md Canonical URL: https://fonline.ru/Docs/en/how-to/release/backup-and-recovery.html Content SHA-256: 713a72447d6b5557f13815104d957372dae7cdc87820b332ee8827f55ec4991b --- layout: default title: Backup and Recovery locale: en document_id: backup-and-recovery permalink: /Docs/en/how-to/release/backup-and-recovery.html --- # Backup and Recovery This runbook defines the reusable backup, restore, and disaster-recovery boundary for an FOnline server. Use it with [Persistence](../../explanation/persistence/) for storage mechanics, [Release Operations](operations.md) for process control, and the [Engine Upgrade Guide](../migration/engine-upgrade.md) when durable data crosses an Engine or game revision. ## Recovery decision Identify the complete durable set first: `Memory` has no persistent set; SQLite needs `Storage.sqlite` and active WAL sidecars; JSON needs the complete storage tree; Mongo needs a provider-native consistent backup. The recovery oplog is not a backup: only a command whose backend write reports failure is appended to the pending log, and the committed file records only the committed prefix of that pending log. The two files therefore cannot reconstruct successful writes that were never appended or changes lost through unreported power failure. For a portable baseline, quiesce traffic, stop gracefully through `Server stopped!`, capture the backend plus both oplog files, and call the result only a backup candidate until it restores in an isolated environment. Never mix binaries, resources, and data across incompatible release units. Require `Start server complete!`, a project-owned semantic probe, and a new write/read cycle that proves the write survived. Rehearse from the off-site copy, measure RPO/RTO, record semantic checks, and assign corrective action for every missed objective. The Engine exposes no online backup or checkpoint command; provider-native SQLite or Mongo procedures must come from the reviewed project runbook. ## Establish the recovery boundary The Engine owns the database facade, JSON/SQLite/Mongo/Memory backends, asynchronous commit queue, recovery oplogs, startup replay, panic callback, and graceful commit drain. An embedding game or its operator owns: - the selected backend, storage location, Mongo topology, and connection options; - collection schemas, data migrations, and compatibility between data and code; - backup provider, schedule, retention, encryption, replication, and off-site policy; - recovery point objective (RPO), recovery time objective (RTO), service dependencies, traffic drain, and restore authority; - production credentials, personal-data handling, incident decisions, and destructive-action approval. The Engine has no database snapshot command, online-backup API, point-in-time recovery controller, migration transaction, traffic-drain endpoint, or restore orchestrator. A project must supply and test those operations without presenting them as built-in Engine behavior. Treat one recoverable release as a compatible unit: immutable server package, effective configuration and sub-config identity, Engine and game revisions, baked resources, persistent data, recovery oplogs, migration state, and the secrets or key identifiers needed to read them. Never restore data into an arbitrary binary merely because both start. ## Identify the durable set `Server.DbStorage` selects one of these connection forms: | Backend | Durable data | Consistency and recovery boundary | |---|---|---| | `Memory` | None | Process-local test state. It cannot satisfy a persistent backup or recovery objective. | | `JSON ` | One JSON file per record under collection directories | Each insert/update writes `.json.tmp` and renames it, but operations spanning records are not one transaction. Take a stopped copy or a filesystem snapshot whose consistency has been proven for the complete directory. | | `DbSQLite ` | `/Storage.sqlite` plus active SQLite WAL sidecars | The Engine opens WAL mode with `synchronous=NORMAL`; individual writes autocommit. Do not copy only `Storage.sqlite` while the server is running. Use a provider/SQLite-consistent online backup or stop the server and preserve the complete storage directory. The Engine exposes no online backup or checkpoint command. | | `Mongo ` | The named Mongo database in the configured deployment | The URI/provider defines write concern, replication, snapshot, dump, and point-in-time capabilities; the Engine does not override them. Use a provider-native database-consistent method and record its guarantees. | The Engine also opens `DataBase.OpLogPath` and derives the committed-progress path by replacing `.oplog` with `-committed.oplog`. Their default names are `DbPendingChanges.oplog` and `DbPendingChanges-committed.oplog`. Paths are relative to the server working directory unless the project makes them absolute. Startup rejects only an empty configured path; it does not enforce the `.oplog` suffix. Keep the conventional suffix and verify that the two resolved paths are distinct before deployment. Both oplog files belong to capture and incident evidence. Preserve them with the backend snapshot, even when empty. Never edit, merge, reorder, partially copy, or manually truncate them. ## Understand the recovery oplog The recovery oplog is not a backup, replication stream, audit history, or point-in-time log. Normal writes go directly from the in-memory commit queue to the backend. A command is appended and flushed to the pending oplog only after a backend write reports failure while `DataBase.OpLogEnabled` is true. The commit queue then removes that command. On a successful reconnect or a later process start, the Engine: 1. validates both files and requires the committed prefix to match the pending prefix; 2. replays only pending commands beyond the committed prefix; 3. appends and flushes each replayed command to the committed file; 4. verifies exact line equality, truncates the committed file, then truncates the pending file. Replay is deliberately idempotent within narrow rules: an absent delete is accepted, an identical existing insert is accepted, and an update already contained by the stored document is accepted. A conflicting insert, malformed file, mismatched prefix, replay failure, append failure, or failed truncation stops recovery. With oplog disabled, the first backend write failure starts database panic. With it enabled, the Engine retries according to `DataBase.ReconnectRetryPeriod`; reaching `DataBase.PanicOpLogSizeThreshold` or failing recovery starts panic. Panic requests application shutdown and, after `DataBase.PanicShutdownTimeout`, forces termination. An oplog cannot reconstruct successful changes made after an older backup, and it cannot cover a power loss that the backend did not report. Never combine a stale snapshot and a later oplog and call the result point-in-time recovery. ## Define the backup contract Before operating production, record one reviewed policy per environment: | Required field | What to record | |---|---| | Scope | Backend identity, complete storage set, both oplog paths, external files needed by game-owned persistence, and explicit exclusions | | Consistency method | Graceful-stop copy, filesystem snapshot, SQLite-consistent backup, or Mongo/provider-native snapshot/dump; include the proven atomicity boundary | | Recovery objectives | RPO, RTO, backup frequency, replication lag allowance, and maximum acceptable restore age | | Retention | Rotation classes, off-site copies, legal/privacy expiry, deletion authority, and immutable-copy policy | | Security | Encryption in transit/at rest, restore-role access, key identifier, audit trail, and redaction rules | | Compatibility | Engine/game revision, `CompatibilityVersion`, configuration identity, migration/schema version, and supported rollback versions | | Verification | Hash/inventory checks, plausible size/count comparison with prior recovery points, backend integrity checks, semantic game checks, last isolated restore date, and evidence owner | Each backup needs a sidecar manifest outside the mutable data set. Record a unique backup ID, UTC start/end, environment, source host/cluster, backend and provider snapshot ID, exact paths/namespaces, file sizes and hashes where applicable, Engine and game commit IDs, package/provenance manifest digest, effective non-secret configuration digest, oplog file sizes/hashes, encryption key ID, consistency method, operator/automation identity, and verification status. Do not put credentials or recovered personal data in this manifest. ## Take a quiesced backup A stopped backup is the reusable baseline when no reviewed online method exists: 1. Confirm the target environment, backup ID, restore destination, free capacity, retention class, and operator authority. Reject an ambiguous storage path or database name. 2. Stop new sessions and game-owned mutating jobs through project infrastructure. The Engine has no traffic-drain protocol. 3. Request graceful stop through the process route in [Release Operations](operations.md). Do not copy data merely because the process disappeared. 4. Require `Server stopped!`, successful process exit, no critical database failure, and no warning that pending commits could not be guaranteed. If any is absent, switch to the incident capture path below. 5. Capture the backend using the backend-appropriate complete set. Capture both oplog files without modifying them. 6. Create the sidecar manifest, hashes/inventory, and provider completion evidence. Make the backup immutable according to project policy before reopening traffic. 7. Restore the new backup into an isolated environment and run the acceptance checks. A completed copy without a tested restore is only a backup candidate. 8. Start the production release through the normal readiness gate and retain the previous known-good recovery point until the new one passes policy. An online backup is acceptable only when the backend/provider method gives a documented consistency boundary and the project has restored and validated that exact method under concurrent writes. Process liveness, a filesystem copy tool returning success, or a cloud snapshot being marked complete is not sufficient semantic evidence. ## Capture a database incident After `Critical database failure`, a forced exit, corrupted storage, or a failed oplog replay: 1. Remove traffic and prevent automated restart loops from mutating evidence. 2. Preserve server logs, crash reports, health evidence, effective non-secret configuration identity, backend state, and both oplog files as one timestamped incident set. 3. Do not start a second server against the same files or database namespace. Oplog handles use exclusive file locking, but that does not protect the backend from every external writer. 4. Do not truncate or repair production data in place. Clone the evidence and investigate the clone. 5. Select a known-good backup whose code/data compatibility and integrity are proven. Treat the incident oplog as replay evidence only; do not assume it fills the interval since that backup. 6. Escalate malformed/mismatched oplogs, insert conflicts, backend integrity failures, or unknown migration state to the project data owner before any production write. ## Restore safely Restore first to an isolated namespace or host with outbound player traffic and external side effects disabled: 1. Verify backup identity, retention status, signature/hash/inventory, encryption key access, backend version support, and operator approval. 2. Select the exact compatible server package, Engine/game revisions, baked resources, non-secret configuration, and migration level recorded by the backup. Never mix binaries, resources, and data from different release units. 3. Provision an empty, explicitly allowlisted destination. Refuse a restore over the source or only known-good copy. Where the provider supports namespace/path remapping or a dry run, prove the exact destination before the first write; never infer safety from a similar database or directory name. 4. Restore the complete backend set with its native tool. Restore both oplog files to their recorded `DataBase.OpLogPath` locations, preserving names, bytes, ordering, and permissions. Preserve a failed destination for investigation and delete a successful disposable destination only through an exact-name cleanup guard. 5. Run backend-native integrity/consistency checks before starting FOnline. For JSON, also reject leftover `.tmp` files until their origin is understood; for SQLite, validate the complete WAL-aware database; for Mongo, validate the provider restore result and expected database/collections. 6. Start one isolated server. Startup connects the backend, validates oplogs, restores pending commands, loads persistent entities, and only then reaches `Start server complete!`. Treat any startup/replay exception as restore failure. 7. Run a project-owned semantic probe: authenticate a synthetic account, load representative entities and locations, verify critical balances/progress/references, perform a reversible write, stop gracefully, restart, and prove the write survived. 8. Compare expected collection counts/invariants and migration records with the backup manifest. Backend integrity alone cannot prove game semantics. 9. Record actual restore duration, resulting recovery point, data loss against RPO, all commands/tools used, check results, and approver. Promote the restored environment only through the staged rollout/readiness procedure. Do not let an automatic startup migration modify the first restored copy before an unmodified baseline has been retained. Test forward migration, restart, and any promised rollback against clones. ## Rehearse disaster recovery At a project-defined cadence, execute a full drill from an off-site or otherwise failure-independent copy. The drill must assume the primary storage is unavailable, obtain required keys through the real emergency path, restore infrastructure and data, start the exact compatible package, run semantic checks, and measure RPO/RTO. Include at least these failure cases over time: - missing, expired, corrupt, or partially uploaded backup; - unavailable encryption key or restore credentials; - stale backup plus non-empty oplog; - malformed or prefix-mismatched oplog; - SQLite WAL sidecar omitted from an unsafe live copy; - Mongo snapshot with weaker-than-required consistency; - migration succeeds but rollback cannot read the new data; - restore is technically healthy but a game-level invariant fails. A drill passes only when evidence identifies the backup, release unit, restore owner, measured recovery point/time, backend checks, semantic checks, discrepancies, and corrective action. Update the runbook and automation in the same change as a discovered gap. ## Failure routing | Symptom | Action | |---|---| | No `Server stopped!` or pending commits are not guaranteed | Preserve an incident set; do not label the copy quiesced | | Pending/committed oplogs differ or fail parsing | Stop; preserve both exact files and escalate to Engine/runtime plus project data owner | | Replay reports a conflicting insert or cannot truncate | Stop restart automation; investigate a cloned backend and oplogs | | JSON backup contains unexplained `.tmp` files | Treat it as interrupted/inconsistent until source state is proven | | SQLite live copy omitted WAL state | Reject it; use a complete stopped copy or a proven SQLite-consistent method | | Mongo restore guarantee is unknown | Reject production promotion until provider consistency and write concern are documented | | Restored binary cannot read data or migration is one-way | Keep traffic closed and choose a compatible release/backup or approved forward recovery | | Backend checks pass but semantic probe fails | Keep the restore isolated; route to the owning game schema/system maintainer | | A backup, log, or manifest exposes a secret | Restrict access, revoke/rotate the credential, preserve sanitized incident evidence, and follow [Security and Secrets](security-and-secrets.md) | ## Validate the runbook For every supported persistent backend, automate backup creation, isolated restore, integrity checks, server startup, semantic read/write/restart checks, and measured recovery evidence using synthetic data. Test the exact production topology and provider method in the project lane; Engine unit tests prove backend and oplog mechanics, not an operator's backup system. When persistence or recovery source changes, run `Source/Tests/Test_DataBase.cpp` through the Engine unit-test target. Keep failure-injection coverage for spill-to-oplog, reconnect/replay, invalid records, conflicts, thresholds, and backend round trips. Run the package and release-operations lanes when a recovery unit or process procedure changes. ## Source paths inspected - `Source/Common/Settings.inc` - `Source/Server/DataBase.h` - `Source/Server/DataBase.cpp` - `Source/Server/DataBase-Json.cpp` - `Source/Server/DataBase-SQLite.cpp` - `Source/Server/DataBase-Mongo.cpp` - `Source/Server/DataBase-Memory.cpp` - `Source/Server/Server.cpp` - `Source/Tests/Test_DataBase.cpp` ===== END DOCUMENT backup-and-recovery ===== ===== BEGIN DOCUMENT security-and-secrets ===== Source: Docs/en/how-to/release/security-and-secrets.md Canonical URL: https://fonline.ru/Docs/en/how-to/release/security-and-secrets.html Content SHA-256: e109a9def1e2b0d0abdb1245495135d21c194f2029f97e9aed967cf037ccb0d5 --- layout: default title: Security and Secrets locale: en document_id: security-and-secrets permalink: /Docs/en/how-to/release/security-and-secrets.html --- # Security and Secrets This guide defines the reusable FOnline boundaries for credentials, configuration substitution, package signing, CI, diagnostics, and incident response. It does not choose a secret manager, certificate provider, production account, retention period, or incident policy for an embedding game. Use [Project Configuration](../build/project-configuration.md) for general `.fomain` precedence, [Packaging and Release](packaging.md) for artifact production, and [Client Updater](../../explanation/runtime/client-updater.md) for the downloaded native-runtime boundary. ## Secret decision Use `$ENV{NAME}` only when the value may resolve on the build or baking host; its concrete value can enter baked config. Use `$TARGET_ENV{NAME}` for a value that must remain unresolved until the target application runs. The command-line override log masks only its narrow supported form and does not redact target-time directives, generated files, process state, or arbitrary logs. Keep signing material outside staged artifacts. Hand native signing through `Packaging.CodeSigningHook`, whose project-owned process reads credentials from its protected environment. The current Android packager has no host-only secret input: it reads passwords from baked target config before copying them into `FO_ANDROID_RELEASE_STORE_PASSWORD` and `FO_ANDROID_RELEASE_KEY_PASSWORD` for Gradle. Do not put production passwords into that config; use a project-owned protected signing stage until a dedicated handoff exists. After exposure, contain access, revoke the affected credential, rotate replacements, rebuild or redeploy affected artifacts, and audit use. Rewriting git history is cleanup, not revocation. The network channel has a separate credential from package-signing and account-authentication keys. `ServerNetwork.ChannelSecretKey` is the server's static X25519 secret (64 hex digits); `ClientNetwork.ChannelServerKeys` is the public-key pin list shipped to clients. Generate a new secret on the destination host with `python BuildTools/secure_channel_key.py generate ` and record only the printed public key in client configuration. The command does not overwrite an existing file. Use `$TARGET_FILE{...}` or another target-time secret provision for the server setting; `$FILE{...}` or `$ENV{...}` at bake time may embed the secret. A missing secret or missing client pin fails closed, including for interthread clients. Rotate by shipping the new public pin alongside the old one, switching the host secret, then retiring the old pin. A channel protects traffic and pins the server, but does not sign downloaded binaries or defend against a compromised client or host; signing and updater artifact integrity remain independent gates. See [Networking](../../explanation/authority-and-networking/#secure-channel). ## Threat model and ownership Protect at least these asset classes: | Asset | Typical exposure paths | Owner | |---|---|---| | Runtime credentials | tracked config, baked internal config, process arguments, logs, crash reports, settings UI, memory | embedding project and deployment operator | | Signing credentials | CI variables, local environment, keystore/certificate files, signing-provider session, build logs | release operator | | Release integrity | compromised runner, altered binary after signing, mutable artifact, unverified updater payload | project release pipeline | | Player and operational data | database credentials, backups, support exports, telemetry payloads | project operations | | Engine supply chain | source revision, dependencies, actions, SDKs, package tools | Engine maintainers plus the qualifying project | Treat project source, pull-request code, downloaded dependencies, build runners, package staging, artifact storage, deployment hosts, and the running client/server as separate trust zones. A value becoming available in one zone is not permission to copy it into another. The Engine provides substitution mechanics, one narrow log-masking rule, package-time handoffs, and validation fixtures. The project owns secret creation, access policy, storage backend, rotation, revocation, audit retention, environment separation, and incident severity. Never put real values in Engine examples, tests, documentation, issue text, or generated reports. ## Choose the right configuration form `GlobalSettings::SetValue()` recognizes four substitutions. Their time of resolution is a security boundary, not just syntax. | Form | Normal resolution | During `ConfigBaker` | Correct use | |---|---|---|---| | literal | config parse | copied as the value | public, non-sensitive configuration only | | `$ENV{NAME}` | process reading the authored config | resolves on the baking host, so the concrete value can enter baked config | non-secret build input that is intended to be embedded | | `$FILE{path}` | process reading the authored config; relative to the owning config directory | reads on the baking host, so file contents can enter baked config | non-secret generated metadata such as a version | | `$TARGET_ENV{NAME}` | target application runtime | remains a directive while baking | runtime secret that must not be baked | | `$TARGET_FILE{path}` | target application runtime | remains a directive while baking | protected target-host file whose contents are needed at runtime | For runtime credentials, prefer `$TARGET_ENV{...}`. Use `$TARGET_FILE{...}` only when the deployment owns the file path and permissions; a relative target-file path in an embedded config resolves from the target process context, not from the original repository. Do not assume the packager resolves target directives. Android packaging reads the already baked effective target config; a retained `$TARGET_ENV{...}` value is still a directive string, not a package-host secret lookup. Windows signing reads `Packaging.CodeSigningHook` as a path from the project config and likewise has no target-directive resolver. Keep package credentials out of both authored and baked config. ```ini # Values, paths, and aliases below are examples of variable names, not credentials. Auth.SessionSigningSecret = $TARGET_ENV{MYGAME_AUTH_SESSION_SECRET} Android.Keystore = $TARGET_ENV{MYGAME_ANDROID_KEYSTORE_PATH} Android.KeystorePassword = $TARGET_ENV{MYGAME_ANDROID_STORE_PASSWORD} Android.KeyAlias = $TARGET_ENV{MYGAME_ANDROID_KEY_ALIAS} Android.KeyPassword = $TARGET_ENV{MYGAME_ANDROID_KEY_PASSWORD} Packaging.CodeSigningHook = Tools/SignWindowsArtifacts ``` The Android lines above illustrate runtime-directive syntax only; the current packager does not resolve them for signing. The Windows hook path is non-secret. Its executable obtains credentials from a protected environment that Engine does not inspect. Do not pass a secret as a command-line setting. `Common.SecretSettingTokens` masks matching values only in `ApplyCommandLine()`'s `Set to ` log line. The raw argument can still be visible in shell history, process inspection, `Common.CommandLine`, `Common.CommandLineArgs`, a debugger, or a crash dump. ## Understand redaction limits The default `Common.SecretSettingTokens` entries are case-insensitive name substrings: `secret`, `token`, `password`, and `apikey`. Extend the list for project names such as `dsn`, but treat this as defense in depth only. The Engine does **not** infer a credential type, encrypt settings, zero memory, rotate values, or guarantee redaction outside that one command-line override log. In particular: - config values are ordinary strings after resolution; - `GlobalSettings::Save()` emits applied values in baking mode; - `ConfigBaker` writes applicable values into side-specific internal configs; game settings named by `Baking.BootstrapGameSettings` are written in full to every baked sub-config because their consumers run before metadata exists; - `GlobalSettings::Draw()` renders registered setting values; - project code, third-party libraries, crash handlers, CI shells, and signing tools can log their own inputs; - a custom setting reported as unknown during baking is logged with its current value. Therefore, target-time directives are the default for runtime secrets, production logs and crash attachments require an independent redaction review, and settings/debug UI must not be exposed to untrusted operators. Never test redaction with a real credential. Use a unique synthetic canary in an isolated lane, then delete the lane's logs and artifacts. ## Package and sign without copying credentials ### Windows `Packaging.CodeSigningHook` names a project-owned executable. The packager calls it as ` ` after binary patching and before archive or MSI production. No signing credential is passed as a command-line argument by the Engine. The hook obtains its provider session or credentials from the protected packaging-host environment and must fail nonzero when signing or signature verification fails. The hook path itself must be a directly usable non-secret path; the packager does not resolve `$TARGET_ENV{...}` there. Keep the executable outside untrusted workspace writes, pin or hash it, restrict who can replace it, and have it verify the final signature and timestamp. The current Engine hook signs staged `.exe` and `.dll` files; signing the enclosing installer or publication metadata remains project-owned. ### Android Android release packaging reads `Android.Keystore`, `Android.KeystorePassword`, `Android.KeyAlias`, and `Android.KeyPassword` from the baked effective target config. The complete tuple is required when any member is set. `package.py` then passes the two password strings to Gradle through `FO_ANDROID_RELEASE_STORE_PASSWORD` and `FO_ANDROID_RELEASE_KEY_PASSWORD`; the generated `build.gradle` reads those environment variables instead of containing the passwords. That Gradle handoff prevents password substitution into `build.gradle`, but it does not make the input host-only: a concrete password has already passed through baked config, while a `$TARGET_ENV{...}` directive is not resolved. The current Engine therefore does not provide a production-safe Android signing-secret boundary. Leave the tuple empty for development output or use a protected project-owned Android signing stage whose credentials never enter authored or baked Engine config. The keystore path and key alias are patched into the generated project and are not treated as passwords. Protect the keystore itself, the generated Gradle tree, `GRADLE_USER_HOME`, process memory, and worker logs. An APK produced through the debug-key fallback is a development artifact, not a production release. ### Artifacts and updater payloads Signing does not prove that an artifact is secret-free or that the correct bytes were published. After signing: 1. verify every required signature and timestamp; 2. inventory raw payloads and archives, including client-runtime updater libraries; 3. scan the staged tree and archive members for forbidden files, private keys, config dumps, and synthetic canaries; 4. hash final bytes and bind hashes to the source, Engine revision, toolchain, config, and package identity; 5. publish immutable artifacts only after install/deploy and updater acceptance pass. Do not print a real value to search for it. Prefer forbidden-path and file-type rules, secret-scanning tools, entropy checks with reviewed allowlists, and synthetic canaries created solely for the test lane. ## CI trust boundaries The reusable Engine validation workflow uses top-level `contents: read` permission and does not perform release signing or deployment. Its only current GitHub secret reference is the coverage upload token. That repository state is evidence for Engine validation, not a template proving that an embedding project's release workflow is safe. A project release lane should enforce all of the following: - untrusted pull-request code never receives release, deployment, database, or signing credentials; - signing and deployment run only from reviewed, protected revisions and protected environments; - workflow and third-party action revisions are pinned under project policy; - self-hosted runners are treated as persistent privileged hosts, cleaned between jobs, and isolated from untrusted builds; - secret-bearing steps disable shell tracing and never echo environment, command lines, generated config, or provider responses; - caches never contain keystores, credentials, signed-session state, private config, or production database material; - artifacts have explicit retention and access policy, and upload paths cannot include the entire workspace by accident; - release credentials are scoped to one environment and least privilege; development, staging, and production do not share values; - approval, signing, publication, and rollback events leave a sanitized audit trail. Repository-secret masking is not a content scanner. A transformed, split, encoded, short, or tool-emitted value may escape masking, and a malicious build can exfiltrate a value without printing it. ## Provision, rotate, and revoke Maintain a project-owned inventory with a non-secret identifier, purpose, owner, storage location, consumers, environments, privilege, creation date, rotation rule, and revocation procedure. Do not put the value itself in the inventory. For routine rotation: 1. create the replacement through the owning provider; 2. provision it to the narrow target environment; 3. deploy consumers that can use the replacement, allowing a reviewed overlap only when the protocol requires it; 4. verify normal operation and negative behavior for the old value; 5. revoke the old value; 6. remove stale copies from runners, hosts, caches, backups where policy allows, and operator machines; 7. record sanitized evidence and the next rotation trigger. For suspected exposure, stop affected publication/deployment, revoke first when service safety permits, rotate every credential derived from or co-located with the exposed material, and preserve sanitized forensic evidence. Remove exposed logs and artifacts from normal access, but assume every downloaded copy persists. Rewriting git history or deleting a CI log does not make the old value trustworthy again. Rebuild and re-sign from a known-good revision and runner before resuming rollout. ## Verification workflow Run the focused reusable checks after changing substitution, signing, or security guidance: ```bash python BuildTools/tests/test_package_security.py python BuildTools/tests/test_docs_security_and_secrets.py python BuildTools/tests/test_docs_package.py python BuildTools/docs_snippets.py --check --external python BuildTools/docs_validate.py ``` Then qualify the affected project lane with non-production credentials: 1. inspect the authored `.fomain` and selected sub-config for literals; 2. bake and verify that runtime secrets remain `$TARGET_*` directives in `Baking/Configs`, and that package credentials are absent; 3. package a development artifact and inspect the generated tree, raw package, archives, logs, and manifest; 4. verify the Windows signing hook obtains credentials from its own protected environment, or verify the project-owned Android signing stage without placing secrets in Engine config; 5. install or deploy, start the packaged application, and exercise updater/network behavior; 6. revoke or delete the synthetic credentials and purge the isolated evidence according to test policy. Passing Engine tests proves runtime substitution and the current packager boundaries, including the Android host-only handoff gap. It does not prove a provider account, runner, secret manager, application log, database, distribution store, or incident process is secure. Use [Release Operations](operations.md) for target-host preflight, staged rollout, evidence preservation, and rollback. Use [Backup and Recovery](backup-and-recovery.md) for encrypted recovery sets, restore-role access, key identifiers, and secret-free sidecar manifests. Keep credentials and sensitive incident material out of both operational records. ## Failure routing | Symptom | Inspect first | |---|---| | Concrete credential appears in `Baking/Configs` | replace `$ENV`/`$FILE` with a target form and rebake from a clean output | | Command-line log shows a credential | setting name and `Common.SecretSettingTokens`; rotate the value because other argument exposures remain | | Packaging reports a missing path/file | baked target config or root project config, selected sub-config, and file permissions; do not put a secret there | | Android release uses the debug key | complete four-field signing tuple and selected package config | | Generated Gradle file contains a password | packaging implementation regression; stop and rotate before publication | | Windows hook was skipped | literal `Packaging.CodeSigningHook` path in project config and hook file visibility | | Signature is absent after a successful hook | hook verification/failure contract and post-sign mutation order | | Secret appears in logs, artifacts, cache, or history | revoke/rotate first, restrict access, preserve sanitized evidence, then remove copies | | Untrusted CI job can reach protected material | workflow event, permissions, environment approval, runner isolation, and cache/artifact scope | ## Source paths inspected - `Source/Common/Settings.inc` - `Source/Common/Settings.cpp` - `Source/Frontend/ApplicationInit.cpp` - `Source/Tools/ConfigBaker.cpp` - `Source/Tests/Test_Settings.cpp` - `BuildTools/foconfig.py` - `BuildTools/package.py` - `BuildTools/android-project/app/build.gradle` - `BuildTools/tests/test_package_security.py` - `.github/workflows/validate.yml` ===== END DOCUMENT security-and-secrets ===== ===== BEGIN DOCUMENT project-configuration ===== Source: Docs/en/how-to/build/project-configuration.md Canonical URL: https://fonline.ru/Docs/en/how-to/build/project-configuration.html Content SHA-256: 35098b15063ce35ad3615b9f7d04a08d42bf81898cbf0a2e4a98000c08fb7314 --- layout: default title: Configure a Game Project locale: en document_id: project-configuration permalink: /Docs/en/how-to/build/project-configuration.html --- # Configure a Game Project This guide shows how an embedding project should author its `.fomain` file, resource packs, and named sub-configs. Use [Configuration and Data Sources](../../reference/settings/configuration-and-data-sources.md) for the exact runtime model and [generated settings reference](../../../generated/api/settings.md) for current built-in setting names. ## Source paths inspected - `Source/Common/Settings.h` - `Source/Common/Settings.cpp` - `Source/Common/Settings.inc` - `Source/Frontend/ApplicationInit.cpp` - `Source/Tests/Test_Settings.cpp` - `BuildTools/cmake/stages/ScriptsAndBaking.cmake` - `Examples/MinimalProject/CMakeLists.txt` - `Examples/MinimalProject/FOnlineStarter.fomain` - `Examples/MinimalMultiplayer/CMakeLists.txt` - `Examples/MinimalMultiplayer/FOnlineMinimalMultiplayer.fomain` ## Start from an executable baseline Copy structure from [MinimalProject](../../../../Examples/MinimalProject/README.md) for a headless first run or [MinimalMultiplayer](../../../../Examples/MinimalMultiplayer/README.md) for a client/server game. Keep the game repository as the CMake root and set the master config explicitly: ```cmake include(Engine/BuildTools/Init.cmake) SetOption(FO_MAIN_CONFIG "MyGame.fomain") SetOption(FO_DEV_NAME "MYGAME") SetOption(FO_NICE_NAME "My Game") ``` `FO_MAIN_CONFIG` is a configure-time project option. The `.fomain` contents are runtime and baking settings. Do not move product values into Engine defaults merely to avoid maintaining the project file. ## Understand precedence For an unpackaged application, settings are applied in this order: 1. defaults declared in `Source/Common/Settings.inc`; 2. the selected `.fomain`, found from `-ApplyConfig` or by walking upward for `FO_MAIN_CONFIG`; 3. each explicitly selected `-ApplySubConfig`, in command-line order; 4. the installed-client local config stored in the writable cache, when present; 5. ordinary command-line setting overrides; 6. platform/build auto-settings. Packaged applications use the generated internal config in place of the external `.fomain`. Its patch area is fixed by the Engine at 20000 bytes. A packaged build has no sub-config table and rejects `-ApplySubConfig` instead of silently ignoring it; select the sub-config when constructing the package. Game-setting root values also travel in metadata; the internal config carries only selected sub-config deltas for them, including explicit false or empty overrides. The command line still applies after local config. Later layers override earlier scalar settings. Metadata is applied only after `BaseEngine` construction. If project code reads a declared game setting from `ApplicationInitHook` or an earlier startup path, list its fully qualified name in `Baking.BootstrapGameSettings`. The config baker then writes that setting in full to every internal config and rejects unknown or misspelled names. Do not list ordinary runtime settings: they belong in metadata and consuming the fixed patch area for them can make packaging fail. Use fully qualified names such as `Server.DbStorage` in authored files and operational commands. The parser accepts short built-in names, but qualified names make ownership and review unambiguous. ## Author the root settings Keep one deliberate value per line: ```ini Common.GameName = My Game Common.GameVersion = 0.1.0 Network.ServerPort = 4000 Network.WebSocketPort = 4001 Server.DbStorage = Memory ServerNetwork.DisableNetworking = False Baking.BakeLanguages = engl russ Baking.BakeOutput = Baking Baking.ServerResources = ServerResources Baking.ClientResources = Resources Baking.PlatformBinaries = PlatformBinaries Baking.CacheResources = Cache ManagedScript.Assemblies = MyGame ManagedScript.ProjectName = MyGame ManagedScript.TargetFramework = net10.0 ManagedScript.MsBuild = dotnet msbuild ManagedScript.Dirs = Engine/Source/Scripting/Managed/CoreScripts Scripts ManagedScript.Analyzers = Engine/Source/Scripting/Managed/Analyzers/FOnline.Analyzers.csproj ``` Unknown names become project custom settings and are available through `GetCustomSetting` / `FindCustomSetting`. That is intentional for game-owned configuration, but a typo in a built-in setting can therefore look valid. Add a focused project test for every content ID, port/profile, prototype name, path, or custom setting that affects startup or gameplay. Values beginning with `+` accumulate instead of replacing. String values append with a space, vectors append elements, numeric values add, booleans use logical OR, and enums use bitwise OR; use this deliberately and test the resulting value rather than assuming list-only behavior. `$ENV{NAME}` and `$FILE{path}` resolve while the authored config is read, including during baking, so their concrete values can enter generated internal configs. `$TARGET_ENV{NAME}` and `$TARGET_FILE{path}` remain directives while baking and resolve only when a target application reads them; the current packager does not provide a general target-directive resolver. Keep credentials outside tracked config, use target forms for runtime secrets, and follow [Security and Secrets](../release/security-and-secrets.md) for command-line, logging, signing, CI, rotation, and artifact boundaries. ## Define resource packs A resource pack selects inputs, bakers, and runtime recipients: ```ini [ResourcePack] Name = Protos InputDirs = Content Maps IncludePatterns = ** ExcludePatterns = **/Draft/** Bakers = Proto [ResourcePack] Name = Maps InputDirs = Maps IncludePatterns = **/*.fomap Bakers = Map [ResourcePack] Name = ServerScripts InputDirs = Scripts IncludePatterns = **/*.fos Bakers = AngelScript ServerOnly = True [ResourcePack] Name = ManagedScripts InputDirs = Engine/Source/Scripting/Managed/CoreScripts Scripts IncludePatterns = * Bakers = Managed ``` Choose the backend deliberately. An AngelScript pack bakes `.fos` modules through `AngelScriptBaker`; a Managed pack compiles the configured top-level `.cs` sources and generated API into target-specific assemblies. The Managed pack must include the Engine CoreScripts and project sources selected by `ManagedScript.Dirs`; keep its assembly, analyzer, extra-source/reference, and generated-directory settings aligned with the same build. See [Managed C# Scripting](../scripting/managed-csharp.md) for the complete backend contract. The accepted fields are: | Field | Meaning | |---|---| | `Name` | Required pack identity and generated resource entry | | `ConfigDir` | Derived owning-config directory used to resolve relative inputs; not authored in the section | | `InputDirs` | Space-separated directories, relative to the owning config | | `InputFiles` | Space-separated explicit files, also config-relative | | `IncludePatterns` | Optional input glob allowlist | | `ExcludePatterns` | Optional input glob denylist | | `Bakers` | Space-separated baker names | | `ServerOnly` | Emit only a server resource entry | | `ClientOnly` | Emit only a client resource entry | | `MapperOnly` | Emit only a mapper resource entry | At most one side-only flag may be true. With none set, the pack is delivered to server and client. Mapper-only packs are separate. `RecursiveInput` appears in older project files but is not a current `ResourcePackInfo` field; express recursion through `IncludePatterns = **`. Use separate packs where ownership, release cadence, side visibility, or update policy differs. Do not use pack order as a hidden gameplay override system: duplicate resource identities need an explicit project policy and a test. ## Add named sub-configs Sub-configs are reviewed overlays for a launch mode: ```ini [SubConfig] Name = LocalDev Server.DbStorage = Memory ServerNetwork.DisableNetworking = False Render.RenderDebug = True [SubConfig] Name = TutorialSmoke Parent = LocalDev Tutorial.Automation = True Render.HeadlessWindow = True Render.NullRenderer = True Audio.DisableAudio = True ``` `Parent` names must refer to earlier sub-config sections. Multiple parents are applied left to right; later parents override earlier parents per key, then the child section wins. A launch may pass multiple `-ApplySubConfig` options, which are applied in command-line order. Use `-ApplySubConfig NONE` for generation/baking commands that must consume only the master config. BuildTools does this for `CompileAngelScript`, `CompileManagedScripts`, `BakeResources`, and `ForceBakeResources`. Keep sub-configs narrow: - environment modes choose infrastructure and diagnostics; - tests choose deterministic fixtures and headless behavior; - scenes choose startup content; - release modes choose product-safe settings; - secrets stay out of sub-configs. ## Validate a configuration change 1. Reconfigure the embedding project if CMake options or the main config path changed. 2. Run `CompileAngelScript` and/or `CompileManagedScripts` for every enabled backend whose script inputs, generated API, analyzers, or metadata changed. 3. Run `BakeResources`; use `ForceBakeResources` after pack membership, baker selection, include/exclude patterns, language sets, or migration rules change. 4. Launch the smallest sub-config that consumes the changed setting. 5. Inspect startup logs for `Apply config`, `Apply sub config`, unknown/missing files, skipped languages, missing bakers, and side resource entries. 6. Run a project test that resolves custom settings and content-backed references. 7. Build/package once when internal config or runtime resource entry composition changed. The two Engine-owned examples are executable configuration fixtures: ```bash (cd Examples/MinimalProject && python3 validate.py) (cd Examples/MinimalMultiplayer && python3 validate.py) ``` Use the `win64-` equivalents on Windows. ## Common failures | Symptom | Cause | Recovery | |---|---|---| | `Config file not found` | Wrong working directory, `FO_MAIN_CONFIG`, or `-ApplyConfig` path | Pass the explicit config path or launch below the project root | | `Sub config not found` | Misspelled name or section not loaded | Check section order/name and the applied master config | | `Parent sub config not found` | Parent appears later or is absent | Move parent before child or correct the name | | `Resource pack name not specified` | Missing `Name` | Add a unique pack name | | Side receives an unexpected pack | Missing or incorrect side-only flag | Split packs and inspect generated resource entries | | Incremental bake keeps old output | Pack membership or baker changed | Run `ForceBakeResources` and remove only documented disposable outputs if needed | | Built-in setting appears ignored | Later sub-config/local config/CLI/auto layer wins | Inspect the complete precedence chain | | Typo silently becomes a custom setting | Unknown names are project-owned by design | Add a settings/content validation test | ## Update discipline When Engine or the embedding project is updated, inspect changes to `Settings.inc`, `Settings.cpp`, `ApplicationInit.cpp`, BuildTools project options, baking stages, and the project's `.fomain` range. Update this guide, the project config, tests, and generated references in the same change when precedence, fields, defaults, pack routing, or launch profiles change. ===== END DOCUMENT project-configuration ===== ===== BEGIN DOCUMENT generated-content-workflow ===== Source: Docs/en/how-to/build/generated-content.md Canonical URL: https://fonline.ru/Docs/en/how-to/build/generated-content.html Content SHA-256: 4425e1accf4ed9b3d4dfc0d5e2555e86b7201b695a5e435a5982d370df7fa2f6 --- layout: default title: Generated Content Workflow locale: en document_id: generated-content-workflow permalink: /Docs/en/how-to/build/generated-content.html --- # Generated Content Workflow This guide explains what to regenerate after changing Engine or game sources, what is authoritative, and how to review generated output without editing it by hand. ## Source paths inspected - `BuildTools/Init.cmake` - `BuildTools/cmake/ProjectInterface.json` - `BuildTools/cmake/stages/Codegen.cmake` - `BuildTools/cmake/stages/ScriptsAndBaking.cmake` - `BuildTools/codegen.py` - `BuildTools/docs_metadata.py` - `BuildTools/docs_contract_diff.py` - `BuildTools/docs_validate.py` - `Source/Tools/MetadataBaker.cpp` - `Examples/MinimalProject/CMakeLists.txt` - `Examples/MinimalMultiplayer/CMakeLists.txt` ## Classify the output first FOnline has three distinct generated layers: | Layer | Typical output | Owning input | |---|---|---| | Configure/code generation | build-tree `GeneratedSource/`, generated native bindings and internal config | CMake project interface, C++ tags/templates, project options | | Resource baking | `Baking/`, `Resources/`, `ServerResources/`, `PlatformBinaries/`, `Cache/` | `.fomain` resource packs, scripts, prototypes, maps, assets, metadata tags | | Documentation generation | `Docs/generated/`, `_data/docs-site.json`, search/AI artifacts | source-backed interface models and `Docs/documentation-manifest.json` | Generated output is evidence, not an editing surface. Fix the source annotation, interface model, project config, generator, or authored asset, then regenerate.
Pipeline diagram with four columns. Authoritative engine and game inputs feed CMake configuration and code generation, then native and script compilation plus resource baking, then runtime and contract validation, and finally generated documentation, search, AI delivery, and the reviewed release diff.
Regenerate from left to right. Delivery artifacts consume earlier generated models and hashes, so a green final gate is meaningful only when configure, compile, bake, and focused validation have already succeeded.
## Configure and generate native sources The embedding project calls the staged BuildTools pipeline: ```cmake StartProjectGeneration() RegisterProjectOptions() AddThirdPartyLibraries() RegisterEngineSources() SetupCodeGeneration() BuildCoreLibraries() BuildApplications() SetupScriptsAndBaking() BuildPackages() FinalizeProjectGeneration() ``` `SetupCodeGeneration()` consumes Engine and project native sources, code-generation tags, templates, and project options. `ForceCodeGeneration` is the dependency used by script compilation and baking targets, so stale native metadata cannot be hidden behind an unrelated incremental resource bake. Reconfigure after changing CMake options, source registration, stage hooks, generated templates, or the Engine pin. Build the smallest target that compiles the affected generated source. ## Compile scripts For AngelScript projects: ```bash cmake --build --config RelWithDebInfo --target CompileAngelScript ``` Format authored scripts through the wrapper described in [AngelScript Style and Refactoring](../scripting/style-and-refactoring.md) before compilation. A generated `.fos` failure is fixed in its owning metadata, generator, or authored source and then regenerated; the derived file is not a manual edit target. BuildTools invokes the generated ASCompiler with: ```text -ApplyConfig -ApplySubConfig NONE ``` This validates the master project contract instead of a convenient development overlay. A project may add focused script/test targets, but should keep the master compile route green. For Managed C# projects: ```bash cmake --build --config RelWithDebInfo --target CompileManagedScripts ``` This runs the standalone `ManagedScriptBaker` after `ForceCodeGeneration`, emits target API files plus `.gen.csproj`/`.gen.sln`, and compiles the configured assemblies without performing a full resource bake. The real delivery gate remains `BakeResources` or `ForceBakeResources`: the `Managed` baker writes target-specific assemblies and the prepared `ManagedRuntime/` payload into the selected pack. Fix generated C# at its C++ metadata, configuration, CoreScripts, analyzer, or baker owner and regenerate; do not hand-edit `.gen.cs`. See [Managed C# Scripting](../scripting/managed-csharp.md). ## Bake resources Run the normal incremental route first: ```bash cmake --build --config RelWithDebInfo --target BakeResources ``` Use the forced route when the input graph changed: ```bash cmake --build --config RelWithDebInfo --target ForceBakeResources ``` A forced bake is appropriate after changing: - resource-pack directories, explicit files, include/exclude patterns, recipients, or baker lists; - baker behavior or a baked binary schema; - language set/order or text fallback policy; - prototype/map migrations or identity rules; - generated metadata tags, entity/property layouts, remotes, enums, fixed/value/ref types; - output paths or platform binary composition; - an incremental-cache bug or missing dependency. Do not routinely delete the whole workspace. Preserve logs and failed outputs long enough to diagnose ownership, then remove only documented disposable directories. ## Understand metadata outputs `MetadataBaker` parses project script tags and emits side-specific metadata such as: ```text Baking/Metadata/Metadata.fometa-server Baking/Metadata/Metadata.fometa-client ``` The pair is consumed by runtime dynamic metadata registration and can also generate a project-owned remote-call catalog: ```bash python Engine/BuildTools/docs_metadata.py \ --metadata Baking/Metadata/Metadata.fometa-server \ --metadata Baking/Metadata/Metadata.fometa-client \ --write ``` Both sides must agree on every paired remote call, including its `MaxBytes` and `MaxCollectionSize` structural limits. Every record carries a mandatory `Limits` trailer, with zeroes when limits are omitted. Do not reconstruct that catalog by parsing `.fos` or `.cs` with a second grammar; the baked metadata is authoritative. Metadata changes can affect persistence, network synchronization, script bindings, content validation, and save compatibility even when native C++ compiles. Review the generated model and run a real bake plus the narrow runtime/test route. ## Regenerate documentation contracts Each checked interface owns its generator. Run the affected generator with `--write`, then verify all outputs: ```bash python BuildTools/docs_diagrams.py --write python BuildTools/docs_screenshots.py --write python BuildTools/docs_reference.py --write python BuildTools/docs_snippets.py --write --external python BuildTools/docs_description_translations.py --write python BuildTools/docs_localization.py --write python BuildTools/docs_site.py --write python BuildTools/docs_ai_eval.py --write python BuildTools/docs_ai_delivery.py --write python BuildTools/docs_validate.py ``` Focused format/CLI/CMake generators are listed in [Generated API and Metadata](../../reference/metadata/index.md). `docs_validate.py` checks byte-for-byte freshness; it is not a replacement for the focused semantic test. For a source/API change, compare against the base revision: ```bash python BuildTools/docs_contract_diff.py \ --baseline-git-ref \ --current-dir Docs/generated \ --dispositions Docs/contract-change-dispositions.json \ --write \ --enforce ``` Complete the required owner, migration, release-note, and compatibility dispositions. Never edit generated JSON merely to silence the comparator. ## Dependency order Use this order when a change crosses layers: 1. update source contracts, project configuration, authored content, and tests; 2. reconfigure and regenerate native sources; 3. compile native targets and scripts; 4. bake resources and side-specific metadata; 5. run focused native/content/runtime tests; 6. regenerate canonical documentation models, source-owned diagrams and screenshot catalogs, and Markdown projections; 7. regenerate example and snippet inventories, localization status, then route, site, search, AI-evaluation, and AI-delivery artifacts; 8. run aggregate documentation validation and contract diff; 9. inspect the final diff for unexpected generated churn. Later steps may consume hashes or inventories from earlier ones. Running site/AI generation before canonical pages are current can produce internally consistent but stale delivery artifacts. ## Review generated changes Review the input and output together: - generated symbols should trace to a source tag/template; - generated settings/options should trace to their runtime-consumed interface; - baked files should trace to exactly one resource pack and baker; - side-specific metadata should agree where a contract is paired; - removed IDs need migration and compatibility review; - unrelated mass churn usually indicates a path, ordering, line-ending, toolchain, or non-determinism problem. Generated artifacts should be deterministic for the same inputs. Run the generator twice or use its `--check` mode to prove this before committing. ## Recovery | Failure | Recovery | |---|---| | Generated source does not compile | Fix the source tag/template or project registration, reconfigure, then rebuild | | Script compiler and runtime disagree | Ensure every enabled backend uses the same config, Engine revision, and fresh generated metadata; for Managed also inspect the target assembly and `ManagedRuntime/` payload | | Formatter changes `T?`, a cast/template form, or a named argument | Use the Engine-aware wrapper from [AngelScript Style and Refactoring](../scripting/style-and-refactoring.md), not raw clang-format | | Metadata sides disagree | Fix paired declarations and rebake both sides | | Incremental resources stay stale | Run `ForceBakeResources`; inspect pack selection and baker dependency tracking | | Documentation `--check` fails | Run the named generator with `--write`, then inspect why source changed | | Contract diff reports a break | Restore compatibility or add the exact reviewed disposition; do not hide the model delta | | Site/search/AI output changes unexpectedly | Regenerate canonical pages first, then delivery artifacts in dependency order | ## Update discipline Every Engine or embedding-project update is a generation-bearing change. Record old/new revisions, audit the complete range, identify affected generated layers, regenerate in dependency order, and update owning docs/tests in the same work. A green native build alone is not evidence that scripts, resources, metadata, documentation, or compatibility outputs are current. ===== END DOCUMENT generated-content-workflow ===== ===== BEGIN DOCUMENT support-matrix ===== Source: Docs/en/reference/platforms/support-matrix.md Canonical URL: https://fonline.ru/Docs/en/reference/platforms/support-matrix.html Content SHA-256: d356552b3da1aa91e0a16896da15f00e8113d95f2a259a0c730f454458d4efa4 --- layout: default title: Support Matrix locale: en document_id: support-matrix permalink: /Docs/en/reference/platforms/support-matrix.html --- # Support Matrix This page defines what the rolling `current` FOnline documentation may call supported. It separates source capability, required CI compilation, automated process smoke tests, and project release acceptance. Use the generated [exact matrix](generated-matrix.md) for the current platform profiles and validation target names. The machine-readable model is [support-matrix.json](../../../generated/support-matrix.json). ## Support decision Use only these evidence labels: **Build-gated** means required CI configures and compiles it; **Smoke-gated** adds an automated process route; **Source-capable** means the source exposes it without required CI proof; and **Project-qualified** means an embedding game repeatedly accepts its actual artifact. A project release matrix must keep Renderer, Networking, Packaging, and Updater evidence explicit. A build or Engine smoke does not silently fill those project-owned cells. Before a release claim, bind those cells to the same versioned artifact and include its install/start/runtime result; an implemented payload branch is capability, not "full support." ## Source paths inspected - `BuildTools/SupportMatrix.json` - `BuildTools/docs_support_matrix.py` - `BuildTools/buildtools.py` - `BuildTools/cmake/stages/Init.cmake` - `BuildTools/cmake/stages/Applications.cmake` - `.github/workflows/validate.yml` - `Examples/MinimalProject/` - `Examples/MinimalMultiplayer/` - `Docs/generated/support-matrix.json` - `Docs/en/reference/platforms/generated-matrix.md` ## Support vocabulary - **Build-gated** means the required workflow configures and compiles that profile on every change. - **Smoke-gated** means the profile is build-gated and a named starter or multiplayer process route also runs. - **Source-capable** means BuildTools exposes the profile, but required CI does not exercise it. - **Project-qualified** means an embedding project has added the runtime, packaging, hardware, service, or store checks needed for its release. Engine CI cannot make this claim for a game. - **Unsupported for release** means a project has neither an Engine build gate nor its own maintained acceptance route. These labels are deliberately narrower than "works on my machine." A successful cross-compile does not prove a window opens, a device resumes correctly, a browser connects, a renderer works on shipping drivers, or a signed package can be installed. ## Current qualified baseline The strongest required reusable route is native Windows x64 and Ubuntu 24.04 x64: 1. The required workflow builds the desktop client, server, mapper/viewers, AngelScript compiler, and baker. 2. Engine-owned examples provide opt-in local validators for a minimal headless project, tutorial multiplayer flow, native extensions, packaging, and Content Showcase; they are not registered as required workflow lanes. 3. Visible rendering, audio, signing/installers, persistence backends, public networking, and long-running service operation remain project-owned acceptance concerns. Windows x86, Linux GCC, macOS, iOS, Android ARM, and Web are build-gated at the narrower scope recorded in the generated matrix. Android x86 and Windows ClangCL are source-capable profiles, not release support claims. ## Application boundaries Desktop builds may expose more applications than mobile and Web builds. The public mobile/Web matrix covers the client path only. Do not infer supported servers, mappers, bakers, compilers, services, or daemons on those targets merely because common source can compile there. The actual application construction lives in `BuildTools/cmake/stages/Applications.cmake`. The public validation names live in `BuildTools/buildtools.py`; `.github/workflows/validate.yml` decides which names are required gates. The generic Editor target has been removed. Interactive map authoring is provided by Mapper; animation and particle inspection use their focused viewers. Required CI must not retain `*-editor` validation names after the corresponding BuildTools target disappears. ## Renderer boundaries Backend availability is a compile-time capability, not visual qualification: - Windows, Linux, and macOS compile their platform OpenGL path and can include Vulkan and SDL_GPU unless disabled. - Android and iOS compile mobile platform capabilities; device acceptance remains mandatory. - Web uses WebGL 2. The platform stage excludes Vulkan and SDL_GPU. - Headless smoke tests deliberately prove no pixels and no audible output. Every game that ships a renderer must maintain a representative visible scene on each supported GPU/platform family. Capture startup, map load, resize/orientation where applicable, device loss or background/resume where applicable, and at least one effect, font, image, model or sprite, and GUI path used by the product. ## Project release matrix An embedding project should copy the evidence model, not this table. For every shipping combination record: | Dimension | Required project evidence | |---|---| | Host and compiler | Clean configure/build on the pinned Engine revision | | Client platform and architecture | Install or launch on representative hardware/runtime | | Server platform | Process/service lifecycle, database, backup, restore, logs, and graceful shutdown | | Renderer | Visible scene and driver/device coverage | | Networking | Native or WebSocket transport, reconnect, timeout, and compatibility behavior | | Packaging | Reproducible package, contents audit, signing/notarization/store route | | Localization | Bake, glyph coverage, layout, input, and language switching | | Updater | Exact protocol/ABI compatibility and rollback/reinstall policy | A project may promote a profile only after those gates are versioned and repeatable. A temporary manual pass is useful evidence, but it is not the same as maintained support. Use [Packaging and Release](../../how-to/release/packaging.md) to turn the packaging row into an auditable project procedure and acceptance lane. ## Adding or changing a profile 1. Add or modify the real validation target in `BuildTools/buildtools.py`. 2. Add it to the required workflow when it is meant to be build-gated. 3. Update `BuildTools/SupportMatrix.json` with the narrowest truthful level. 4. Run: ```bash python BuildTools/docs_support_matrix.py --write python BuildTools/tests/test_docs_support_matrix.py python BuildTools/docs_support_matrix.py --check ``` 5. Update platform how-tos and the embedding project's release matrix when behavior or prerequisites changed. 6. Do not promote `source_capable` to `build_gated` without required CI, or `build_gated` to `smoke_gated` without an executable route. ## Maintenance The generated model rejects unknown BuildTools target names and claims that a CI target exists when it is absent from the required workflow. It cannot infer runtime quality from compilation, so runtime evidence and limitations remain reviewed policy text. When Engine or an embedding project is updated, audit the complete incoming range for changes to platform detection, minimum toolchains, application construction, BuildTools validation profiles, workflow runners, renderer gates, package support, and updater boundaries. Update this matrix in the same change. ===== END DOCUMENT support-matrix ===== ===== BEGIN DOCUMENT engine-upgrade-guide ===== Source: Docs/en/how-to/migration/engine-upgrade.md Canonical URL: https://fonline.ru/Docs/en/how-to/migration/engine-upgrade.html Content SHA-256: c0e5519a74ffe4d8bba9e9dff36a9673095fee0ac855095375d9e26bee13fdb4 --- layout: default title: Upgrade an Embedding Project locale: en document_id: engine-upgrade-guide permalink: /Docs/en/how-to/migration/engine-upgrade.html --- # Upgrade an Embedding Project This guide provides a repeatable Engine-update procedure for a game repository. It covers source integration, generated contracts, content, saves, networking, client runtime, and documentation. ## Source paths inspected - `AGENTS.md` - `BuildTools/docs_contract_diff.py` - `Docs/en/contributing/contract-change-management.md` - `Docs/en/explanation/runtime/client-updater.md` - `Docs/en/explanation/persistence/index.md` - `Docs/en/how-to/build/project-configuration.md` - `Docs/en/how-to/build/generated-content.md` - `Docs/en/reference/platforms/support-matrix.md` - `Docs/en/contributing/documentation/index.md` ## Define the update Record before touching the submodule or vendored Engine checkout: - embedding-project root revision; - old exact Engine revision; - intended new exact Engine revision; - upstream branch/repository; - supported build/runtime matrix; - safety branch or stash names; - owner of compatibility, persistence, release, and documentation review. An Engine update is not a pointer-only change. The complete incoming Engine range and the project changes made to adopt it form one review unit. ## Preserve the starting state 1. Fetch the project and Engine remotes. 2. Confirm whether either worktree has local changes. 3. Create a named safety branch and, when needed, a named stash in both repositories. 4. Record `git rev-parse HEAD`, the Engine gitlink, and remote tips. 5. Keep safety refs until the updated project validates cleanly. Do not reset, discard, or overwrite unrelated local work to make the update look clean. ## Audit the complete Engine range Inspect every commit and changed path between the old and new pins: ```bash git -C Engine log --oneline .. git -C Engine diff --stat .. git -C Engine diff .. -- \ Source BuildTools ThirdParty Resources Docs Examples ``` Classify changes by owner and consequence: | Change | Required review | |---|---| | CMake option/stage/application/package | Project configure, target, CI, package, and support-matrix impact | | Project library helper/core-role graph | Project dependency targets, role assignment, native bridge, platform gates, and runtime package impact | | Setting/default/config parser | `.fomain`, sub-config, secret, resource-pack, and launch impact | | Script API/metadata/property | Compile, bake, network, persistence, migration, and gameplay impact | | Baker/file format/resource runtime | Authored content, forced rebake, cache/output schema, and platform impact | | Networking/updater/client runtime | Protocol, gameplay compatibility, host/runtime ABI, package, and rollout impact | | Database/entity serialization | Save migration, backup/restore, rollback, and mixed-version prohibition | | Tool/editor | Authoring workflow, round trip, generated files, and screenshots/manual impact | | Documentation/example | Engine/project ownership, links, commands, pins, and translation freshness | Use Last Frontier or TLA only as integration evidence. The Engine source, tests, interfaces, and generated models remain normative. ## Compare generated contracts Generate the new Engine models, then compare the old revision: ```bash python Engine/BuildTools/docs_contract_diff.py \ --root Engine \ --baseline-git-ref \ --current-dir Docs/generated \ --dispositions Docs/contract-change-dispositions.json \ --write \ --enforce ``` For every change determine: - additive, documentation-only, policy-only, or breaking; - current stability promise; - project code/content affected; - migration and release-note requirement; - minimum compatible client/server/save revision; - whether rollback remains possible after data conversion. Do not treat an `internal` label as proof of no project impact. It only means the Engine has not made a public compatibility promise. ## Reconcile project configuration Compare the project's CMake root and `.fomain` against: - generated [CMake reference](../../reference/cmake/index.md); - [project-local dependency guide](../../../ProjectDependencies.md); - generated [settings reference](../../../generated/api/settings.md); - [project configuration guide](../build/project-configuration.md); - [security and secrets guide](../release/security-and-secrets.md); - changed BuildTools validation/package interfaces. Remove retired options and targets, add required values explicitly, review defaults, and test every sub-config used by CI, development, staging, and production. Re-audit `$ENV`/`$FILE` versus `$TARGET_ENV`/`$TARGET_FILE`, command-line masking tokens, side-specific baked configs, package signing handoff, and secret-bearing CI jobs. A project config should record deliberate product choices instead of inheriting a new default accidentally. ## Rebuild generated and baked data Follow [Generated Content Workflow](../build/generated-content.md) in dependency order: 1. fresh configure/code generation; 2. native compile; 3. script compile; 4. forced resource bake when contracts or pack inputs changed; 5. side-specific metadata comparison; 6. project-generated references and snippet inventory; 7. localization status; 8. site routes, navigation, and search; 9. AI evaluation and delivery artifacts. Keep old and new generated contract reports as update evidence. Do not hand-edit generated source or baked output. ## Protect persisted state Before testing against valuable data: 1. create and verify a backup; 2. rehearse restore into an isolated database; 3. identify property/prototype/version migration rules; 4. test the upgrade on a representative copy; 5. verify entity counts, ownership, critical fields, and login/loading paths; 6. decide whether the migration is reversible; 7. prohibit old binaries from opening converted data when rollback is unsafe. Property and prototype rename/remove rules are runtime contracts, not cleanup conveniences. Update references in authored content and scripts, keep migration rules for the supported save horizon, and test missing/legacy values. Execute the provider-neutral procedure in [Backup and Recovery](../release/backup-and-recovery.md). The Engine database abstraction does not choose a game's provider, schedule, retention, schema rollout, RPO/RTO, or disaster-recovery authority; keep those concrete decisions and evidence in project operations documentation. ## Protect network and client compatibility Review three independent boundaries: - gameplay `CompatibilityVersion`; - updater protocol generation; - frozen client host/runtime ABI. Do not assume one version covers the others. A protocol/ABI break can require a full client package and manual reinstall even when resources can self-update. A gameplay compatibility change can reject mixed client/server revisions without changing updater wire format. For an online rollout define: 1. accepted old client cohort; 2. resource/native update path; 3. server deployment order; 4. reconnect behavior; 5. rollback point; 6. user-facing recovery for incompatible frozen hosts; 7. monitoring for update, login, sync, and migration failures. Use [Client Runtime Split and Updater](../../explanation/runtime/client-updater.md) for the exact current host/runtime and updater boundary. Execute the deployment, readiness, graceful-stop, and rollback sequence through [Release Operations](../release/operations.md). ## Validate the adoption Run the narrowest checks first, then the full declared project matrix: - Engine unit tests for changed native domains; - configure and compile with each supported host compiler; - `CompileAngelScript` and/or `CompileManagedScripts` for every scripting backend enabled by the project; - `ForceBakeResources` when the data graph changed; - focused content/gameplay tests; - starter/tutorial smoke when integration mechanics changed; - visible client scene for rendering, GUI, audio, video, input, maps, or assets; - persistence upgrade/restore rehearsal; - client/server compatibility and updater route; - package contents and install/launch; - synthetic-secret checks across baked configs, package trees, archives, logs, and signing handoff; - documentation generators, links, locale freshness, site artifact, and AI delivery. Map claims to [Support Matrix](../../reference/platforms/support-matrix.md). A cross-build is not device qualification, and a headless test is not visible-client evidence. ## Update documentation in the same work Reconcile: - reusable behavior in `Engine/Docs/`; - project integration and product policy in the embedding project's docs; - `AGENTS.md` routing when ownership or required procedure changed; - public examples and exact Engine pins; - generated API/format/settings/package references; - support matrix and platform guides; - English source pages and every existing translation whose source hash changed; - active plan, update record, and verification report. The project documentation may link to Engine mechanics but should not duplicate them. Engine docs must not use a private game repository as normative proof. ## Completion record An update record should contain: ```text Project old/new: Engine old/new: Incoming Engine commits audited: Generated contract report: Required dispositions: Configuration changes: Content/resource migrations: Save migration and restore evidence: Network/updater/ABI decision: Validated host/target matrix: Visible/device checks: Documentation and translation status: Known residual risks: Safety refs retained until: ``` Do not call the update complete while required evidence is missing. Record an unobserved platform or owner-gated deployment as pending rather than inferring success from adjacent checks. ===== END DOCUMENT engine-upgrade-guide ===== ===== BEGIN DOCUMENT scripting-runtime ===== Source: Docs/en/explanation/scripting-runtime/index.md Canonical URL: https://fonline.ru/Docs/en/explanation/scripting-runtime/ Content SHA-256: 0959ed1e4851cec7ee7941f6af383b0306ff3e17620dbbe54ab7362875074d3e --- layout: default title: Scripting locale: en document_id: scripting-runtime permalink: /Docs/en/explanation/scripting-runtime/ --- # 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: - [Managed C# Scripting](../../how-to/scripting/managed-csharp.md) for the complete managed authoring, generated-project, async, synchronization, runtime, packaging, platform, diagnostics, and migration contract. - [AngelScript Style and Refactoring](../../how-to/scripting/style-and-refactoring.md) for module construction, source layout, formatter behavior, generated-file discipline, refactoring batches, and validation gates. - [GeneratedApiAndMetadata.md](../../reference/metadata/index.md) for generated metadata, `///@` annotations, and codegen output. - [Script Lifecycle and Concurrency](../../how-to/scripting/lifecycle-and-concurrency.md) for module initialization, callback ownership, `[[Async]]`, `Yield`, server synchronization covers, mutable-state ownership, and teardown rules. - [Remote Calls](../../reference/scripting/remote-calls.md) for remote-call grammar, direction, handlers, authority, project catalog generation, and validation. - [Nullability.md](../../../Nullability.md) for `T?` (script) and `ptr`·`nptr` (native) contracts across script/native boundaries. - [Entity Model](../entity-and-property-model/) for entity, prototype, property, and holder concepts exposed to scripts. - [Server Runtime](../runtime/server.md) and [Client Runtime](../runtime/client.md) for runtime events and script callback ownership. - [Mapper Tools](../../how-to/tools/mapper.md) for mapper-specific script helpers. - [Script Methods Map](../../reference/script-api/method-ownership.md) for the native script method file map. - [Text and Localization](../../how-to/content/text-and-localization.md) for `TextPackKey`, `LanguageName`, `Game.GetText`, language switching, and the boundary from project-owned lexem formatting. ## Source paths inspected - `Source/Common/ScriptSystem.h` - `Source/Common/ScriptSystem.cpp` - `Source/Scripting/AngelScript/AngelScriptScripting.h` - `Source/Scripting/AngelScript/AngelScriptScripting.cpp` - `Source/Scripting/AngelScript/AngelScriptBackend.h` - `Source/Scripting/AngelScript/AngelScriptBackend.cpp` - `Source/Scripting/AngelScript/AngelScriptAttributes.cpp` - `Source/Scripting/AngelScript/AngelScriptCall.cpp` - `Source/Scripting/AngelScript/AngelScriptEntity.cpp` - `Source/Scripting/AngelScript/AngelScriptGlobals.cpp` - `Source/Scripting/AngelScript/AngelScriptRemoteCalls.cpp` - `Source/Scripting/AngelScript/AngelScriptReflection.cpp` - `ThirdParty/AngelScript/sdk/angelscript/source/as_compiler.cpp` - `Source/Scripting/*ScriptMethods.cpp` - `Source/Scripting/Managed/CoreScripts/*.cs` - `Source/Scripting/Managed/ManagedScripting.*` - `Source/Scripting/Managed/ManagedScriptBackend.*` - `Source/Scripting/Managed/ManagedInteropAbi.*` - `Source/Scripting/Managed/ManagedRuntime.*` - `Source/Scripting/Managed/ManagedHost/ManagedLoadContextHost.cs` - `Source/Tools/ManagedScriptBaker.*` - `Source/Scripting/Native/.keepalive` - `BuildTools/cmake/stages/ScriptsAndBaking.cmake` - `Source/Tests/Test_AngelScriptAttributes.cpp` - `Source/Tests/Test_AngelScriptBaker.cpp` - `Source/Tests/Test_AngelScriptBytecode.cpp` - `Source/Tests/Test_AngelScriptCall.cpp` - `Source/Tests/Test_ManagedScriptBaker.cpp` - `Source/Tests/Test_ClientDataValidation.cpp` - `Source/Tests/Test_CommonScriptMethods.cpp` - `Source/Tests/Test_EntityLifecycle.cpp` - `Source/Tests/Test_EntitySync.cpp` - `Source/Tests/Test_MetadataBaker.cpp` - `Source/Tests/Test_NetBuffer.cpp` - `Source/Tests/Test_ScriptBuiltins.cpp` - `Source/Tests/Test_ScriptEntityOps.cpp` - `Source/Tests/Test_ServerScriptMethods.cpp` ## Layer map The scripting subsystem has four layers: 1. **Common runtime facade** — `Source/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 implementations** — `Source/Scripting/AngelScript/` provides the AngelScript compiler/runtime and `Source/Scripting/Managed/` provides the Managed C# compiler/runtime bridge hosted on embedded Mono. Both are implemented and tested backends. `Source/Scripting/Native/` is only a reserved source root and must not be presented as operational. 3. **Script-visible native methods** — `Source/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 library and game scripts** — `Source/Scripting/Managed/CoreScripts/*.cs` provides the reusable managed bridge library. High-level gameplay, GUI, and other project libraries belong to the embedding project for both languages; AngelScript no longer has an Engine-owned `CoreScripts` library. Projects select `.cs` and/or `.fos` sources through configuration and script/resource 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: - register one or more `ScriptSystemBackend` instances with `RegisterBackend()`; - map C++ types to metadata descriptors with `MapScriptTypes()` and `MapEngineType()` / `MapEngineDictType()`; - initialize modules with `InitModules()`; - find and invoke global functions through `FindFunc()`, `CheckFunc()`, `CallFunc()`, and `CallAdminFunc()`; - store `ScriptFuncDesc` entries from backends with `AddGlobalScriptFunc()`; - run registered init functions and loop callbacks through `AddInitFunc()`, `AddLoopCallback()`, and `ProcessScriptEvents()`. `ScriptFunc` 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](../../../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: - `RegisterMetadata()` binds engine metadata and registers C++/script-visible types. - `BindRequiredStuff()` registers arrays, dictionaries, strings, math/value types, globals, entity wrappers, remote callers, reflection helpers, and backend helpers. - `CompileTextScripts()` preprocesses script source, adds script sections to a module, resolves includes, builds the module, and serializes bytecode. - `LoadBinaryScripts()` loads compiled bytecode from resources at runtime. - `SetMessageCallback()` / `SendMessage()` route compiler/runtime diagnostics to the caller. AngelScript diagnostic locations keep the original script line but format only the source file name, not the full source path, so logs remain stable across local and CI workspaces. - cleanup callbacks and post-cleanup callbacks release backend-owned resources in a controlled order. 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. Runtime overrun diagnostics use `Script.OverrunReportTime` as an independent threshold for two measurements. `Script execution overrun` reports wall time after subtracting the server synchronization context's accumulated entity-lock wait, while `Script lock wait overrun` reports the contention component itself. Both messages include execution, lock-wait, and total wall durations, so a compute-heavy function and a wait-heavy function stay separately searchable without losing the full latency picture. Non-server engines return zero lock wait. A value of zero still disables both diagnostics, and an attached debugger still suppresses them. Managed entries use the matching `ManagedScript.OverrunReportTime` threshold and the same two message shapes. Both backends suppress these diagnostics while `BaseEngine::IsStartingUp()` is true. Every Engine starts in that state and calls `FinishStartingUp()` exactly once when it begins serving: the server at the end of `InitDoneJob`, the client at the end of `ClientEngine` construction, and Mapper at the end of `MapperEngine` construction because it continues initialization after the shared client constructor. There is no setter or transition back to start-up; a second finish is a duplicated start path and throws. One-time shader, font, GUI, model, or similar loading during construction is therefore not reported as a responsiveness failure before anything can be waiting on a frame. Profiling builds also expose Managed C# method and JIT zones through Mono. See [Managed C# diagnostics](../../how-to/scripting/managed-csharp.md#diagnostics-and-debugging) for the instrumentation contract and [Profiling](../../how-to/quality/profiling.md#managed-script-zones) for capture interpretation. AngelScript assigns registered object type IDs lazily. Separate script contexts may request the same fresh type concurrently, so the vendored runtime reads and initializes `asCTypeInfo::typeId` under the engine reader/writer lock and refreshes the value after acquiring exclusive access. `AngelScriptTypeIdsAreLazilyAssignedAcrossThreads` drives 16 native workers over 128 new types through public `asITypeInfo::GetTypeId()` and requires one identical valid ID per type. 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. Destroyed entities are rejected at the script-to-native boundary too. `NativeDataCaller::ConvertArg` validates access first and then rejects a destroyed entity for every ordinary `///@ ExportMethod`. On the server, an uncovered destroyed handle therefore reports the actionable missing-cover fault; a still-covered handle that the caller destroyed and reused reports the destroyed-argument fault. The client has no cover validation and sees only the latter. The sole opt-out is `///@ ExportMethod ... AllowDestroyedEntityArgs`, emitted as a compile-time call-policy flag. It exists for explicit synchronization primitives such as `Game.Sync`: a concurrent destroy can always happen between a script liveness check and the synchronization call, and these wrappers must return `false` rather than fail at argument conversion. Do not apply the flag to ordinary exports. `ServerEngineDestroyedEntityArgumentReportsMissingCoverFirst` and `SyncAcceptsDestroyedEntity` pin the two contracts. ## Attributes, declarations, and metadata `Source/Scripting/AngelScript/AngelScriptAttributes.cpp` parses engine-specific script attributes and declaration tags. Important contracts include: - nullable `T?` suffix stripping and propagation into metadata; - `///@ Event` declarations and matching `[[Event]]` handlers; - `///@ RemoteCall` declarations, optional structural `MaxBytes N` / `MaxCollectionSize N` limits, and matching `[[ServerRemoteCall]]` or `[[ClientRemoteCall]]` implementations; - the separate `[[AdminRemoteCall]]` command entry point; - module/init-function priorities; - callback attribute validation rules; - `[[InvokeEntry]]` for functions dispatched only by name through the global `Invoke(...)` helper. It blocks ordinary direct calls while still allowing a function reference for `NameOf(...)` registration. These attributes are source-level contracts. AngelScript sees normalized declarations after preprocessing, while engine metadata and analyzers retain the higher-level FOnline-specific meaning. The complete authoring and runtime model for `[[ModuleInit]]`, callback-only attributes, transitive `[[Async]]`, `Yield`, server `Game.Sync` / `Game.Lock`, state ownership, and callback teardown is in [Script Lifecycle and Concurrency](../../how-to/scripting/lifecycle-and-concurrency.md). Keep this page focused on subsystem composition and use that guide for lifecycle-sensitive script design. ## 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: - server scripts work against authoritative entities owned by `ServerEngine` and managers; - client scripts work against view/client entities owned by `ClientEngine`; - mapper scripts work against mapper-owned editor state; - script handles must not be treated as persistence ownership. Use [Entity Model](../entity-and-property-model/) for entity/property/prototype ownership and [Persistence](../persistence/) 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: - server-side command processing validates client-originated remote calls before invoking server script handlers; - client-side runtime receives server-originated remote calls and dispatches client script handlers; - admin remote calls use the `CallAdminFunc()` path and require the `AdminRemoteCall` attribute. For an untrusted client-to-server call, author `MaxBytes` as the largest legitimate serialized payload and `MaxCollectionSize` as the largest legitimate declared collection. The server resolves the descriptor before body allocation, and native validation plus AngelScript decoding enforce the collection limit before reserve or construction, including nested dictionary arrays. The server-wide `ServerNetwork.MaxRemoteCallPayloadSize` remains a separate hostile-input ceiling. See [Remote Calls](../../reference/scripting/remote-calls.md) for the declaration, baked-metadata, and compatibility contract. 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. The complete authoring, caller-surface, namespace, security, baked-catalog, and compatibility contract is in [Remote Calls](../../reference/scripting/remote-calls.md). ## Native script method exports Native script APIs are grouped by file name: - `Common*ScriptMethods.cpp` — APIs shared by multiple sides, including global helpers and ImGui wrappers. - `Server*ScriptMethods.cpp` — authoritative server APIs for game creation, persistence, movement, entity mutation, and player/critter/map/item/location operations. - `Client*ScriptMethods.cpp` — client/view APIs for UI, resources, rendering-facing map operations, visible critters/items, audio/video, input, and local state. - `Mapper*ScriptMethods.cpp` — mapper/editor APIs for creating, moving, selecting, saving, and organizing map entities. 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 [Script Methods Map](../../reference/script-api/method-ownership.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 a covered entity must keep its own lock across a detach or reparent, use the cover-retaining, idempotent `EnsureEntitySynced(...)`; it retains existing caller cover — never releasing or parking on it — and cannot acquire an omitted dependency. Managed C# dispatch likewise validates the entity receiver's access before either method ABI or event ABI enters native code. Event subscription validates its target as well. This makes an uncovered server receiver fail at the script boundary, before a `noexcept` property accessor could terminate the process; it does not acquire cover for the caller. See [Managed C# server synchronization](../../how-to/scripting/managed-csharp.md#server-entity-synchronization). 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. AngelScript stores a `bool` value in one byte of a four-byte VM stack slot, whose upper bytes may retain earlier data. The patched native-call marshalling paths for x64 GCC, x64 MSVC, and ARM64 zero the destination argument slot and copy only the value type's in-memory bytes. Native callees may therefore rely on an incoming `bool` register being normalized to `0` or `1`; `AngelScriptNativeCallNormalizesBoolArgument` in `Source/Tests/Test_AngelScriptAlignment.cpp` pins this ABI boundary. `Gui::RegisterScreen` precaches each screen inside a try/catch, so one window that cannot be built no longer costs the registrations behind it: the failing screen keeps its creator, the rest register normally, and `Gui::VerifyScreensInitialized()` then raises a single `verify` naming every window that failed. `Gui::IsScreenRegistered` answers whether a creator is stored, and the `verify` in `CreateScreen` carries the screen's enum name as context. Note what an AngelScript `catch` does not give you: it binds no exception object, so `GetExceptionInfo()` reports the message of the exception the current catch block is handling, and that context is reset only when the script context is reprepared. Text lookup follows the same side ownership. Client/mapper scripts can retrieve strings and change language; server scripts expose only text presence and variant counts. The complete behavioral contract and missing-data semantics are in [Text and Localization](../../how-to/content/text-and-localization.md). 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. ## Backend parity and ownership The backend-neutral metadata and native export surface is shared, but language syntax and runtime mechanics are not interchangeable: | Contract | AngelScript | Managed C# | |---|---|---| | Enablement | `FO_ANGELSCRIPT_SCRIPTING` | `FO_MANAGED_SCRIPTING` | | Project source | project-owned `.fos` modules | project-owned `.cs` modules | | Compile artifact | baked AngelScript bytecode | target assemblies plus generated `.gen.cs`, `.gen.csproj`, and `.gen.sln` | | Initialization | `[[ModuleInit]] void` | `[ModuleInit]` static `void` or `Task` | | Suspension | transitive `[[Async]]` and `Game.Yield` | `Task`, `await`, and `Game.YieldAsync` on the backend synchronization context | | Synchronization proof | runtime cover operations and AngelScript attribute validation | `[RequiresCover]`, `[ProvidesCover]`, `[PreservesCover]`, `CoverReach`, runtime checks, and Roslyn `FOSYNC` diagnostics | | Runtime ownership | AngelScript engine, modules, contexts, and GC | one process-wide Mono runtime plus backend-scoped load contexts, scheduler queues, handles, and managed GC roots | | Native interop | registered AngelScript calls and VM contexts | one generated indexed ABI for scalar/value hot paths plus boxed fallback for complex values | Managed C# is not a renamed version of the removed experimental `Source/Scripting/Mono/` path. It is a complete backend with its own baker, generated bindings, load-context host, analyzers, runtime payload, and platform wiring. See [Managed C# Scripting](../../how-to/scripting/managed-csharp.md) for its full contract. Native scripting remains a placeholder. The Managed hot path is bound once from a shared `ManagedInteropAbi` manifest. Methods, events, settings, and inner-entity operations use dense ids and packed frames; entity-like arguments occupy nullable pointer slots, while plain fixed values are copied by layout. Callback adapters, wrapper factories, reflection lookups, and list factories are prepared at bind time. Complex strings and collections retain the boxed path. Event subscriptions are owned by the native entity, so an equal subscription is idempotent and can be removed through a different wrapper for the same entity. ## Core script ownership The Engine-owned script-side library now lives under `Source/Scripting/Managed/CoreScripts/` and contains the managed native bridge, attributes, initialization, invocation, remote-call, synchronization, async, verification, item-holder, enum, and value-type helpers required by the backend. It is infrastructure, not game policy. Its `[TemporaryCompat("Id", "YYYY-MM-DD")]` attribute marks managed old-build or old-data handling under the same [dated removal gate](../../reference/native/essentials.md#temporary-compatibility) as native `FO_TEMPORARY_COMPAT`. The former Engine-owned AngelScript high-level library was removed. An embedding project that uses AngelScript owns its `.fos` helpers, GUI implementation, module order, and gameplay modules. Do not copy project GUI or gameplay contracts back into reusable Engine documentation. ## Build and baking flow `BuildTools/cmake/stages/ScriptsAndBaking.cmake` wires script compilation into the project build: - `FO_ANGELSCRIPT_SCRIPTING` enables the `CompileAngelScript` command target. - The target runs the project AS compiler app (`${FO_DEV_NAME}_ASCompiler`) with the main config arguments. - `CompileAngelScript` depends on `ForceCodeGeneration`, so script-visible generated metadata is current before compilation. - `FO_MANAGED_SCRIPTING` creates the standalone `${FO_DEV_NAME}_ManagedScriptBaker`, `CompileManagedScripts`, `SetupManagedRuntime`, and `PrepareManagedRuntimePayload` integration. `CompileManagedScripts` depends on `ForceCodeGeneration` and compiles every configured resource-pack/target assembly from `ManagedScriptSourceDirs`, `ManagedScriptExtraSources`, references, and analyzers. - `BakeResources` and `ForceBakeResources` also depend on code generation and run the project baker app. 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 [Baking Pipeline](../content-pipeline/baking.md) for resource baking. ## Managed and native scripting roots `Source/Scripting/Managed/` is the implemented C# backend; its complete operational contract is in [Managed C# Scripting](../../how-to/scripting/managed-csharp.md). The obsolete `Source/Scripting/Mono/`, `FO_MONO_SCRIPTING`, `CompileMonoScripts`, and `BuildTools/compile-mono-scripts.py` interfaces no longer exist. `Source/Scripting/Native/` currently contains only `.keepalive`, marking the reserved source-root location for future native scripting integration. Do not document it as implemented until runtime, build, tests, and an authoring contract exist. ## Tests to inspect Script behavior is covered by focused tests: - `Source/Tests/Test_AngelScriptAttributes.cpp` — attribute parsing, nullable suffix handling, events, remote calls, and callback rules. - `Source/Tests/Test_AngelScriptBaker.cpp` — AngelScript bytecode/resource baking path. - `Source/Tests/Test_AngelScriptBytecode.cpp` — bytecode compilation/loading behavior. - `Source/Tests/Test_AngelScriptCall.cpp` — native/script call ABI and object-return lifetime. - `Source/Tests/Test_ManagedScriptBaker.cpp` — generated C# API and indexed ABI, packed frames/adapters, project/assembly construction, attributes, values, properties, remotes, and diagnostics. - `Source/Scripting/Managed/Tests/` — managed core-library bootstrap and generated-API fixtures. - `Source/Scripting/Managed/Analyzers/Tests/` — synchronization-cover analyzer diagnostics. - `Source/Tests/Test_ClientDataValidation.cpp` and `Test_NetBuffer.cpp` — inbound remote-call payload validation and framing. - `Source/Tests/Test_CommonScriptMethods.cpp` — common exported methods. - `Source/Tests/Test_EntityLifecycle.cpp` and `Test_EntitySync.cpp` — lifecycle and server synchronization boundaries. - `Source/Tests/Test_MetadataBaker.cpp` — baked event and remote-call metadata grammar. - `Source/Tests/Test_ServerScriptMethods.cpp` — server exported methods. - `Source/Tests/Test_ScriptBuiltins.cpp` — built-in script helpers/types. - `Source/Tests/Test_ScriptEntityOps.cpp` — script/entity interactions. Use these tests as executable documentation when changing script registration, generated wrappers, method signatures, nullability, event declarations, or remote-call dispatch. ## Change routing - Backend-neutral call ABI: `Source/Common/ScriptSystem.*`. - AngelScript compiler/runtime lifecycle: `Source/Scripting/AngelScript/AngelScriptScripting.*` and `AngelScriptBackend.*`. - Managed compiler/runtime/lifecycle: `Source/Tools/ManagedScriptBaker.*`, `Source/Scripting/Managed/`, and [Managed C# Scripting](../../how-to/scripting/managed-csharp.md). - Script module construction, source conventions, formatting, generated-file ownership, and refactoring gates: [AngelScript Style and Refactoring](../../how-to/scripting/style-and-refactoring.md). - Attribute syntax and nullable preprocessing: `Source/Scripting/AngelScript/AngelScriptAttributes.*` and [Nullability.md](../../../Nullability.md). - Script entity/property registration: `Source/Scripting/AngelScript/AngelScriptEntity.*` plus [Entity Model](../entity-and-property-model/). - Remote caller registration/dispatch support: `Source/Scripting/AngelScript/AngelScriptRemoteCalls.*`, [Remote Calls](../../reference/scripting/remote-calls.md), and [Networking](../authority-and-networking/). - Reflection helpers: `Source/Scripting/AngelScript/AngelScriptReflection.*`. - Native exported methods: `Source/Scripting/*ScriptMethods.cpp` and [Script Methods Map](../../reference/script-api/method-ownership.md). - Build target wiring: `BuildTools/cmake/stages/ScriptsAndBaking.cmake` and [BuildTools Pipeline](../../reference/cmake-and-buildtools/pipeline.md). - Generated metadata/codegen: [GeneratedApiAndMetadata.md](../../reference/metadata/index.md). ## Validation checklist 1. If signatures or annotations changed, regenerate code and inspect generated metadata/wrapper diffs. 2. Compile every enabled backend: `CompileAngelScript` for AngelScript and `CompileManagedScripts` for Managed C#. 3. Run the smallest affected script tests. For AngelScript start with `Test_AngelScriptAttributes`, `Test_AngelScriptBaker`, and `Test_AngelScriptCall`; for Managed C# start with `Test_ManagedScriptBaker`, managed core/analyzer tests, and affected `BuildTools/tests/test_managed_*.py`. Run shared method/entity tests for backend-neutral changes. 4. For nullable changes, run the nullability analyzers described in [Nullability.md](../../../Nullability.md). 5. For server/client/mapper method changes, validate the owning runtime path; do not rely only on compilation. 6. Update [Script Methods Map](../../reference/script-api/method-ownership.md) when exported method files are added, removed, or materially regrouped. ===== END DOCUMENT scripting-runtime ===== ===== BEGIN DOCUMENT managed-csharp-scripting ===== Source: Docs/en/how-to/scripting/managed-csharp.md Canonical URL: https://fonline.ru/Docs/en/how-to/scripting/managed-csharp.html Content SHA-256: b05abccc5edf06158da0b806f86d0c7beb04d0308e8ff5d4aba43d4da90e327f --- layout: default title: Managed C# Scripting locale: en document_id: managed-csharp-scripting permalink: /Docs/en/how-to/scripting/managed-csharp.html --- # Managed C# Scripting > Engine-owned documentation. This guide describes the reusable Managed C# backend, its authoring contract, generated API, lifecycle, synchronization, build, delivery, and validation. Game modules and project-specific policy belong to the embedding project. ## Contract status Managed C# is an implemented scripting backend for server, client, Mapper, bakers, and tests. It embeds Mono, compiles project scripts with the configured .NET SDK, and exposes the same backend-neutral Engine metadata used by AngelScript. Native scripting remains an unimplemented source-root placeholder and is not an equivalent third backend. An embedding project selects its scripting backend at configure time. Enable `FO_MANAGED_SCRIPTING` and disable `FO_ANGELSCRIPT_SCRIPTING` when the project is fully managed. Do not enable a backend merely because its source directory exists; the project configuration, resource packs, generated metadata, script sources, packages, and tests must agree. Use this guide beside: - [Scripting Runtime](../../explanation/scripting-runtime/) for the backend-neutral facade and the AngelScript comparison; - [Script Lifecycle and Concurrency](lifecycle-and-concurrency.md) for lifecycle rules shared by both implemented backends; - [Remote Calls](../../reference/scripting/remote-calls.md) for the wire contract; - [Generated API and Metadata](../../reference/metadata/) for the source-to-generated dependency graph; - [Packaging](../release/packaging.md) and [Client Updater](../../explanation/runtime/client-updater.md) for delivery. ## Ownership and source layout The Engine owns: - `Source/Scripting/Managed/ManagedRuntime.*`: process-wide Mono startup and native-thread attachment; - `ManagedScriptBackend.*`: one backend instance, assembly load context, marshalling, callbacks, events, remote calls, exceptions, and shutdown; - `ManagedPInvokeTable.*` plus `BuildTools/generate_pinvoke_table.py`: native interop shim registration; - `CoreScripts/*.cs`: the reusable `FOnline` namespace, attributes, synchronization helpers, async scheduler, invocation bridge, remote-call bridge, verification, and value-type helpers; - `ManagedHost/ManagedLoadContextHost.cs`: per-backend assembly isolation; - `Analyzers/FOnline.Analyzers.csproj`: compile-time entity-cover analysis; - `Source/Tools/ManagedScriptBaker.*`: generated project/API and assembly production. The embedding project owns game scripts, its namespace, project-only attributes and registrars, higher-level libraries such as a GUI object model, its resource-pack selection, target framework, analyzers, tests, and release qualification. The Engine no longer ships the former AngelScript high-level CoreScripts library; copying that library into Engine under C# would cross the same ownership boundary. ## Configure the backend The CMake switch is `FO_MANAGED_SCRIPTING`. It adds the managed runtime, backend, baker, CoreScripts tests, and managed application wiring. `FO_NATIVE_SCRIPTING`, `FO_ANGELSCRIPT_SCRIPTING`, and `FO_MANAGED_SCRIPTING` are independent build options, but a production project should deliberately select one gameplay backend unless it is testing cross-backend invocation. The immutable startup settings in the `ManagedScript` group define the generated project: | Setting | Contract | | --- | --- | | `ManagedScript.Assemblies` | Logical entry assemblies to build. | | `ManagedScript.ProjectName` | Base name for the generated solution and project. | | `ManagedScript.TargetFramework` | Target framework passed to the generated SDK-style project. | | `ManagedScript.MsBuild` | Command used to build the generated project. | | `ManagedScript.Dirs` | Source roots scanned for top-level `.cs` files; normally Engine CoreScripts plus project scripts. | | `ManagedScript.GeneratedDir` | Optional generated-project directory; empty selects the build tree's `GeneratedSource/Managed`. | | `ManagedScript.ExtraSources` | Additional `assembly,target,path` inputs. | | `ManagedScript.ExtraReferences` | Additional `assembly,target,reference` inputs. | | `ManagedScript.Analyzers` | Roslyn analyzer projects included in the generated build. | | `ManagedScript.AnalyzerPackages` | Roslyn analyzer NuGet packages as exact `name,version` pairs. | | `ManagedScript.AdditionalFiles` | Analyzer configuration files exposed through MSBuild `AdditionalFiles`. | | `ManagedScript.AnalysisLevel` / `AnalysisMode` | Optional SDK analysis-level and analysis-mode overrides. | | `ManagedScript.BakerDryRun` | Structural baker mode for tests; it does not prove executable assemblies. | | `ManagedScript.PatchPointWeaver` | Optional path to the Engine patch-point weaver project. When set, server/client script methods are woven during compilation; Mapper methods are not. | | `ManagedScript.ServerPatchesEnabled` / `ClientPatchesEnabled` | Per-side runtime permission to apply patches; both default to `false`. Each client reads its own switch. Weaving cost is independent of these switches. | | `ManagedScript.DeepTrackEntityWrappers` | Opt-in shutdown diagnostics that name still-live entity wrappers; ordinary live-wrapper counting is always enabled. | Add a resource pack whose `Bakers` list contains `Managed`. The pack inputs must include Engine `CoreScripts`, the project script roots, and the `///@` metadata sources needed by those scripts. The assembly, target, pack, and metadata selection are one contract: compiling a loose project that differs from the baker input is not Engine validation. ## Generated project and assemblies `ManagedScriptBaker` generates target API files, one SDK-style project, and a solution under the configured generated directory. Generated filenames carry `.gen`, an auto-generated banner, and their own nullable directive. Stale generated files not present in the new set are removed. Do not commit or hand-edit these outputs unless an embedding project explicitly treats a generated input as authored. The generated project enables nullable analysis, warnings as errors, Engine code style, configured analyzers, and the sources/references selected for each assembly and runtime target. API generation covers: - Engine settings, enums, value types, entities, prototypes, fixed types, and dynamic ref types; - scalar, array, list, dictionary, and supported dict-of-list property forms; - methods, overload identities, mutable arguments, callback delegates, and events; - remote-call sender facades and inbound handler registration; - native ref-type wrappers with explicit reference management where a borrow outlives the call. Unsupported type or member shapes fail baking with `ManagedScriptBakerException`; the baker must not emit a placeholder that fails only when gameplay reaches it. Compiled entry assemblies are target-specific, such as `.Server.dll`, `.Client.dll`, and `.Mapper.dll`. They are written under the baked pack's `Assemblies/Assemblies/` tree. Helpers and dependencies remain next to the entry assembly. When `ManagedScript.PatchPointWeaver` is set, the generated build runs the Engine-owned Mono.Cecil weaver on the intermediate server and client assemblies after compilation, before the later copy/package steps. The weaver and its sources participate in incremental bake/build inputs, so changing them recompiles and reweaves the scripts. An already woven assembly is left alone. The weaver is built as a tool, not shipped as a script dependency. ## Authoring shape Use the namespace owned by the embedding project and import `FOnline` for generated Engine types. Engine CoreScripts use file-scoped `namespace FOnline;`; a game should not put its own domain types there. Treat nullable reference types as part of the script/native contract. Generated reference and entity APIs distinguish nullable and non-nullable values. Narrow expected absence with an ordinary branch; use `Game.Verify` or `Game.VerifyNotNull` for violated invariants. A managed reference to an Engine entity does not own persistence or lifetime. Avoid mutable process-wide state. Each backend has an isolated load context, but gameplay state still belongs to a `Game`, entity, property, ref type, or another explicit per-engine owner. Static constants and immutable type metadata are fine; a mutable cache, registry, timer gate, or collection is not made correctly owned merely by being in a C# static field. Use fixed exception messages with dynamic values as context through the Engine verification helpers. Do not hardcode player-visible text in exceptions or logs and then surface it to the player; localization stays project-owned. ## Initialization and attributes `Initializator.InitializeEarly` runs before ordinary module initialization. It rejects every declared `async void` method, registers Engine attributed functions and remote calls, then runs project `[ScriptFuncRegistrar]` methods. A registrar must be static, parameterless, and return `void`; use it for project attributes that must be resolvable by bake-time reflection. `Initializator.Initialize` runs static constructors, discovers `[ModuleInit(priority)]` methods, orders them by ascending priority, and invokes them. A module initializer must be static, parameterless, and return `void` or `Task`. A `Task` result is awaited in the initializer's private synchronous continuation context; a null task is an error. Managed marker attributes mirror the Engine dispatch roles rather than ordinary direct calls: - `[Event]` for event handlers; - `[TimeEvent]` for time-event callbacks; - `[PropertyGetter]` and `[PropertySetter]`; - `[ServerRemoteCall]`, `[ClientRemoteCall]`, and `[AdminRemoteCall]`; - `[ItemTrigger]`, `[ItemInit]`, `[ItemStatic]`, `[CritterInit]`, `[MapInit]`, and `[LocationInit]`; - `[AnimCallback]`, `[ClassExtension]`, and project-registered marker attributes; - `[CallableByName]` for functions intentionally exposed to named invocation. Generated event wrappers reject a handler without `[Event]` before creating the native subscription. Named calls are likewise deny-by-default: `Game.Invoke` and administrative dispatch accept only explicitly marked functions and the applicable allowlist. ## Events, callbacks, timers, and named calls Generated event wrappers support handlers returning `void`, `Task`, `EventResult`, or `Task`. Native dispatch awaits result-bearing tasks before deciding whether the subscriber chain continues. Use `Task` when an awaited handler may consume or destroy an argument that later subscribers must not receive. Time-event APIs accept synchronous delegates and generated async delegates returning `Task`. The delegate identity is part of registration: stopping a time event with a separately constructed delegate does not identify the existing callback. Repetition or cancellation changes future scheduling and does not cancel a task that has already started. Entity-only post-set reactions may return `Task`; value-transforming setters with `ref` arguments and property getters remain synchronous because the native caller needs their result before the call returns. Inbound remote-call handlers may return `void`, `Task`, or `Task`. Remote calls have no wire result, so incomplete tasks are observed without blocking the network/client pump; a `Task` value is ignored. A named script function returning non-generic `Task` follows the same asynchronous boundary. A `Task` named function remains synchronous when native code requires `T`. Callback and remote-call arguments keep their metadata shape: native `any[]` arrives as `List` and `string=>any` as `Dictionary`. The bridge does not adapt these into `List` or string dictionaries; declare the exact generated types. ## Async and continuation scheduling `Game.YieldAsync(milliseconds)` is the managed equivalent of a script suspension. It completes from an Engine time event and resumes through the backend-owned `ScriptSynchronizationContext`. Do not use `async void`; return `Task` or `Task` so the Engine can observe completion and faults. Each backend owns a separate continuation queue. `BaseEngine::FrameAdvance` pumps only its backends after releasing the frame-property lock. Every resumed continuation re-enters the owning Engine through `RunScriptContext`; on the server this creates a fresh synchronization context. A newly posted continuation waits for a later frame, so a yielding loop cannot monopolize one frame. `Post` raises an atomic backend-ready flag while the scheduler is open; an idle frame does not enter managed code just to find an empty queue. A partly failed pump signals remaining work for the next frame. Shutdown closes the scheduler before unbinding the backend, so a late post cannot signal released native state. `Native.GetAndResetContinuationPumps` exposes pump counts to interop tests. Synchronous native-result callbacks and module initialization use a private continuation queue and drain only their own awaited continuations. `Game.YieldAsync` is rejected in that context because the blocked caller cannot advance the timer pump. Completed tasks remain valid. `ConfigureAwait(false)`, `Task.Run`, `Task.Factory`, `Parallel`, and manually dispatched ThreadPool work deliberately bypass the Engine synchronization context. They may perform isolated computation, but they must not call Engine APIs. The entry assembly's backend binding can still identify an Engine from such a thread, so this misuse is not guaranteed to fail at every native call; only synchronization-sensitive routes such as server entity access reliably reject it. Embedding projects should ban these escape APIs statically and return to the captured Engine context before touching Engine state. ## Server entity synchronization The server cover contract is backend-neutral: script callers must cover every existing entity the native call graph can read or mutate. An `await` ends the old cover; re-resolve or revalidate retained entities and reacquire cover before reuse. The native bridge validates a managed entity receiver before invoking an exported instance method or firing an event through either ABI; event subscription validates its target too. An uncovered receiver therefore reports a script-boundary cover fault instead of reaching a `noexcept` property accessor that would terminate the process. This validation does not acquire cover: callers still need to satisfy the declared requirements. Managed scripts declare and prove this contract with: - `[RequiresCover]` on a parameter or method receiver; - `[ProvidesCover]` on a parameter or return value that establishes cover; - `[PreservesCover]` on an awaitable helper that restores the caller's cover; - `CoverReach.Parent`, `Ancestors`, and `DestroyGraph` for transitive requirements; - the `Sync` CoreScript helpers as the only normal wrappers around raw `Game.Sync`, `SyncRelease`, `Lock`, and `Unlock`. The Roslyn analyzer reports invalid annotations (`FOSYNC001`), unsatisfied transitive cover (`FOSYNC002`), missing entry-point declarations (`FOSYNC003`), cover probing instead of acquisition (`FOSYNC004`), raw synchronization calls outside the helper (`FOSYNC005`), and cover use not re-proved after `await` (`FOSYNC009`). `FOSYNC006` and `FOSYNC007` are retired: use `using GameLock scope = GameLock.Acquire();`, whose `ref struct` scope releases on every path and cannot survive an `await`. Configure the analyzer through `ManagedScript.Analyzers` or `ManagedScript.AnalyzerPackages` and treat its warnings as build failures. Provider inference first proves that a candidate call executes on every returning path and only then traverses its callees. A call hidden in a conditional branch cannot establish cover; this order also prevents dense conditional call cycles from expanding exponentially. Analyzer self-tests time-bound that graph and still require `FOSYNC009` for an uncovered use after `await`. `Sync.Acquire` expands linked cover in place through `Game.SyncWiden`; it does not release and reacquire the already-held entities. This keeps the native cover continuous while following `[SyncWiden]` relationships and avoids a race window between the two sets. `FOSYNC010` rejects discarding a boolean acquisition answer, including a bare call or assignment to `_`: failure must influence control flow. `FOSYNC011` requires a `Sync` helper that changes held cover, directly or through another effectful helper, to declare its own `[CoverEffect]`. The analyzer treats these as build verdicts, not advisory warnings; the proposed redundancy diagnostics `FOSYNC012`–`FOSYNC014` were withdrawn. `FOSYNC015` rejects `[CoversOnlyArguments]` widening when its `[ProvidesCover]` arguments remain covered and no later `Sync.Snapshot` needs an own lock. Such suspension can break synchronous handlers; see [Sync-Cover Analysis](../../../SyncCoverAnalysis.md). For changed relations, `Sync.Yield()` hands off thread locks via `Game.SyncYield()`; re-read afterward. Unavailable entities defer to the next frame for teardown. `Sync.OnRetry` and `Sync.ReportRetry(reason)` report attempts, not failures. Attributes state a proof; they do not lock anything. Entry points annotate the entity the Engine already synchronized. Ordinary helpers either acquire the required cover or propagate `[RequiresCover]` to their callers. Custom dispatchers can declare their marker attribute with `EntryPointMarker`, so the analyzer treats their handlers as entry points. `FOSYNC009` requires a current cover after `await`: locking an earlier `map = cr.GetMap()` does not re-prove `cr`; use the current direct alias, a whole covered collection, or an explicit `PassesCover`/`RestoreCallerCover` relationship. The withdrawn redundancy rules `FOSYNC012`–`FOSYNC014` are not part of the active contract. Failed `Sync` acquisitions may be observed through `Sync.OnFailure`; subscribers receive immutable caller, reason, entity, and stack snapshots without changing the helper's `false` result. With no subscribers, the diagnostic snapshot is not allocated. ## Values, collections, properties, and lifetime The bridge converts supported primitives, enums, strings, `hstring`, value types, entities, ref types, lists, dictionaries, delegates, mutable arguments, and return values through Engine metadata. A registered value type is plain packed data: every field is a primitive, enum, `hstring`, or single-field value type; every offset is aligned to the field size; the total size has no tail padding; and a native twin is trivially copyable with the same size. Metadata registration rejects every other shape. Generated C# structs use sequential layout, and the backend checks the Mono value size before copying their bytes. Managed `FOnline.any` is a value type holding the Engine's textual `any_t`, not `object` or `string`. Conversion into it is implicit for supported primitives, strings, enums, and generated value structs; conversion out is explicit and rejects invalid or out-of-range data. Empty text reads as zero/false/empty text, numeric enum reads validate the underlying range, and lowercase float suffixes are accepted for numeric reads. `ToEnum()` accepts a qualified member, bare member, or numeric value. Equality compares the stored text; `IsEmpty` tests empty text. Use the explicit operators, not `System.Convert`/`IConvertible`. Collections use `List` and `Dictionary`, and generated `GetAsAny`/`SetAsAny` bridge properties. The managed ABI and baked assemblies must move together when this representation changes. Managed script property writes, including unboxed, fixed-list, and converting paths, use `Properties::SetValue`: validation/clamping happens first, unchanged stored bytes stop the write, and only a changed value runs setters and post-setters (persistence and client sync). `SetValueFromData` is for applying received network data, never a script assignment. Generated entity properties are native-backed. Dynamic ref types are managed DTOs whose values are materialized from or assigned to native property storage. A getter returns detached structured state; persist a mutation with read-modify-reassign unless the generated member itself is a live wrapper. Native ref types are explicit borrowed wrappers. If a project keeps one beyond the call/frame that returned it, follow the generated `__AddRef()`/`__Release()` contract. Factory-backed wrappers start with a reference that must be released after ownership is transferred or detached. `hstring` is an eight-byte blittable value containing the native intern-entry pointer. Frames and value types copy that pointer unchanged; only property and RPC storage uses the 64-bit hash and converts at the storage boundary. Values are interned through the Engine metadata bound to the entry assembly, resolve their text from that exact entry, and do not fall back to a process-wide hash table shared by engine instances. Static managed fields still initialize separately in every load context. Arrays of primitives, enums, `hstring`, and registered value types use `GetPropertyList` / `SetPropertyList` and cross as raw bytes. Longer reads retry directly into the final list storage while the same cover remains held. Strings, dictionaries, dynamic ref types, nullable proto/fixed-type values, and other structured forms keep the converting bridge, but generated access selects the property by registrar index rather than repeating owner and property names. Ordinary native/managed `List` arguments and results use a single raw-byte block for `byte`, `sbyte`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`, `float`, and `double`. Other element types retain the element-wise conversion path. The receiver rejects a byte count that is not a multiple of the element size; this optimization changes neither the list order nor the declared method signature. ### Indexed native interop ABI `ManagedScriptBaker` and the native backend share `ManagedInteropAbi`: one manifest of dense method, event, setting, and inner-entity ids plus a content hash. Generated `*Abi.gen.cs` bind stubs call `Native.BindAbi` during `Initializator.InitializeEarly`; a hash or count mismatch fails loading before script execution. Generated ABI files participate in the incremental bake stamp, so a generator-only change cannot publish new wrappers with an old assembly. The indexed path covers primitives, enums, `hstring`, registered value types, and by-value entity/proto/fixed/ref-type handles. Methods use `CallMethodIndexed`, eligible events use `FireEventIndexed`, numeric/bool settings use `GetSettingValue`, and inner entities are collected by one `FillInnerEntities` snapshot instead of `Count` plus repeated indexed lookups. Complex signatures use the corresponding boxed path with the same dense id. Nullability belongs to the manifest: a non-nullable handle slot rejects zero, nullable handles may carry zero, and dynamic ref types, by-ref handles, and abstract/base entity results remain boxed where their runtime type is required. Managed frames are compact packed buffers, but native code never dereferences an unaligned slot. `BuildManagedAbiNativeFrame` copies inputs and result slots into aligned stack storage, native dispatch works on that storage, and `CopyBackManagedAbiNativeFrame` returns only mutable arguments and the result. Event adapters return `EventResult` through a trailing `ref int`, copy by-ref arguments back after invocation, and avoid boxing the result. Native-to-managed callbacks whose signatures contain only fixed values and entity/ref-type handles use generated `CallbackAdapters.Adapt_` methods. One `ManagedCallbackPlan` resolves the adapter during registration; wrapper factories and native wrapper classes are also registered/cached during ABI binding, so dispatch does not repeat reflection or constructor lookup. Unsupported callback shapes retain the boxed `MonoArray`/`DynamicInvoke` path. Entity event subscriptions belong to the native entity rather than to one wrapper: an equal handler is idempotent, any wrapper of that entity can unsubscribe it, and destruction removes the subscriptions. Each generated entity-wrapper type belongs to one backend load context. Its native pointer is therefore sufficient for equality and hashing within that type; wrappers from different Engine instances are different runtime types. A live wrapper checks `Native.IsBackendAlive` before exposing its pointer. Once shutdown unbinds the entry assembly, later access throws `ObjectDisposedException`, and a late finalizer deliberately keeps its native reference instead of calling released Engine state. Backend-owned caches are built before hot-path use: managed helper methods, metadata-named classes, dynamic ref-type accessors, wrapper constructors, callback adapters, list factories, and per-event adapters. Typed custom settings keep a parsed cell behind `GlobalSettings::GetCustomSettingsGeneration()`; every custom-setting writer advances the generation, while a warmed read is a generation comparison plus a value copy. `ScriptSynchronizationContext` likewise allocates its continuation queue only on the first post. ## Runtime loading, isolation, and shutdown Mono is initialized once for the process. The first managed entry on a native Engine worker attaches that thread to the root domain and caches the attachment for the thread's lifetime. Later entries only switch the attachment GC-unsafe around managed execution and park it GC-safe while native work or locks run; reentrant entries inherit the attachment. The thread that initializes Mono is the exception because `mono_jit_init_version` attaches it implicitly and the initialization scope releases that adopted attachment. A backend then creates its own non-collectible `AssemblyLoadContext`; this is the per-engine isolation boundary because the embedded runtime does not provide usable classic AppDomain unload. Attaching once avoids repeated managed `Thread` allocation, but detaching a worker would not remove its native registration from Mono's preemptive stop-the-world. On Windows, the prepared Mono source retries a refused `SuspendThread` or `GetThreadContext` while the worker is alive. It may skip a thread only after that thread exits; after five seconds of refusal from a live thread it reports the thread id/name and Windows error to stderr and aborts instead of continuing with an unscanned stack and corrupting the managed heap. A later successful retry is also reported. The source-patch marker invalidates older Windows runtime caches; a prebuilt runtime must already include the patch. Before any code or type initializer runs from an entry assembly, the backend calls `Native.BindBackend` with its own pointer. Every internal call that needs Engine state passes that bound pointer explicitly, so static constructors, marshalling constructors, callbacks, and continuations identify the correct Engine without thread-local caller state. Binding identifies ownership only; it does not create a script synchronization context or server entity cover. At startup, baked assemblies are restored into content-hashed subdirectories under the writable `Cache/ManagedAssemblies/` root. Existing byte-identical files are reused, so concurrent in-process engine instances do not rewrite an assembly Mono already loaded. Missing managed assemblies are a supported empty-backend state for tests/tools that do not bake scripts; a configured gameplay project should treat that as a packaging or resource-selection failure. `DynamicAssemblies.Load(image, symbols)` loads a post-bake PE image into this backend's non-collectible load context. Its assembly name must be a fresh `FOnline.Dynamic.*` name; loaded code shares the backend's script types and statics and remains loaded for the process lifetime. `RunEntryAsync(MethodInfo)` accepts a static parameterless method returning a value, `Task`, or `Task` and runs it as its own script entry and server synchronization context; it rejects `async void` and unwraps invocation exceptions. `ScriptsVersionId` is the entry assembly MVID. On a server, `ReadClientScriptsImage()` returns the client entry image used by the updater (or local bake), allowing a fragment compiler to target the matching client version. Dynamic assemblies participate in script-static cleanup. The optional engine-owned `FOnline.ScriptCompiler` library compiles live fragments with Roslyn. Projects add its `.csproj` through `ManagedScript.ExtraReferences` only for targets that compile fragments; it and its dependencies are then packed alongside those entry assemblies. `DynamicScriptCompiler.CompileAsync` works off the Engine thread, uses a unique `FOnline.Dynamic.*` name, accepts a statement body or expression plus usings/symbols, and maps compile diagnostics to fragment line/column. It can compile against the running scripts or a supplied other-target image. Compilation has access to private/internal script members, so authorization to submit code is entirely an embedding-project responsibility. The compiler emits a portable PDB in `DynamicCompileResult.Symbols` alongside the image; load both streams to retain source locations in runtime frames. Roslyn requires cryptography even to bind strong-named references. On Linux the linked `System.Security.Cryptography.Native.OpenSsl` shim opens the system OpenSSL library at runtime, so fragment compilation requires that library on the host. The Engine excludes its static LibreSSL from the executable's dynamic symbol table to prevent the system library from binding to incompatible symbols. These assemblies are not hot-unloadable. ### Live script patches A fragment runs as a new entry; it cannot replace a method already called by scripts. With patch-point weaving enabled, `DynamicScriptCompiler` can instead compile a C# compilation unit with `Kind = DynamicCompileKind.Patch`. Its static methods marked `[ReplacesMethod(typeof(TargetType), "MethodName")]` replace eligible script methods. The replacement returns the same type and takes the same parameters and `ref`/`out` kinds; for an instance method, its first parameter is the target object (`ref` for a value type). Private replacements and private target members are supported. Source usings become global usings, the server/client compilation symbols and access to script members remain available, and diagnostics refer to `patch(line,column)`. The weaver gives a point to script methods with bodies outside Engine's `FOnline` namespace. It excludes constructors, generic methods or generic declaring types, varargs, compiler-generated bodies (including lambdas, local functions and state-machine `MoveNext`), and `[NoPatchPoint]` methods. Patch the method creating a lambda or async/iterator state machine instead; an invocation already running or suspended keeps its original body. Reserve `[NoPatchPoint]` for a measured hot method and patch its callers if needed. The compiler rejects a patch with no replacements (`FOPATCH001`) or an invalid, ambiguous, foreign, unweaved or signature-incompatible target (`FOPATCH002`) before application. A patch may define its own helpers and static state, but changing existing type layout, method signatures, or generated metadata requires a normal deploy. `ScriptPatches.Apply(assembly)` validates every replacement and publishes one table for the whole patch, so new calls see all of its replacements or none. A later patch of a method takes precedence; `Revert(set)` restores an earlier patch or the original body, and `RevertAll()` clears all sets. `IsAvailable`, `PatchPointCount`, `HasPatchPoint(method)` and `Applied` expose availability and state. Applying is refused when that side's `ServerPatchesEnabled` or `ClientPatchesEnabled` switch is off; Mapper has no patch application. Every published table remains allocated until process exit because concurrent callers can still hold it; loaded patch assemblies are non-collectible too. The woven fast path tests a static active flag, then consults a per-method slot only while patches are active. It remains present even if application is disabled. The Engine exposes the mechanism, not remote authorization, persistence, delivery, audit, or deployment policy; the embedding project must own those boundaries. The weaver writes a `FOnline.PatchPoints.Table` manifest into each woven assembly. The compiler uses that manifest, including when compiling against an image for the other side, to verify target eligibility. The replacement manifest records each method and its function pointer. The redirect is shared per erased signature; on JIT runtimes it calls native code, while the Web interpreter uses its method handle. A concurrent revert can leave a redirect with an empty slot; it falls back to the original method rather than calling a stale replacement. Desktop JIT cost and memory measurements depend on the embedding project and workload; Web and Android performance need separate measurement. Shutdown first calls `BeginManagedTeardown`, which invokes `Native.BeginBackendTeardown` before any other cleanup and makes `Native.IsBackendTearingDown` true while the backend is still bound. This distinguishes a wrapper finalized during ordinary runtime from one made unreachable by teardown itself. A thread-affine wrapper that cannot release its native resource from the finalizer thread may suppress its leak report in the latter case because the owning Engine subsystem is about to be destroyed. `Native.IsBackendAlive` cannot make that distinction: unbinding deliberately remains later so entity wrappers collected during shutdown can still return their native references. Shutdown then closes the continuation scheduler and discards queued work before releasing backend state. It clears project static references and persistent callback roots, runs bounded collect/finalizer passes while the Engine and assembly images still exist, reports remaining entity wrappers (and names them when deep tracking is enabled), then calls `Native.UnbindBackend` for every entry assembly before releasing the load scope and native global data. A non-browser finalizer wait runs on an Engine-requested pool task with a separate five-second budget, so a blocked finalizer cannot park the teardown thread indefinitely. A timeout or remaining-wrapper report is diagnostic and teardown continues; a wrapper that finalizes after unbinding must not release through dead native state. The single-threaded browser runtime has neither a usable managed thread pool nor a finalizer thread. Its shutdown therefore performs one inline collect/wait pass; `GC.WaitForPendingFinalizers()` returns immediately, finalizers run later as main-thread jobs, and the interim wrapper count is reported but not verified. Later posts cannot run against a disposed Engine. Managed exceptions are counted and logged through the common script exception path; deferred task faults are observed once. Managed frames and nested managed causes are spliced into the common native stack trace, while native exceptions crossing managed code keep identity through GC handles. ## Build and bake workflow The generated CMake target `CompileManagedScripts` runs the standalone `_ManagedScriptBaker`. It depends on `ForceCodeGeneration`, loads the project configuration, prepares metadata, generates the managed API/project including `*Abi.gen.cs`, and compiles target assemblies without a full resource bake. The generated API files are part of the assembly stamp. A `.csproj` in `ManagedScript.ExtraReferences` is built as a project reference and its package dependencies are copied for that target. Every packed helper assembly is claimed as a bake output, including on an up-to-date target; freshness checks use the pack output, not a temporary MSBuild output directory that the outdated sweep may remove. `BakeResources` and `ForceBakeResources` run the `Managed` baker as part of the selected resource pack. Use the compile target for a fast source/API check and the bake target for the real resource, assembly, runtime-payload, and metadata contract. After a force bake, run an ordinary incremental bake and require it to settle cleanly. The runtime toolchain is prepared by `SetupManagedRuntime`; `PrepareManagedRuntimePayload` produces the deployable subset and a `runtime.manifest`. Toolchain setup runs with an isolated environment so a workstation's `DOTNET_*`, NuGet, or SDK state does not silently redefine the published runtime. Runtime source builds disable the live NuGet advisory audit: the pinned source revision, not a later feed update, defines the reproducible dependency set. Before each runtime build, BuildTools removes dotnet's target-dependent repo-local tasks semaphore so switching from a desktop build to Android cannot reuse an incomplete task set. A configured workspace cache stores only a verified published runtime tree under a target/toolchain-specific key; local runtime source checkouts are never shared, incomplete cache hits are rebuilt, and a stale SDK bootstrap that lacks its matching shared runtime is removed before setup retries. ## Packaging and updating The prepared runtime contains only managed class libraries actually referenced by the target assemblies, including `System.Private.CoreLib.dll`; native runtime libraries, JIT binaries, headers, import libraries, and symbols are excluded from the resource payload. Mono and the generated native interop table remain linked into the application. Target-specific class libraries are resolved from that target's published runtime rather than copied from the host SDK. The Managed baker places the prepared runtime under `ManagedRuntime/` in the same resource pack as the game assemblies. Client packaging rebuilds that pack from the runtime payload belonging to the exact application target. Server packaging stages one target-specific copy for every distributed client target under `PlatformBinaries//`; the updater substitutes that copy for the common pack when serving that target. Several native variants may share this updater target while their independently built equivalent CoreLib payloads differ byte-for-byte, so packaging deterministically chooses the least-qualified matching binary entry, normally the default Release build, instead of requiring those payloads to be identical. Runtime startup restores the selected payload atomically to `/ManagedRuntime//`, adds its class-library directory to Mono's search path, and uses the cached payload as the source of truth. Unpackaged native development binaries also receive the prepared payload beside the executable; packaged native, Web, and Android applications use the resource-pack copy. The embedded payload defaults to invariant globalization because `System.Globalization.Native` is not shipped. A project that supplies and qualifies its own globalization native library may override the environment before runtime startup. ## Platforms and sanitizers Managed scripting is wired for Windows, Linux, Android, WebAssembly, macOS, and iOS build paths, but an Engine source-capable path is not a project release claim. Qualify every shipped target with the exact project resource pack, assemblies, runtime payload, startup, callbacks, async work, shutdown, packaging, and update route. Web uses the Mono interpreter plus Engine JavaScript scheduling/entropy glue; keep its interpreter thread attached until teardown. Because the interpreter compiles no native entry points, managed callbacks use `mono_runtime_invoke`; thunk and `UnmanagedCallersOnly` probe modes are skipped when `RuntimeFeature.IsDynamicCodeCompiled` is false. Script PDB resources are loaded there when present so managed stack traces retain source information. Android and Apple targets use target-specific runtime archives and class libraries. Never reuse one target's prepared payload for another target or architecture. MemorySanitizer and ThreadSanitizer configurations are rejected with `FO_MANAGED_SCRIPTING`: embedded Mono and generated/JIT code cannot satisfy those instruments and otherwise report false failures. AddressSanitizer and the supported undefined/data-flow combinations still require the project's actual managed build and runtime checks. ## Diagnostics and debugging No C++ exception may unwind through a Mono internal-call frame: doing so bypasses managed `catch`/`finally` and can leave a nested script entry active after an awaited continuation. `RegisterInternalCalls` therefore accepts only `noexcept` function pointers. Fallible calls capture the native failure in `CaptureNativeError`, return an error payload to `CoreScripts/Native.cs`, and throw `NativeCallException` only after control is back in managed code. The native error remains catchable at the C# call site, including after `await`; genuinely non-failing calls remain `noexcept` and terminate deterministically if that contract is broken. The managed backend reports fixed native context plus managed exception text and stack information through the common script error path. `ScriptExceptions.GlobalCount` is process-wide; a harness can call `ScriptExceptions.OpenScope()` and inspect the disposed scope's `Count` for faults recorded in its logical async flow, including continuations on another OS thread. Nested scopes also charge their parents. Deferred task faults count globally when observed, not as synchronous faults in a scope. A build that merely produces assemblies does not prove startup or callback dispatch. Set `ManagedScript.InteropProbeOnStart = True` for a client/device/browser qualification run that cannot host the native test suite; startup logs one `INTEROP-TRANSPORT` line per condition and a final summary. Both script backends retain at most 32 distinct overrun entry names per Engine, counting repeats while preserving independent maximum execution and lock-wait times. `TakeScriptOverruns()` drains that buffer. The client drains before `OnLoop` and dispatches `OnScriptOverrun(entry, execution, lockWait, count)` outside the buffer lock; server and mapper do not publish the event in their loops. An overrun caused by a subscriber waits for the next drain. The usual threshold/debugger suppression still applies. `InteropProbe` compares runtime invoke, classic thunk, and `UnmanagedCallersOnly` transports where the runtime supplies them, then measures production dispatch and its synchronization, attachment, and overrun-report components. Each series verifies delivery and arguments and reports GC handles, metadata lookups, managed objects, wrapper construction, and—under Tracy—native allocations per call. Counters are thread-local and disabled outside a measured stretch. Latency is evidence for a quiet-host comparison, not a shared-CI threshold; allocation and delivery counts are hard assertions. Both `UnmanagedCallersOnly` entries initialize together; the probe reports their cost after a benchmark. When `FO_TRACE_ENABLED` and the `Script` category are enabled, the backend installs a Mono profiler immediately after runtime initialization and before any entry assembly executes. Method-call instrumentation is restricted to registered game assembly images and methods with metadata tokens; runtime plumbing and generated wrappers stay out of the call tree. JIT zones are not image-filtered because a handler's first invocation pays for every method it reaches. The hook requests `ENTER | LEAVE | EXCEPTION_LEAVE`, deliberately not `TAIL_CALL`: Mono can eliminate a self tail call without a matching enter event, so treating that notification as an ordinary leave would close the caller's zone. An exceptional or inlined leave closes the per-thread zone stack down to the method Mono names. Method names and source locations are resolved once into a process-wide table shared by all managed backends and then read under a shared lock. Zone names omit parameter lists and commas because Tracy's CSV hotspot exporter does not quote that field. Mono profiler callbacks are `noexcept` C-ABI boundaries and must never unwind through JIT-generated code. See [Profiling](../quality/profiling.md#managed-script-zones) for how to read these zones beneath a `Script execution overrun` entry. Use the generated solution/project for IDE navigation and Roslyn diagnostics. Debug native startup and P/Invoke at the host process boundary; debug managed behavior with runtime logs and focused callbacks unless the embedding project provides a qualified managed debugger attachment workflow. The AngelScript UDP debugger does not debug C# and its settings should not be presented as a managed debugger. First diagnosis routes: | Symptom | Inspect first | | --- | --- | | Generated type or member is missing | Metadata input, target selection, and `ManagedScriptBaker` diagnostic. | | Build sees stale API | Generated directory selection and `CompileManagedScripts` dependency. | | Assembly builds but runtime loads none | Baked pack selection and `Assemblies/Assemblies/`. | | Works natively but not on Web/Android | Target-specific runtime payload and platform build, not the host SDK output. | | Continuation never resumes | Captured `ScriptSynchronizationContext`, frame pump, and forbidden ThreadPool escape. | | Native API fails after `await` | Entity liveness and reacquired synchronization cover. | | Callback cannot be registered | Required marker attribute and exact generated delegate signature. | | Package starts with missing framework type | `ManagedRuntime/runtime.manifest` and target-specific pack replacement. | | ABI bind fails before module initialization | Stale generated `*Abi.gen.cs`, native manifest/hash mismatch, or a skipped managed rebuild. | | A wrapper unsubscribe leaves the callback active | Subscription ownership on the native entity and delegate equality; do not keep wrapper-local event state. | ## Validation matrix | Change | Required evidence | | --- | --- | | Managed CoreScripts or backend | C# format/style checks, CoreScripts tests, generated project build, focused native unit tests. | | Dynamic assemblies or live compiler | `test_managed_dynamic_assemblies.py`, `test_managed_script_compiler.py`, target package/closure check, and an embedding-project runtime authorization and execution test. | | Patch-point weaving or live patches | `test_managed_patch_points.py`, managed baker/incremental inputs, exact target bake, and embedding-project application/revert/authorization tests on each enabled side; profile the target runtime separately. | | Synchronization failures | `FOnline.Sync.Tests.csproj`, analyzer tests, and the embedding-project subscriber/log behavior. | | Generated API shape or native export | Codegen, managed baker, generated diff, API contract diff, both backend tests where the contract is shared. | | Attribute, event, callback, timer, or named call | Managed reflection/registration test plus the owning native/runtime dispatch. | | Async scheduler | `test_managed_async_callbacks.py`, backend-isolation/frame-pump tests, and an embedding-project awaited gameplay path. | | Entity-cover contract | Roslyn analyzer tests, warning-free managed build, and the owning synchronized server behavior. | | Runtime/cache/thread attachment | Managed baker/backend native tests plus repeated multi-instance startup/shutdown. | | Indexed ABI, callback adapters, or wrapper caches | `Test_ManagedScriptBaker`, aligned-frame/native backend tests, `InteropProbe.VerifyTransports`, allocation counters, and the exact target runtime. | | Package or updater | Runtime-payload and packaging tests, exact target package inspection, startup from the packaged artifact, and update replacement. | | Platform claim | Configure/build, target payload, process/device/browser smoke, and project acceptance for that platform. | At minimum, run the focused Python managed suites under `BuildTools/tests/test_managed_*.py`, the analyzer test project, CoreScripts tests, `Test_ManagedScriptBaker.cpp`, the generated embedding-project unit-test target, `CompileManagedScripts`, and the affected bake/package/runtime path. A dry-run baker marker, a successful `dotnet build`, and a native process-start smoke prove different layers and must be reported separately. ## Migration from AngelScript A source port is complete only when declarations, generated API use, lifecycle, callbacks, named calls, synchronization, tests, resource inputs, packages, and runtime evidence have all moved. Deleting `.fos` files without removing the AngelScript baker or adding the Managed pack leaves a project with no active gameplay scripts. Preserve public metadata names, remote-call wire declarations, property layouts, persisted identifiers, and behavior unless the migration deliberately changes them. Backend-neutral metadata should not change merely because syntax changed. Run both sides during a controlled comparison only when the project has explicitly designed that lane; do not ship two handlers for the same event or remote call accidentally. Replace AngelScript `[[ModuleInit]]`, `[[Event]]`, `[[Async]]`/`Yield`, and dynamic synchronization comments with the managed `[ModuleInit]`, `[Event]`, `Task`/`YieldAsync`, and cover-attribute/analyzer contracts. Port Engine-provided helpers by semantics, not by transliterating syntax or retaining no-op compatibility shims. ## Project documentation boundary Every managed embedding project should document: - selected backend options, target framework, SDK/runtime pin, configured source roots, assemblies, analyzers, and generated directory; - project namespace and source layout, generated inputs, formatter/style gates, and how to open the generated project; - module catalog, authority boundaries, project attributes, higher-level libraries, and mutable-state owners; - exact compile, bake, test, package, launch, browser/device, and update commands; - migration status, intentionally unsupported shapes, platform qualification, and operational rollback. The Engine guide defines reusable behavior; a project's documentation must say how that behavior is configured and proven in that repository. ## Maintenance triggers Reconcile this page and its Russian mirror when any of these change: - `FO_MANAGED_SCRIPTING`, managed CMake targets, toolchain setup, or generated project structure; - `ManagedScript.*` settings or `ManagedScriptBaker` discovery/output; - CoreScript attributes, marshalling shapes, generated wrappers, events, remote calls, or named invocation; - continuation scheduling, thread attachment, load-context isolation, exception accounting, or shutdown; - synchronization-cover attributes or analyzer diagnostics; - runtime payload composition, cache restoration, packaging, updater substitution, or platform support. Also regenerate affected CMake, helper-CLI, package, API, public-contract, translation, site, search, and agent-delivery artifacts in their documented dependency order. ## Source paths inspected - `Source/Common/ScriptSystem.*` - `Source/Common/Settings.inc` - `Source/Scripting/Managed/` - `Source/Tools/ManagedScriptBaker.*` - `Source/Applications/ManagedScriptBakerApp.cpp` - `BuildTools/cmake/stages/Init.cmake` - `BuildTools/cmake/stages/Applications.cmake` - `BuildTools/cmake/stages/ScriptsAndBaking.cmake` - `BuildTools/cmake/stages/ThirdParty.cmake` - `BuildTools/cmake/stages/Packages.cmake` - `BuildTools/managed_runtime_payload.py` - `BuildTools/package.py` - `BuildTools/tests/test_managed_*.py` - `Source/Tests/Test_ManagedScriptBaker.cpp` ===== END DOCUMENT managed-csharp-scripting ===== ===== BEGIN DOCUMENT angelscript-style ===== Source: Docs/en/how-to/scripting/style-and-refactoring.md Canonical URL: https://fonline.ru/Docs/en/how-to/scripting/style-and-refactoring.html Content SHA-256: 6a874ba5b84269757a273178381088d73c4d5b26c3f5697acd58419a1944411d --- layout: default title: AngelScript Style and Refactoring locale: en document_id: angelscript-style permalink: /Docs/en/how-to/scripting/style-and-refactoring.html --- # AngelScript Style and Refactoring > Engine-owned documentation. This guide defines the reusable AngelScript source, formatting, module, and refactoring contract supported by the current FOnline compiler, formatter wrapper, public examples, and tests. An embedding game owns its domain vocabulary, module catalog, concrete formatter layout, generated project formats, gameplay architecture, and migration policy. ## Contract status This is a `current-revision` guide, not a promise that every historical FOnline project already follows these rules. Normative claims are derived from the current Engine source and tests. The public `Examples/*/Scripts/*.fos` modules supply maintained minimal examples, while Last Frontier and FOnline TLA are comparison evidence only. The former Engine `AngelScript/CoreScripts` library is project-owned after the Managed C# migration and is not a current source anchor. Use the [scripting runtime explanation](../../explanation/scripting-runtime/), [lifecycle and concurrency guide](lifecycle-and-concurrency.md), [nullability contract](../../contributing/coding-contracts/nullability.md), and [generated content workflow](../build/generated-content.md) for their deeper owning contracts. This page owns the route from an authored `.fos` change to a reviewable, behavior-preserving result. ## Scope and ownership Use this guide when adding, moving, formatting, or refactoring Engine or project AngelScript. It separates four concerns: 1. source layout and formatting that Engine tools can enforce; 2. module, side, attribute, mutable-state, and nullability rules enforced by compilation or validation; 3. generated or compatibility-sensitive names whose owner must be changed first; 4. project policy that must not be presented as a universal Engine rule. The Engine can define the compiler and formatter contract. It cannot choose a game's comment language, gameplay terminology, service decomposition, persistent schema, content keys, test scenes, or acceptance thresholds. ## Fast convention Before changing reusable or project script code: - give each authored file one primary namespace matching the file stem, such as `Time.fos` and `namespace Time`; - keep first-line `Sort N` source ordering separate from runtime `[[ModuleInit(priority)]]` ordering; - keep one clear responsibility in that namespace and call other modules explicitly as `Namespace::Function()`; - isolate target-specific declarations with `#if SERVER`, `#if CLIENT`, or `#if MAPPER`; for example, the server module has `SERVER=1`, while `#ifdef SERVER` is true on every side because all side macros are always defined as `0` or `1`; - keep authoritative mutation on its owning side and mutable state on an owning engine or entity object; - use the exact function and declaration attributes required by the dispatcher or generator; - enter dispatcher-owned attributed functions through the dispatcher route; - keep module globals const-only by default: `Script.MutableGlobalsAllowedNamespaces` is a narrow compatibility escape hatch for explicitly owned legacy namespaces, not permission for unowned mutable state; - write nullable handles as `T?`, narrow before use, and use a non-null type when absence is not part of the contract; - edit authored inputs and regenerate derived `.fos` files instead of patching generated output; - classify the change as mechanical, structural, behavioral, or contract work; use small, narrow batches and add migration/compatibility proof for contract work; - run `python BuildTools/buildtools.py format-source`, compile every affected side without warnings, and run the narrowest test that observes the behavior; - keep the module catalog, comment language, game vocabulary and architecture, project-generated formats, persistence migrations, fixtures, and gameplay acceptance policy in the embedding project. The Engine does not own a game's module catalog merely because it supplies the compiler or formatter. A complete Engine/project boundary summary must state both rules that are easy to lose in compression: dispatcher-owned attributed functions are entered only through their dispatcher route, and generated project formats, persistence migrations, plus gameplay acceptance remain project policy. The namespace-to-file rule is an Engine convention and retrieval aid, not AngelScript grammar. A generated or compatibility-owned exception should be pinned in its generator or validator rather than weakening the default. ## How scripts become a module ### File discovery and ordering The backend receives the configured script files, reads each file's first line, and looks for `Sort N`. Missing directives use sort value `0`. It then performs a stable ascending sort by numeric value and, for equal values, by filename stem. The generated root source includes every resulting file. This ordering can affect preprocessing and declaration visibility. Keep `Sort N` on the first line when it is needed, treat an existing value as behavior-bearing, and do not use it as a substitute for explicit lifecycle ordering. `[[ModuleInit(priority)]]` owns runtime module-initializer order; the [lifecycle guide](lifecycle-and-concurrency.md) owns that contract. Projects normally configure script inputs through their Engine integration. Authored modules do not need a hand-maintained include graph. A manual include or sort change is therefore a structural change until every affected side compiles and the relevant startup path passes. ### Side-specific compilation FOnline preprocesses and compiles a separate module for each requested side. All three side macros exist in each compile and have values `0` or `1`: - server: `SERVER=1`, `CLIENT=0`, `MAPPER=0`; - client: `SERVER=0`, `CLIENT=1`, `MAPPER=0`; - mapper: `SERVER=0`, `CLIENT=0`, `MAPPER=1`. Use value tests such as `#if SERVER`. `#ifdef SERVER` is true on every side and does not isolate server code. A balanced preprocessor guard proves only lexical structure; compiling each affected side proves that its declarations, attributes, and calls are valid. ### Namespace and file ownership The maintained public example scripts use one top-level namespace matching each `.fos` stem: ```angelscript // Time.fos namespace Time { timespan Seconds(int value) { return timespan(value, SecondsPlace); } } ``` This gives contributors, diagnostics, search, and retrieval systems the same route from `Time::Seconds` to `Time.fos`. A file may contain target guards or private implementation helpers, but its primary public ownership should remain obvious. ### Dependency and compatibility boundaries Cross-namespace calls should name the owner. Move a helper only when its behavior and callers belong to the receiving module, not merely to satisfy a size or ordering preference. Treat the following as contracts until the owning source and tests prove otherwise: - first-line sort directives and side guards; - function attributes and dispatcher signatures; - `///@` declaration metadata; - remote-call subsystem and method names; - reflection strings, invoke names, property names, enum values, serialized identifiers, and content keys; - generated filenames and generator inputs. A local rename is mechanical only when none of these surfaces can observe it. ## Formatter contract ### Supported command and version The Engine does not own one universal `.clang-format` layout for project AngelScript after the high-level CoreScripts move. A project must pin its layout and clang-format version. The reusable `BuildTools/buildtools.py format-source` wrapper requires clang-format 20, preserves the file's BOM/line-ending convention, and repairs FOnline-specific nullable suffix, cast, array, and named-argument spacing without changing literals or comments. From the Engine repository run: ```bash python BuildTools/buildtools.py format-source git diff --exit-code ``` `format-source` formats supported files under Engine `Source`; it does not discover an embedding project's separate script tree. A project must use its documented formatter for project-owned `.fos` files and run the Engine command separately when the submodule source changed. BuildTools uses `FO_CLANG_FORMAT` when set, otherwise searches for `clang-format-20` and then `clang-format`, and rejects a binary whose major version is not 20. Engine CI reruns the wrapper and requires an empty diff. ### What the wrapper repairs Raw clang-format parses `.fos` as C++ and can separate AngelScript-specific tokens. The wrapper masks strings, character literals, line comments, and block comments, then repairs at least these source forms after formatting: - nullable declarations such as `Critter? target`; - nullable parameters, return types, casts, and template arguments; - nullable array elements such as `Item?[]`; - named arguments such as `Create(count: 2)`. Use the wrapper rather than raw clang-format. A project formatter may cover additional authored formats, but its `.fos` path must preserve these semantics or delegate to equivalent Engine-backed logic. ### Encoding, line endings, and EOF The wrapper reads and writes UTF-8, removes a UTF-8 BOM, preserves whether an existing file uses LF or CRLF, and leaves exactly one line terminator at EOF. Line-ending normalization alone does not count as a semantic formatting difference. Do not impose a project-wide line-ending rule through this guide. Repository attributes and the project formatter own that policy. The Engine guarantee is preservation of the input convention for a formatted file. ### What formatting does not prove Formatting does not prove namespace ownership, side authority, balanced behavior across roles, attribute use, callback routing, mutable-global policy, generated ownership, nullability flow, serialization compatibility, or runtime behavior. Review formatter output before treating the mechanical phase as complete. ## Source layout ### Module ordering inside a namespace Keep related declarations together and follow the surrounding module's order. Put public ownership ahead of cosmetic uniformity: do not reorder initialization, registration, callbacks, or declaration metadata without checking whether the consumer observes source order. Avoid a universal gameplay-helper order in Engine documentation. The public examples are minimal compiler/integration fixtures, while a game owns its high-level libraries and may group domain declarations differently and pin that structure in project checks. ### Names and comments The reusable baseline is intentionally small: - namespace and type names use `PascalCase`; - public function names follow the surrounding Engine script API; - local names expose intent and follow the surrounding module; - compatibility names change only through their owning API or migration process; - comments explain intent, invariants, ownership, or a non-obvious constraint instead of restating the next statement. The Engine does not mandate a natural language for game comments, a file-header template, one vocabulary for NPC or item variables, or a universal maximum module size. ### Mutable state and globals After module build, the backend rejects every mutable module-level global whose namespace does not match a configured prefix in `Script.MutableGlobalsAllowedNamespaces`. Const globals are accepted. The default empty list therefore enforces const-only module globals. Prefer state owned by the engine instance, entity, or an explicit lifecycle object. If a legacy project temporarily needs mutable globals, allow only the narrowest namespace prefix, record the owner and removal condition, and test startup on every side. Prefix matching means a broad entry can admit more namespaces than its author intended. An allowlist is a compatibility escape hatch, not evidence that a global cache or service is correctly scoped. Module initialization and the global freeze boundary remain owned by the [lifecycle guide](lifecycle-and-concurrency.md). ## Nullability and invariants Use `T?` only when absence is part of the contract. Bind or guard the nullable value before dereferencing it. Use a normal branch for expected absence and `verify(...)` for a violated invariant; keep the message a fixed description and pass dynamic values as context arguments. ```angelscript Critter? target = Game.GetCritter(targetId); if (target == null) { return; } ApplyEffect(target); ``` Do not scatter defensive checks around non-null values or freshly supplied entity arguments. Revalidate retained entity handles after an actual lifetime boundary such as `Yield`, a callback that can destroy or detach the entity, or storage beyond the current call. The [nullability contract](../../contributing/coding-contracts/nullability.md) owns narrowing details; the [lifecycle guide](lifecycle-and-concurrency.md) owns suspension and synchronization. ## Attributes and callback ownership Attributes are compiler and dispatcher contracts, not decorative labels. The pipeline preprocesses source, extracts and binds attributes, validates their use and special forms, validates callback and remote-call contracts, and only then emits bytecode. ### Direct-call blockers The built-in direct-call-blocking set is `Event`, `TimeEvent`, `AnimCallback`, `PropertyGetter`, `PropertySetter`, `ServerRemoteCall`, `ClientRemoteCall`, `AdminRemoteCall`, `ItemTrigger`, `ItemStatic`, `ModuleInit`, and `InvokeEntry`. A script function bearing one of these attributes must be entered through its owning dispatcher or API, not called as an ordinary helper. Projects may add dispatcher-owned attributes through `Script.ExtraDirectCallBlockingAttributes`. `Script.AttributedFunctionDirectCallAllowedNamespaces` can exempt caller namespaces by prefix for compatibility. Keep either list narrow and transitional; extracting a normal helper is preferable when behavior genuinely needs both direct and dispatched entry. The validator also checks callback API ownership, including event subscription, time-event APIs, animation callbacks, and property getter/setter registration. An attribute with the right spelling but the wrong route still fails the contract. ### Marker propagation Any function attribute not classified as direct-call-blocking is treated as a marker by the call validator. If a function calls a marked function, the caller must carry the same marker. `[[Async]]` is the common example: propagation makes the transitive suspension boundary visible instead of hiding it inside a helper. Do not remove or add a marker as formatting cleanup. Compile all callers and follow the owning lifecycle or attribute documentation before changing the call graph. ### Generated declarations Declaration comments such as `///@ Event`, `///@ RemoteCall`, `///@ Property`, and `///@ Enum` feed generated metadata and script declarations. Change the authored declaration, regenerate in dependency order, inspect generated diffs, and then compile the affected sides. Function attributes and `///@` declarations solve different parts of the pipeline. Do not replace one with the other because their names appear related. ## Generated script ownership Every generated `.fos` file needs an upstream owner: Engine metadata/codegen, a project GUI generator, a content generator, or another declared tool. Fix that owner and regenerate. A hand edit to derived output is not a completed change because the next generation pass will erase it. Before editing an unfamiliar file, check repository instructions, generated headers, build tasks, and the [generated content workflow](../build/generated-content.md). When a generated failure is visible only in derived code, retain enough source-located evidence to repair the input or generator. ## Refactoring classification Classify the change before editing. The highest-risk touched surface determines the proof required for the batch. ### Mechanical changes Examples: formatter output, comment correction, and a local rename with no reflected or serialized use. Minimum proof: run the owning formatter, inspect the diff, and compile the affected scripts without warnings. ### Structural changes Examples: helper extraction, function move, namespace move, file split, side-guard regrouping, or sort change. Minimum proof: compile every affected side and run focused tests for module initialization, callbacks, and the moved call paths. Verify that generated and reflected ownership did not change accidentally. ### Behavioral changes Examples: condition, ordering, state mutation, callback result, authority decision, or lifetime behavior. Minimum proof: focused runtime or gameplay tests plus the relevant integration path. Describe the behavior change separately from cleanup. ### Contract changes Examples: attribute, metadata name or type, remote call, property, enum, persisted identifier, content key, or generated schema. Minimum proof: regeneration, compatibility classification, migration or disposition when required, compile and bake gates, and runtime tests on both ends of the contract. ## Safe batch workflow ### Establish ownership Read the nearest repository instructions and owning docs. Identify whether each touched file is authored or generated, which side owns the behavior, and whether names cross process, persistence, reflection, or content boundaries. ### Capture the baseline Run the narrowest existing compile or test before a broad change. Record known failures rather than silently treating them as refactor output. For a migration, inventory temporary allowlists, generated files, and compatibility names before changing them. ### Change one class Keep mechanical cleanup separate from structural, behavioral, and contract changes when practical. Small batches make a failed compile, changed callback order, missing side symbol, or compatibility drift attributable to one decision. Do not bulk-delete commented code or rename strings, reflection tokens, metadata identifiers, serialized fields, or content keys without determining ownership first. Version control preserves old text; it does not prove that disabled code is obsolete or that a name is non-contractual. ### Regenerate and format Run every owner generator whose input changed, then the correct formatter for each authored tree. Never format a generated file as a substitute for fixing its generator unless the generator contract explicitly includes that formatting pass. ### Compile each side Compile SERVER, CLIENT, and MAPPER wherever the changed file or metadata can reach them. Treat warnings as failures. A common-only edit is not proven by compiling one convenient role. ### Prove behavior Run the narrowest Engine unit, project script test, scene, or integration route that observes the changed behavior. When a cleanup reveals a probable bug, either prove and fix it as a separately described behavioral change or record a precise follow-up. ## Validation matrix | Change | Required evidence | | --- | --- | | Engine CoreScript source | Engine `format-source`, clean diff, affected script compiles, focused Engine test | | Project-authored `.fos` | Project formatter, `CompileAngelScript` or equivalent for all roles, focused project test | | Side guard or sort directive | Compilation of every affected side, startup or initialization test where order matters | | Mutable global policy | Startup compile/bake with exact namespace settings, lifecycle test, documented removal condition for any exception | | Function attribute or callback route | Attribute validation, owning dispatcher path, direct-call or propagation regression where relevant | | `///@` metadata or generated declaration | Regeneration, generated diff, compile/bake, compatibility review | | Persisted, reflected, remote, or content identifier | Contract disposition or migration plus producer/consumer runtime tests | | Broad refactor | Repeat the applicable gates for each small batch; finish with the project's aggregate validation | Engine CI runs `buildtools.py format-source` and `git diff --exit-code`. `BuildTools/tests/test_docs_angelscript_style.py` pins the documentation route, compiler ordering and side macros, mutable-global and attribute settings, public-example namespace/guard/encoding rules, formatter repairs, external evidence, localization, and workflow inclusion. ## Failure diagnosis | Symptom | First boundary to inspect | | --- | --- | | Server declaration appears on client or mapper | Replace `#ifdef SIDE` with `#if SIDE`; inspect guard nesting | | Symbol appears or disappears after file move | First-line `Sort N`, equal-sort filename ordering, namespace qualification | | Mutable global rejected after module build | Owning state object and the exact `MutableGlobalsAllowedNamespaces` prefix; do not broaden blindly | | Direct call to attributed function rejected | Enter through the owning dispatcher or extract a normal helper | | Caller is missing `[[Async]]` or another marker | Propagate the marker through the real call chain and review the lifecycle boundary | | Formatter separates `?` or named-argument `:` | Use the Engine-aware wrapper and confirm clang-format major 20 | | Generated `.fos` change disappears | Edit the generator input or generator, then regenerate | | One role compiles while another fails | Compile the failing side with its actual `0`/`1` macros and generated declarations | | Refactor changes startup behavior | Separate file sort, module-init priority, callback registration, and runtime event order | ## Project policy boundary An embedding game must document the parts the Engine cannot choose: - module and domain catalog, gameplay architecture, and authority decisions; - comment language, terminology, headers, and local naming additions; - generated script names, generator commands, and project-only authored formats; - serialized identifiers, migration approvals, and compatibility windows; - test harness, fixtures, launch profiles, and gameplay acceptance; - formatter coverage outside Engine `Source`; - temporary namespace exceptions and their removal plan; - file-size, commented-code, or other quality-ratchet thresholds. Last Frontier's narrow test-only global exceptions and project formatter are useful current evidence. TLA's broad production mutable-global allowlist and ongoing refactoring log are migration evidence, not an Engine recommendation. Neither project is a normative dependency of this guide. ## Maintenance triggers Re-audit this page in the same change when any of these owners changes: - script discovery, first-line sorting, side macros, module construction, or bytecode pipeline in `AngelScriptBackend.cpp`; - direct-call blockers, marker propagation, callback validation, or special attributes in `AngelScriptAttributes.*`; - `Script.MutableGlobalsAllowedNamespaces`, `Script.AttributedFunctionDirectCallAllowedNamespaces`, or `Script.ExtraDirectCallBlockingAttributes`; - `.fos` patterns, clang-format discovery/version checks, repair logic, encoding, line-ending, or EOF behavior in `BuildTools/buildtools.py`; - the reusable formatter wrapper, project-owned `.clang-format`, or maintained public-example layout; - generated declaration ownership or the lifecycle/nullability contracts linked from this guide; - external project evidence used to separate reusable rules from project policy. Run the focused documentation test, localization check, snippet check, site generation check, and standalone documentation validator. If Engine `.fos` behavior changed, also run the formatter, compile every affected side, and execute the owning native/script tests. ## Source paths inspected - `BuildTools/buildtools.py` - `.github/workflows/validate.yml` - `Source/Common/Settings.inc` - `Source/Common/ScriptSystem.cpp` - `Examples/*/Scripts/*.fos` - `Source/Scripting/AngelScript/AngelScriptBackend.cpp` - `Source/Scripting/AngelScript/AngelScriptAttributes.cpp` - `Source/Scripting/AngelScript/AngelScriptAttributes.h` - `Source/Tests/Test_AngelScriptAttributes.cpp` - `Source/Tests/Test_AngelScriptBaker.cpp` - `BuildTools/tests/test_docs_angelscript_style.py` ===== END DOCUMENT angelscript-style ===== ===== BEGIN DOCUMENT script-lifecycle-concurrency ===== Source: Docs/en/how-to/scripting/lifecycle-and-concurrency.md Canonical URL: https://fonline.ru/Docs/en/how-to/scripting/lifecycle-and-concurrency.html Content SHA-256: 03389c356c8ff546c644ed1869b4addf6d1dedd58c23baf8b744ebf7d6f78aee --- layout: default title: Script Lifecycle and Concurrency locale: en document_id: script-lifecycle-concurrency permalink: /Docs/en/how-to/scripting/lifecycle-and-concurrency.html --- # Script Lifecycle And Concurrency > Engine-owned documentation. This guide describes reusable lifecycle and concurrency behavior shared by AngelScript and Managed C#, then names the language-specific rules explicitly. Project modules, gameplay policies, and project-specific synchronization helpers belong to the embedding game. ## Purpose Use this guide when a script needs to initialize a module, subscribe a callback, wait with `Yield`, mutate server entities, own runtime state, or shut down cleanly. It answers four questions that should be explicit before writing the code: 1. Who invokes this function, and during which runtime phase? 2. Can this call chain suspend or resume on another server worker? 3. Which object owns the mutable state and its lifetime? 4. Which entity synchronization cover is valid at this exact point? Keep examples inside the same contract. When a documented lookup can return no entity, store it as `T?` and narrow it before use. Call `Yield` only from a transitively `[[Async]]` chain, then re-resolve, narrow, and reacquire cover after resumption. If the exact lookup or callback signature is not present in the owning reference, explain the lifecycle without inventing shorthand code. Native entry cover is dispatcher-specific: the inbound remote-call rule below does not prove that another event, setter, callback, or direct script entry starts empty or carries the same cover. Inspect its owning native dispatcher before stating an initial cover. Use this decision for every server entry point that accesses an entity: `ServerEngine::RunScriptContext()` creates the nested `SyncContext` that can hold an entity cover; it does not itself cover any entity. The owning native dispatcher may establish an initial entity cover inside that context. 1. Rely on an initial synchronization cover only when that entry point's owning native dispatcher proves the exact covered set. 2. If the required entity is outside that proven set, or the initial set is not documented, call `Game.Sync(...)` with the complete required set before reading or mutating it. Do not fill the evidence gap by saying an unrelated event, setter, callback, or direct entry starts empty. 3. After `Yield`, reacquire the complete cover before entity access; a cover from the previous execution does not survive suspension. Do not paraphrase this as "events, setters, or callbacks create no additional sync scope." That is the same unsupported cross-dispatcher generalization in a different form. State only the proven dispatcher cover and the explicit `Game.Sync(...)` cover used by the script. Read it together with: - [Scripting](../../explanation/scripting-runtime/) for the complete scripting subsystem and native binding path. - [Managed C# Scripting](managed-csharp.md) for managed configuration, generated assemblies, attributes, async scheduling, analyzers, runtime loading, packaging, and platform support. - [Entity Model](../../explanation/entity-and-property-model/) for entity, property, holder, and destruction ownership. - [Server Runtime](../../explanation/runtime/server.md) and [Client Runtime](../../explanation/runtime/client.md) for side-specific loops and managers. - [Remote Calls](../../reference/scripting/remote-calls.md) for network entry points and authority boundaries. - [Nullability.md](../../../Nullability.md) for handles that can disappear before a continuation resumes. - [generated API reference](../../../generated/api/index.md) for current method signatures, attributes, settings, and source links. ## Source paths inspected - `Source/Common/ScriptSystem.h` - `Source/Common/ScriptSystem.cpp` - `Source/Common/EntityProperties.h` - `Source/Common/Entity.h` - `Source/Common/Entity.cpp` - `Source/Client/Client.cpp` - `Source/Server/EntityManager.cpp` - `Source/Server/Server.cpp` - `Source/Server/WorkerPool.cpp` - `Source/Tools/Baker.cpp` - `Source/Server/EntitySync.h` - `Source/Server/EntitySync.cpp` - `Source/Scripting/ServerGlobalScriptMethods.cpp` - `Source/Scripting/ServerCritterScriptMethods.cpp` - `Source/Scripting/ServerItemScriptMethods.cpp` - `Source/Scripting/ServerLocationScriptMethods.cpp` - `Source/Scripting/ServerMapScriptMethods.cpp` - `Source/Scripting/AngelScript/AngelScriptAttributes.cpp` - `Source/Scripting/AngelScript/AngelScriptBackend.cpp` - `Source/Scripting/AngelScript/AngelScriptCall.cpp` - `Source/Scripting/AngelScript/AngelScriptContext.cpp` - `Source/Scripting/AngelScript/AngelScriptEntity.cpp` - `Source/Scripting/AngelScript/AngelScriptGlobals.cpp` - `Source/Scripting/AngelScript/AngelScriptRemoteCalls.cpp` - `Source/Scripting/Managed/CoreScripts/Initializator.cs` - `Source/Scripting/Managed/CoreScripts/ScriptSynchronizationContext.cs` - `Source/Scripting/Managed/CoreScripts/Sync.cs` - `Source/Scripting/Managed/ManagedScriptBackend.cpp` - `Source/Scripting/Managed/ManagedRuntime.cpp` - `ThirdParty/AngelScript/sdk/angelscript/source/as_compiler.cpp` - `ThirdParty/AngelScript/sdk/angelscript/source/as_scriptengine.cpp` - `Source/Tests/Test_AngelScriptCall.cpp` - `Source/Tests/Test_AngelScriptAttributes.cpp` - `Source/Tests/Test_AngelScriptBaker.cpp` - `Source/Tests/Test_ManagedScriptBaker.cpp` - `Source/Tests/Test_EntityLifecycle.cpp` - `Source/Tests/Test_EntitySync.cpp` - `Source/Tests/Test_ServerMapOperations.cpp` - `Source/Tests/Test_ScriptEntityOps.cpp` - `Source/Tests/Test_ScriptBuiltins.cpp` ## Runtime mental model Script execution is a sequence of bounded entries, not one serialized game-wide thread: | Phase | Owner | Important boundary | |---|---|---| | Compile and bake | AngelScript compiler and Managed C# baker/Roslyn | Attributes, callback usage, nullable values, generated bindings, and synchronization contracts are validated before runtime. | | Module initialization | `ScriptSystem::InitModules()` | Init functions run in ascending priority while global assignment is temporarily enabled. | | Entity initialization | `EntityManager::CallInit()` | The entity is marked initialized, its `On*Init` event fires, then its optional persisted `InitScript` callback runs. | | Callback dispatch | Client loop or server worker job | Events, time events, remote calls, and native re-entry invoke attributed functions through their owning API. | | Suspension | AngelScript context manager or managed synchronization context | `Yield` or `await Game.YieldAsync(...)` preserves a continuation, but the current native execution scope returns. | | Resumption | Client scheduled-callback pass or server worker pool | The continuation runs later; on the server it may run on another worker under a fresh synchronization context. | | Entity destruction | Entity/manager owner | Event callbacks and time-event storage are cleared; server dispatch jobs are cancelled by the owning manager. | | Runtime shutdown | Client/server and scripting backend | Global events/time events, entities, script globals, contexts, and the backend are drained in owner-defined order. | This model has two practical consequences: - a live script handle is not proof that its entity is still valid or synchronized; - code after any suspension point is a new observation of mutable world state, not a continuation of an atomic transaction. ## Module initialization Declare a global, no-argument, `void` function with `[[ModuleInit]]`. The attribute accepts an optional signed integer priority: ```angelscript [[ModuleInit(100)]] void InitializeInventoryRules() { Game.OnSomeEvent.Subscribe(OnSomeEvent); } ``` The event name above is illustrative; use a generated event from the current API reference. The runtime indexes valid init functions, stores their priorities, and uses a stable ascending sort. `[[ModuleInit]]` without an argument has priority `0`. Lower values run first. Equal priorities retain registration order, but cross-module dependencies should still use explicit priorities rather than relying on file or bytecode discovery order. `ScriptSystem::InitModules()` performs this sequence: 1. unfreeze script globals; 2. invoke every registered init function; 3. abort startup if any invocation fails; 4. freeze globals after all functions complete. `Game.SetConstGlobalVar(...)` is therefore an initialization-only operation. It throws after the freeze boundary. The baker also rejects mutable module-level globals unless their namespace is explicitly allowed by `Script.MutableGlobalsAllowedNamespaces`. Prefer module init for registration and immutable lookup construction. Do not start gameplay work that assumes the world, current player, map, or network session already exists. The client fires `Game.OnStart` after module initialization; the server fires its initialization lifecycle events after `InitModules()` in the server startup sequence. ## Events and callback ownership An event handler must carry `[[Event]]` and must be passed through `Subscribe` or `Unsubscribe`. Direct calls to event handlers are blocked by attribute validation. The same ownership rule applies to other callback attributes: - `[[TimeEvent]]` functions belong to `StartTimeEvent`, `StopTimeEvent`, `CountTimeEvent`, `RepeatTimeEvent`, and `SetTimeEventData` APIs; - remote-call handlers belong to the remote-call dispatcher described in [Remote Calls](../../reference/scripting/remote-calls.md); - animation and property callbacks belong to their corresponding registration APIs. This separation makes invocation context visible. Put reusable logic in an ordinary helper and let the attributed callback adapt event arguments to it. ```angelscript [[Event]] void OnSomeEvent(Critter critter) { ApplyImmediateRule(critter); } void ApplyImmediateRule(Critter critter) { // Ordinary logic that may also be called from another ordinary function. } ``` Subscriptions are stored on the entity that owns the event. `Entity::MarkAsDestroyed()` clears all of that entity's event callbacks and time-event records before marking it destroyed. Server managers additionally cancel scheduled time-event jobs before final entity destruction. Explicitly unsubscribe when behavior must stop before the owner is destroyed, when replacing a callback, or when a long-lived global owner should release a project object. Do not maintain a second project-wide registry solely to unsubscribe ordinary entity callbacks during destruction; that duplicates engine lifetime ownership and creates another source of stale handles. Event dispatch may run user callbacks that alter subscriptions, so the engine iterates a callback snapshot. Do not rely on a subscription added during a dispatch being called in that same dispatch. ### Property transformation versus post-set reactions A value-transforming property setter runs before storage: it receives the proposed value by reference, while reading the entity still sees the previous property. Use it to validate or transform that value, not to rebuild a UI cache from the entity's new state. Reaction-only callbacks run after storage. In AngelScript, `Game.AddPropertySetter` selects the post-set path for an entity-only handler; the by-reference handler selects the transforming path. In Managed C#, `[PropertySetter]` marks the handler, while generated `Game.AddPropertyDeferredSetter(property, handler)` explicitly selects the entity-only post-set path; the entity-only `AddPropertySetter` overload is equivalent. The `ref T` overload remains pre-set. A later event that happens to refresh a cache is not a substitute for choosing the correct phase. ### Persistence preload boundary `Game.OnCritterPreLoad` is the server-side migration hook for an existing persisted critter. It fires once after the critter properties, inventory, and inner entities have been restored and the critter has been registered, but before map or global-map entry, visibility processing, `OnCritterInit(critter, false)`, and `OnCritterLoad`. Newly created critters do not receive this event. The callback runs with map transfers locked. Limit it to normalizing the critter's own persisted properties and inventory: the rest of the world may still be only partially restored, and resolving, loading, or relocating other persisted entities is not supported at this phase. A handler may explicitly destroy the critter to drop a stale persisted graph. Throwing or stopping the event chain instead marks the load as failed and leaves the persisted record available for diagnosis. See [Server Runtime](../../explanation/runtime/server.md) for the full load order and player-bound behavior. ## Entity `InitScript` callbacks `Item`, `Critter`, `Map`, and `Location` have a server-side persistent `InitScript` property. It names a global callback with the corresponding entity plus `bool firstTime`; authored values are signature-checked during server baking. The exact signatures and prototype-authoring rules are in [Prototype Format](../content/prototype-format.md#init-scripts). For normal entity initialization, `EntityManager::CallInit()`: 1. validates the entity and returns if initialization already ran; 2. holds the entity alive and sets its initialized flag; 3. fires the matching `Game.OnItemInit`, `OnCritterInit`, `OnMapInit`, or `OnLocationInit` event; 4. invokes the named `InitScript` only if the event did not destroy the entity; 5. recursively initializes owned children that remain alive. `firstTime` is `true` for newly created entities and `false` for restored world state. A newly created location initializes its child maps before the location callback, while world loading begins at the location and cascades down through maps, critters, and items. Do not encode cross-entity ordering assumptions in an init callback. An unresolved name or mismatched signature throws `ScriptException`. Because the initialized flag and event dispatch precede resolution, this is a fail-loud `Basic` lifecycle guarantee, not transactional rollback. A script-body exception follows a different path: `ScriptFunc::Call()` is `noexcept`, reports the exception, and returns `false`. Normal `CallInit()` treats that as already reported; runtime `SetupScript` / `SetupScriptEx` convert it to `ScriptException("Call init failed", ...)`. `SetupScript(typedFunction)` and `SetupScriptEx(name)` run the callback immediately with `firstTime = true`, then persist the function name only after success. The typed overload rejects delegates because a persisted callback must be globally resolvable. An empty property means no entity-specific callback; projects may use the corresponding global `Game.On*Init` event when one subscriber should own behavior across many prototypes. The callback enters with its entity covered. Touching another entity requires the normal complete cover or a checked widening operation. The callback is not implicitly async, and no cover survives a `Yield`. ## Async propagation and `Yield` `Yield(int durationMs)` is registered with the `Async` function attribute. `Async` is a transitive marker: every script function that directly calls an `[[Async]]` function must also carry `[[Async]]`. A callback may carry both its invocation attribute and the marker: ```angelscript [[TimeEvent]] [[Async]] void RefreshLater(Critter critter) { Yield(10); // This is a later observation. Revalidate before accessing mutable state. } ``` When `Yield` runs, AngelScript suspends the active context and schedules `ResumeSpecificContext()` for the requested game-frame time. The script stack and local handles remain in that context. The native call that was executing the context returns as suspended. The two runtime sides resume differently: - **Client:** delayed callbacks are processed from a snapshot of callbacks that were already due at the start of the current main-loop pass. `Yield(0)` therefore resumes on the next pass, after the loop can process network and input work. - **Server:** delayed callbacks are submitted to the worker pool. Every worker job has a synchronization context, and every script execution creates a nested synchronization context. A continuation may resume on a different worker thread. The context manager prevents two workers from executing the same suspended AngelScript context concurrently. This protects the continuation object itself; it does not serialize unrelated callbacks or make project state thread-safe. ### The suspension rule Treat every `Yield` and every helper that can suspend as a transaction boundary: 1. Finish or abandon the current mutation before suspending. 2. Do not retain a `Game.Lock` expectation across suspension. 3. After resumption, reacquire the required entity cover. 4. Resolve or revalidate entities that may have been destroyed, detached, or reparented. 5. Re-read properties, collections, parent links, and other mutable decisions before writing. Identifiers are often safer continuation state than an assumed-valid entity snapshot. Resolving an identifier after resumption still requires null/destroyed handling and synchronization before access. ## Server entity synchronization Server callbacks can run concurrently. The engine validates authoritative entity access against the current thread's `SyncContext`; an uncovered read or write is a contract error, not a benign race to ignore. `Game.Sync(...)` is the script-visible acquisition boundary: - it replaces the current entity cover with the supplied non-null entities and engine-defined auto-widen partners; - overloads accept one, two, three, or an array of entities; - the array overload rejects null entries; - a later `Game.Sync(...)` does not extend the previous cover, so request the complete set needed by the next operation; - `Game.GetHeldSyncEntities()` reports the current entity-cover owners for diagnostics; - `Game.IsEntityLocked(entity)` probes coverage without emitting the uncovered-access diagnostic; - `Game.SyncRelease()` releases both the entity cover and any singleton lock entries held by the current script execution scope. `Game.Sync(...)` carries `[[Async]]`, so the marker propagates through its direct script callers even when acquisition completes without suspending the AngelScript context. ### Native entry covers Every server-side AngelScript execution enters through `ServerEngine::RunScriptContext()` and receives its own nested `SyncContext`; an event, setter, or remote-call dispatcher does not create another script scope around the handler. Native entry points may establish the initial cover inside that active context before dispatch. For an inbound server remote call, `Process_RemoteCall()` syncs its `Player` argument immediately before invoking the script handler. Entity widening includes the player's currently controlled `Critter` and the reverse critter-to-player link, so the handler may read that linked pair without an additional `Game.Sync(...)`. Independently resolved entities still require an explicit complete cover. A later `Game.Sync(...)` replaces the entry cover, so include the player or controlled critter again when the following operation still needs them. ### Existing-player reconnect `Game.LoginPlayerToExistentRecord(unloginedPlayer, playerId)` preserves the caller's current cover. For a live reconnect, the caller must acquire the complete graph before calling it: - the incoming unlogined `Player`; - the existing live `Player`; - its controlled `Critter`, when present; - the current `Map` and `Location` when the critter is mapped; - every current global-map group member when the critter is on the global map. The Player/Critter auto-widen pair does not cover the critter's parent map or the map's parent location. Likewise, covering the group leader does not cover sibling global-map members. The complete graph is needed because reconnect dispatches `OnPlayerLogin` and sends critter initial information without narrowing the caller's cover; local-map initial info reads the map/location and global-map initial info serializes the group members. A typical project flow first resolves the optional live player with `Game.GetPlayer(playerId)`, synchronizes the incoming and live players, discovers the covered controlled critter, then acquires and revalidates the relevant map/location or global-group graph. Because every later `Game.Sync(...)` replaces the previous set, all required entities must be present in the final request. The helper and every direct caller must carry `[[Async]]`. Offline stored-record login has no existing runtime player to add. It enters with the unlogined player covered and builds the restored character graph through the normal project `OnPlayerLogin` bootstrap. ### `Game.Lock` is separate `Game.Lock()` acquires the global `Game` singleton lock in a separate, recursive bucket. Pair it with `Game.Unlock()` and keep the critical section small. The entity cover is unchanged by `Game.Unlock()`. Do not call `Game.Sync(...)` while `Game.Lock()` is held. The engine rejects that order to prevent a singleton/entity lock cycle. `Game.SyncRelease()` drains both buckets, including unmatched recursive singleton entries, but that teardown behavior is a safety boundary rather than a recommended substitute for balanced `Lock`/`Unlock` calls. ### Covers do not survive suspension `ServerEngine::RunScriptContext()` creates a nested `ScopedSyncContext` around each `ctx->Execute()` call. The scoped destructor releases all entity and singleton locks when `ctx->Execute()` returns, including when it returns because of `Yield`. Resumption calls `ctx->Execute()` again under a new nested context. Therefore this is invalid reasoning: ```text Game.Sync(entity) -> read state -> Yield(...) -> write using the old cover and decision ``` The correct shape is: ```text resolve entity -> Game.Sync(entity) -> read/mutate -> Yield(...) resolve or revalidate entity -> Game.Sync(entity) -> re-read -> decide/mutate ``` Native extension code has an additional `EnsureEntitySynced(...)` expansion helper for engine-owned operations that pull a covered descendant or freshly created entity into the current context. It is not a replacement for the script-visible `Game.Sync(...)` contract and must not be exposed as a project workaround for an incorrectly scoped operation. ## Mutable state ownership Choose the narrowest owner whose lifetime matches the state: | State | Preferred owner | |---|---| | Per entity, persisted or replicated | A declared entity property with the correct persistence/sync flags. | | Per entity, runtime-only | A non-persistent entity property or engine/project component owned by that entity. | | Per engine instance | A manager or component reachable from `ServerEngine`, `ClientEngine`, or another explicit owner. | | Immutable module data | A `const` global, initialized directly or through `Game.SetConstGlobalVar` during module init. | | Short continuation state | Local values in an `[[Async]]` function, with mutable world data revalidated after suspension. | Avoid mutable script-global dictionaries keyed by entity id for ordinary entity state. They separate data from its lifetime, can leak across id reuse or tests, require independent cleanup, and become a shared concurrency boundary. If state belongs to an entity, storing it on that entity lets synchronization, destruction, persistence, and replication rules remain explicit. An allowlisted mutable-global namespace is an escape hatch for a reviewed subsystem, not the default architecture. Its owner must define synchronization, reset behavior, multi-instance isolation, and shutdown cleanup. ## Managed C# equivalents Managed C# enters the same backend-neutral `ScriptSystem`, entity, property, event, remote-call, and synchronization contracts, but expresses lifecycle in C# and `Task` terms: - `[ModuleInit(priority)]` marks a static parameterless `void` or `Task` method. `ScriptSystem::InitModules()` still orders initializers by ascending priority; a returned task is awaited inside the backend-owned synchronization context before initialization advances. - Event, timer, remote-call, property, and other engine-dispatched handlers use their managed attributes from `CoreScripts/Attributes.cs`. A dispatcher-owned attributed method is entered through its dispatcher, not invoked directly merely because C# can name it. - `async void` is rejected. Awaitable handlers and named calls return `Task`; use `Task` only where the caller contract consumes a result. Inbound remote-call return values are ignored by the wire even when the managed handler is awaitable. - `Game.YieldAsync(milliseconds)` schedules the continuation on the backend's `ScriptSynchronizationContext`. Each backend has its own queue, pumped by engine frames. Work moved to `ThreadPool` or continued through `ConfigureAwait(false)` is outside that context and must not call Engine APIs. - Server cover is declared and checked with `[RequiresCover]`, `[ProvidesCover]`, `[PreservesCover]`, and `CoverReach`. Roslyn diagnostics `FOSYNC001` through `FOSYNC007` and `FOSYNC009` catch invalid targets, missing propagation, entry declarations, covered collections, and locks crossing `await`. - Cover normally does not survive `await`. Preserve it only through the explicit `[PreservesCover]` contract supported by the called operation; otherwise re-resolve mutable state and reacquire the complete cover after resumption. - Named invocation requires `[CallableByName]`. Administrative and internal named-call allowlists are separate boundaries; do not widen one to satisfy the other. - Delegates retained past a call are GC roots owned by the managed backend. Timer unsubscription needs the same delegate identity. Native reference objects retained by managed code must follow the generated `__AddRef` / `__Release` ownership contract. The full generated-project, value-marshalling, runtime-isolation, packaging, platform, and migration details are in [Managed C# Scripting](managed-csharp.md). This section is intentionally the lifecycle crosswalk, not a second competing C# manual. ## Destruction and shutdown Entity destruction and runtime shutdown are related but distinct: - `Entity::MarkAsDestroyed()` clears entity callbacks and time-event records. - Server entity managers cancel dispatcher jobs before final destruction. - Client and server shutdown clear global events/time events and destroy owned entities in an ordered runtime sequence. - `AngelScriptBackend` destroys its context manager, then calls `asIScriptEngine::ShutDownAndRelease()` while modules, types, behaviours, and backend links are still available. The patched AngelScript shutdown calls module exits, releases globals, runs full garbage-collection passes until the live set is empty or stable, discards modules, repeats collection, and reports unreclaimable survivors before the backend links are reset. - `ManagedScriptBackend` stops accepting work, drains and rejects pending scheduler entries in owner order, releases managed handles and its backend load scope, and leaves process-wide Mono shutdown to `ManagedRuntime`. Backend-scoped assemblies are isolated by `ManagedLoadContextHost`; do not use process-global static state as a substitute for an engine-instance owner. The garbage-collection stop condition is **empty or stable**, not always empty. A stable live set can contain unreclaimable survivors, which shutdown reports for diagnosis before continuing its ordered teardown. These bullets do not define one total order that interleaves client/server entity cleanup with the backend-internal module and garbage-collection stages. Follow each owning shutdown sequence independently unless the runtime source establishes a cross-owner ordering edge. Do not use `Game.OnFinish` only to reproduce those owner actions. Use it for functional project teardown such as flushing a project service, ending an external session, or cancelling work owned outside normal entity lifetime. Script expression temporaries, returned object handles, delegates, arrays, and dictionaries are runtime-owned according to their registered AngelScript behaviours. Unsafe-reference expressions may defer receiver and argument cleanup until the expression reaches a safe point; native/script call bridges retain the copied result and release replaced or exceptional-path objects. Project scripts must not add manual reference-count or shutdown registries to compensate for those internals. A surviving object graph indicates a missing owner release or GC enumeration contract in the owning type. A suspended continuation can outlive the gameplay assumptions under which it started. On resumption, a destroyed handle must be treated as invalid even if the local variable is still non-null at the script-language level; the normal entity access guard is expected to reject destroyed access. Prefer explicit resolution and nullable narrowing where destruction is a normal outcome. ## Recommended workflow When adding or changing a callback: 1. Identify its owning API and apply the required callback attribute. 2. Add `[[Async]]` to the complete direct call chain if it reaches `Yield`, `Game.Sync`, or another async-marked function. 3. List every authoritative entity read or written between suspension points. 4. Acquire one complete cover for that operation and do not hold `Game.Lock` while acquiring it. 5. Put mutable state on its lifecycle owner; document any mutable-global exception. 6. Revalidate and reacquire after suspension or re-entry. 7. Add a focused script/compiler/runtime test and run the embedding project's script bake. ## Validation routes Choose the narrowest gate that proves the changed contract: | Change | Minimum validation | |---|---| | AngelScript callback attribute or direct-call rule | `Test_AngelScriptAttributes` and project `CompileAngelScript`/bake. | | Managed callback, named-call, or async signature | `Test_ManagedScriptBaker`, managed core tests, and project `CompileManagedScripts`/bake. | | Mutable global/static policy | `Test_AngelScriptBaker` for AngelScript; managed owner/isolation tests plus project compile for C#. | | `Yield` / `YieldAsync` or context scheduling | The affected AngelScript context test or managed async/callback-context tests plus the affected client/server runtime test. | | Server cover, singleton lock, or access validation | `Test_EntitySync`, affected script-method/entity tests, and a project runtime path. | | Inbound server remote-call cover | Server runtime/remote-call tests plus a handler that reads the caller's controlled critter. | | Persisted critter preload migration | `Test_EntityLifecycle` plus the embedding project's migration and bake tests. | | `InitScript` authoring or runtime resolution | `Test_ServerMapOperations`, focused baker tests, and an embedding-project bake. | | Entity callback/time-event lifetime | Entity/time-event tests and a destruction or shutdown smoke path. | | Script object lifetime or shutdown GC | AngelScript: `Test_AngelScriptCall` and an engine shutdown smoke; Managed: callback-GC/load-context tests and a managed runtime shutdown smoke. | | Project script behavior only | Embedding-project bake plus the narrowest gameplay/scene test. | For all engine documentation changes, also run the standalone documentation gate from [Documentation maintenance](../../contributing/documentation/). ## Review checklist - The callback is entered through its owning API, not called directly. - Every async caller carries `[[Async]]`. - Every managed async entry returns `Task` (never `async void`), remains on the backend synchronization context while calling Engine APIs, and re-establishes cover after `await` unless the operation explicitly preserves it. - No entity cover or state snapshot is assumed to survive `Yield`. - `Game.Lock` is balanced and released before `Game.Sync`. - One `Game.Sync` call names the complete entity set for the next operation. - A preload migration touches only the restored critter state supported at that phase. - Mutable state lives on an explicit lifecycle owner. - Destruction cleanup is not duplicated in a project-global registry without functional need. - The selected test exercises the actual compile, scheduling, synchronization, or teardown boundary. ===== END DOCUMENT script-lifecycle-concurrency ===== ===== BEGIN DOCUMENT testing ===== Source: Docs/en/contributing/testing/index.md Canonical URL: https://fonline.ru/Docs/en/contributing/testing/ Content SHA-256: 831b0851f5778ed8cf4578b7afb5131af23002a1e283d0f280cbbcb61afb9601 --- layout: default title: Testing locale: en document_id: testing permalink: /Docs/en/contributing/testing/ --- # Testing > Engine-owned documentation. This page maps the current engine test executable, generated test targets, coverage targets, and every `Source/Tests/Test_*.cpp` suite currently present in the checkout. ## Purpose Use this page when choosing native validation for an engine change or when adding/removing Catch2 tests. The source-tree README at [Source/Tests/README.md](../../../../Source/Tests/README.md) is a short entry point; this page is the maintained full test map. For deterministic script, content, server, and client process tests in an embedding game, continue with [Gameplay and Integration Testing](../../how-to/testing/gameplay-and-integration.md). ## Source paths inspected - `Source/Applications/TestingApp.cpp` - `Source/Tests/README.md` - all current `Source/Tests/Test_*.cpp` files - `BuildTools/cmake/stages/EngineSources.cmake` - `BuildTools/cmake/stages/Applications.cmake` - `BuildTools/cmake/stages/Init.cmake` - `BuildTools/cmake/helpers/RunAndLog.cmake` - `BuildTools/codecoverage.py` - `BuildTools/validate.sh` - `BuildTools/validate.cmd` ## Test runner model `Source/Applications/TestingApp.cpp` is the test application entry point. It requires `FO_TESTING_APP`, initializes the application layer with `InitAppForTesting()`, marks `IsTestingInProgress`, and delegates execution to `Catch::Session().run(argc, argv)`. `BuildTools/cmake/stages/EngineSources.cmake` owns `FO_TESTS_SOURCE`, the explicit list of test source files compiled into test builds. `BuildTools/cmake/stages/Applications.cmake` builds test executables through `SetupTestBuild(name)`: `BuildTools/check_windows7_imports.py [...]` is the standalone PE-level regression check for Windows 7 artifacts. It accepts one or more PE files and fails closed on unreadable or malformed input. It rejects a curated set of Windows 8+ `kernel32`, `user32`, `dxgi`, and `d3d11` exports (including `CreateFile2` and `GetCurrentThreadStackLimits`), libraries absent from Windows 7 (`shcore.dll`, `combase.dll`, `d3d12.dll`, `dcomp.dll`, `d3dcompiler_47.dll`), and API-set contracts except Universal CRT forwarders (`api-ms-win-crt-*`). Add any newly discovered incompatible export to the list with its import fix. Static third-party libraries, including the managed runtime, contribute to the same final PE import table. Embedding-project CI should check every linked Win7 executable and DLL after linking and before packaging; this static gate does not prove runtime behavior on a real Windows 7 SP1 host. See the [Windows 7 compatibility lane](../../how-to/build/#windows-7-compatibility-lane). - `UnitTests` when `FO_UNIT_TESTS` is enabled; - `CodeCoverage` when `FO_CODE_COVERAGE` is enabled. The standard generated names use the embedding project's development-name prefix: `_UnitTests`, `RunUnitTests`, `_CodeCoverage`, `RunCodeCoverage`, `GenerateCodeCoverageReport`, and `AnalyzeCodeCoverage`. Treat the prefix as project-generated, not universal. ## Running tests `Test_ClientEntityLifetime.cpp` covers repeated map unloads with retained handles, pending item owners, failed construction and atlas cleanup with live/empty pages. `Test_MapSprite.cpp` pins holder detachment and reuse after `Clear()`; `Test_ResourceIndex.cpp` pins decoded-vector ownership transfer. The destroyed-map storage bound requires debug/profiling allocator statistics. Headless ownership checks do not qualify physical GPU memory, working-set trends or a platform's long-session OOM behavior. The repeated-unload storage comparison warms one complete map load/unload cycle before reading rpmalloc's committed active-page counter. Twelve destroyed maps retained by native handles must then stay within the same 8 MiB bound. Render-target ownership is checked on every cycle, including warm-up; this separates allocator initialization from repeated retained-map storage, not GPU memory or process working-set acceptance. `MapViewItemHitTesting*` and `TransparentEgg*` cover native sprite picking and egg classification. `Test_MapViewHitTesting.cpp` owns its prototypes, baked sprites and optional AngelScript fixture, so it runs with either scripting backend. It checks a faded wall over a floor, both `ignore_transparent_egg` policies, alpha hit testing, an empty point and clearing the egg. These native queries do not certify an embedding game's cursor input or tool effects. Preferred local baseline from a configured build: ```bash cmake --build . --config RelWithDebInfo --target RunUnitTests ``` With `FO_EFFEKSEER_PARTICLES` enabled, the focused `[particle]` Catch2 cases invoke the published helper through the production `ParticleBaker` path. They cover text compilation, dependency invalidation, malformed XML, and rejection of cooked files presented as authored inputs. The executable target can also be invoked directly when you need Catch2 arguments. Test binaries are emitted under `Binaries/Tests-*`, for example `Binaries/Tests-Windows-win64/_UnitTests.exe` or `Binaries/Tests-Linux-x64/_UnitTests`. Atlas and render-target dump tests use `TexDumpArtifacts` from `Source/Tests/Test_DumpArtifacts.h`. A test snapshots the existing `TexDump_*` directories before it invokes production dumping, then removes only new directories created by that run. This prevents parallel or interrupted test sessions from deleting pre-existing diagnostic evidence and keeps cleanup scoped to artifacts the test can prove it owns. With Visual Studio/MSBuild generators, `RunUnitTests` writes the test process output to `/_UnitTests.log` and uses the test process exit code as the pass/fail signal. This keeps expected negative-case diagnostics such as compiler `error` lines from being reclassified as MSBuild errors. On failure the helper also echoes the captured output before stopping, so CI logs name the failing test/assertion even when the runner workspace and file log are discarded. For broad validation scenarios, the BuildTools validators can run selected scenarios: ```bash Engine/BuildTools/validate.sh unit-tests Engine/BuildTools/validate.sh android-arm64-client linux-client linux-server ``` The ordinary `unit-tests` validator resolves `native` to the host toolchain: MSVC/Visual Studio on Windows, Xcode on macOS, and Clang on Linux. It rejects a cached non-Visual-Studio generator when the Windows configure command requires `-A`, while accepting any cached Visual Studio version rather than pinning the generator display name. Sanitizer validators remain explicitly Linux-specific. `BuildTools/tests/test_native_unit_validation.py` covers platform resolution, configure forwarding, generator-cache checks, and unsupported hosts. Use the smallest focused tests first, then the broader run target when the change crosses subsystem boundaries. ### Unit tests under sanitizers The unit tests also run under Clang sanitizers via dedicated validators, which select the matching `San_*` build type and run `RunUnitTests` instrumented: ```bash Engine/BuildTools/validate.sh unit-tests-san-address # AddressSanitizer (+LeakSanitizer) Engine/BuildTools/validate.sh unit-tests-san-memory # MemorySanitizer (requires Workspace/msan-libcxx) Engine/BuildTools/validate.sh unit-tests-san-undefined # UndefinedBehaviorSanitizer Engine/BuildTools/validate.sh unit-tests-san-thread # ThreadSanitizer ``` The `validate.yml` workflow runs these as a `unit-tests-sanitizers` matrix job. ASan/MSan/UBSan/TSan are blocking legs. The `unit-tests-san-memory` validator prepares `Workspace/msan-libcxx` by building LLVM's `libc++`, `libc++abi`, and `libunwind` with MSan instrumentation, then configures `San_Memory` with `FO_MSAN_LIBCXX_ROOT`. The runtime build applies a narrow libunwind ignorelist so C++ exception and sanitizer-report unwinding do not self-report on ABI register snapshots. Engine native stack capture and crash handlers are disabled under MSan and TSan so the sanitizer runtimes own their reports. Bundled LLVM libunwind and libbacktrace are built without instrumentation because their crash paths read other frames and debug data. `unit-tests-san-memory-with-origins` is available locally as the slower diagnostic variant when a future MSan finding needs origin tracking. `San_DataFlow` remains intentionally unwired: DataFlowSanitizer is a taint-tracking framework, not a defect detector. Applications that load `BakerLib` while running under a sanitizer must use a baker built with the same `San_*` configuration. Hiding the plugin's ELF exports prevents direct symbol interposition, but calls implemented inside the shared C++ runtime may still allocate through the host and return to an inline deallocator in the plugin. Matching configurations keep the sanitizer runtime and allocator contract identical on both sides of that module boundary. On MSVC, the `San_Address`/`Debug_San_Address` configs additionally link executables with `/STACK:8388608` (`AddExecutableApplication` in `BuildTools/cmake/helpers/Build.cmake`): ASan's stack-frame inflation overflows the 1 MiB Windows executable default on recursion depths that fit every production configuration, so sanitizer runs get the same 8 MiB reserve that Linux runs already have from the default rlimit. Production configs keep the 1 MiB default. Vendored third-party libraries are excluded from UBSan's `-fsanitize=function` and `-fsanitize=alignment` checks (the rest of `-fsanitize=undefined` still applies to them). `DisableLibWarnings` adds `-fno-sanitize=function,alignment` on the `San_Undefined`/`San_Address_Undefined` configs because several vendored libraries trip those two checks by design: - `function`: AngelScript's script-call dispatch invokes registered C functions through `bool(*)(void*,void*)` and similar signatures, and C callback APIs do the same. - `alignment`: AngelScript builds its bytecode in an `asDWORD[]` (4-byte) buffer and packs pointer-sized `asPWORD` operands at 4-byte-aligned slots (`*(asPWORD*)(bc+1) = ...` in `GenerateFactoryStubForTemplateObjectInstance`), which UBSan reports as a misaligned store even though it is correct on every architecture the engine targets. Both are third-party idioms, not undefined behaviour in engine code, so they must not fail the UBSan leg (which CI runs with `halt_on_error=1`). First-party engine code keeps both checks fully active. LeakSanitizer runs as part of the address-sanitizer leg (CI sets `ASAN_OPTIONS=detect_leaks=1`). It runs with **no suppression list** — every leak it can report is fixed at the source rather than masked. Notable cases: - Linux libbacktrace (`Source/Essentials/StackTrace.cpp`) retains debug information in its own mapped memory for the life of the process. Its state remains reachable through process-lifetime `StackTraceState`, serialized by `NativeResolverLocker`; a module is read once and LSan has no allocated orphan to report. - The AngelScript backend deletes the preprocessor line-number translator during engine userdata cleanup, and each SPARK context frees its `IOManager` converters at context shutdown. - Owning containers free their contents transitively: e.g. `EntityTypeDesc::PropRegistrar` is a `unique_ptr` so every `PropertyRegistrar` (and the `Property` objects it holds) is freed when `EngineMetadata`'s type maps are destroyed. ## Code coverage When `FO_CODE_COVERAGE` is enabled, `BuildTools/cmake/stages/Init.cmake` selects the backend from the compiler: - MSVC / clang-cl: MSVC-style coverage output; - Clang: LLVM profile/coverage mapping; - GCC: GCC/lcov-style coverage flags. `BuildTools/cmake/stages/Applications.cmake` wires coverage command targets through `BuildTools/codecoverage.py`: - `CleanCodeCoverageData` - `RunCodeCoverage` - `GenerateCodeCoverageReport` - `AnalyzeCodeCoverage` Coverage output is rooted under `CodeCoverage///`. The isolated Applications-stage fixtures in `BuildTools/tests/test_codecoverage_llvm_objects.py` include the real resource-pack hash helper sources and prove that native host platforms build the helper without LLVM coverage flags. They also retain actual instrumented-process collection, core library reuse and quick-exit flush assertions. `BuildTools/codecoverage.py` reports first-party production engine sources under `Engine/Source/`; it excludes `Source/Tests/`, `ThirdParty/`, `GeneratedSource/`, and `Applications/` from the denominator. See [Source/Tests/README.md](../../../../Source/Tests/README.md) for current local task notes. Coverage is platform- and environment-specific. Sources not compiled in the current build have no mapping and are reported separately as untouched. Sources that compile but cannot execute in a headless test process belong in `ENVIRONMENT_EXCLUDED_SOURCES` with a written reason; currently this covers device-backed audio/video, Mongo/updater infrastructure, and the deliberately process-killing diagnostic self-test. Loopback sockets and the debugger endpoint remain in the headline. The report shows scoped, all-source, and excluded buckets independently. An exclusion is a routing decision: the owning platform, windowed run, or real-endpoint integration lane must cover it. ### Focused harness patterns - **ImGui panels:** create a backend-less context, set `ImGuiBackendFlags_RendererHasTextures`, and use `ImGui::LogToBuffer(depth)` to auto-open tree nodes and prove nested text rendered. Collapsing headers opt out and need their IDs seeded in `StateStorage`. Destroy the context at scope exit. To cover widget branches, `ImGuiTestHarness::ActivateItem` needs two frames; address controls under child windows with `ActivateChildItem`, draw only the owning panel so another window's focus request cannot erase activation, and clear stale active IDs between presses. - **Server diagnostics:** a sync point does not itself cover entities. Snapshot not-logged-in players under the publication lock, release it before entity locks, and then acquire one replacement cover for the snapshot and registered world. Keep a real not-logged-in fixture in the test. - **Inbound remote calls:** entry covers the calling player and its controlled critter. Any second entity needs explicit `Game.Sync`; do not probe an operation expected to violate cover behind script `try/catch`, because the session is torn down before the next probe arrives. - **Crash reporting:** test non-terminating crash-stream formatting through a private log file, then restore logging to `NUL` or `/dev/null`. Test terminating reporters out of process with `DiagnosticSelfTest`; `main_basic_strong_assert`, `main_fatal_exit`, and `main_failure_exit` distinguish early fatal reporting from raw status-only exit. - **Fonts without assets:** synthesize `.fofnt` text or BMFont `BMF\3` blocks in memory, provide a matching sprite, and bind with scale in `(0..1]`. `SplitLines` emits rect-sized pages, so use a short rectangle when a test needs multiple outputs. - **Logged-in client/server:** declare login remote calls in both metadata blobs with opposite directions and the correct subsystem/namespace, add at least one project-owned persistent `Player` property before login insertion, then create/switch a critter and transfer it into a location/map to reach world-entry replication. Both `.fomap-bin-*` blobs start with `BAKED_MAP_FILE_MAGIC` and `BAKED_MAP_FILE_VERSION`; after the header, the client layout ends after the hash-table and static-item counts. - **World reload:** use file-backed JSON, mark expected entities persistent, shut down one server, and start another on the same directory. Reload reaches critters through their owning map or global-map membership; an off-map runtime critter is not restored. - **Headless 3D:** bake a non-degenerate triangle, build the description with `ModelInfoBaker`, provide both source and baked mesh entries plus `Metadata.fometa-client` and `ModelAnimationInfo.foinfo`, then instantiate through the null renderer. Build fixture metadata with `BakerTests::MakeMetadataBlob` or `MakeEmptyMetadataBlob`; registration rejects a blob without the required metadata version. - **Static maps and mapper disk writes:** write the baked-map format header first, then serialize real `Properties::StoreAllData()` payloads for server map records; zero length is invalid. The server payload continues with hashes, critters, and items, while the shorter client payload contains hashes and static items. Mapper save tests need an actual `InputDirs` Maps root containing a reference `.fomap`; prefer `SaveMapToDir`, because plain `SaveMap` may otherwise write into the process working directory. A per-map static item removal is only observable end to end when the *same* static item id appears in both payloads — the server needs it in `StaticItemsById` to remove and the client needs a view built from it to drop — so `Test_ClientServerIntegration` carries one such item in both map blobs. ## Current test inventory The authoritative test-file count and complete sorted filename list are generated from `Source/Tests/Test_*.cpp` into [source-inventory.json](../../../generated/source-inventory.json). Do not copy the total or full list into prose. Regenerate and verify it from the engine root: ```bash python BuildTools/docs_inventory.py --write python BuildTools/docs_inventory.py --check ``` Use these ownership groups to choose a starting area; the filenames are representative, while the generated JSON is exhaustive: ### Configuration and data sources - `Source/Tests/Test_CacheStorage.cpp` - `Source/Tests/Test_ConfigFile.cpp` - `Source/Tests/Test_DataSource.cpp` - `Source/Tests/Test_FileSystem.cpp` - `Source/Tests/Test_Settings.cpp` - `Source/Tests/Test_SettingsStorage.cpp` ### Common runtime model - `Source/Tests/Test_ApplicationHeadless.cpp` - `Source/Tests/Test_AnyData.cpp` - `Source/Tests/Test_Common.cpp` - `Source/Tests/Test_EngineMetadata.cpp` - `Source/Tests/Test_EntityLifecycle.cpp` - `Source/Tests/Test_EntityProtos.cpp` - `Source/Tests/Test_Geometry.cpp` - `Source/Tests/Test_LineTracer.cpp` - `Source/Tests/Test_MapLoader.cpp` - `Source/Tests/Test_Movement.cpp` - `Source/Tests/Test_PathFinding.cpp` - `Source/Tests/Test_Properties.cpp` - `Source/Tests/Test_ProtoManager.cpp` - `Source/Tests/Test_TextPack.cpp` - `Source/Tests/Test_Timer.cpp` - `Source/Tests/Test_TwoDimensionalGrid.cpp` ### Networking and server/client integration - `Source/Tests/Test_ClientDataValidation.cpp` - `Source/Tests/Test_ClientEngine.cpp` - `Source/Tests/Test_ClientRuntimeApi.cpp` - `Source/Tests/Test_ClientServerIntegration.cpp` - `Source/Tests/Test_DataBase.cpp` - `Source/Tests/Test_EntitySync.cpp` - `Source/Tests/Test_FogOfWar.cpp` - `Source/Tests/Test_LocationAndEntityMgmt.cpp` - `Source/Tests/Test_ModelAnimation.cpp` - `Source/Tests/Test_NetBuffer.cpp` - `Source/Tests/Test_NoiseProtocol.cpp` - `Source/Tests/Test_NetworkClient.cpp` - `Source/Tests/Test_NetworkServer.cpp` - `Source/Tests/Test_NetworkUdp.cpp` - `Source/Tests/Test_ServerAdvancedOps.cpp` - `Source/Tests/Test_ServerEngine.cpp` - `Source/Tests/Test_ServerEventContracts.cpp` - `Source/Tests/Test_ServerItems.cpp` - `Source/Tests/Test_ServerMapOperations.cpp` - `Source/Tests/Test_SecureChannel.cpp` ### Scripting and script-visible APIs - `Source/Tests/Test_AngelScriptAlignment.cpp` - `Source/Tests/Test_AngelScriptAttributes.cpp` - `Source/Tests/Test_AngelScriptBytecode.cpp` - `Source/Tests/Test_AngelScriptCall.cpp` - `Source/Tests/Test_ManagedScriptBaker.cpp` - `Source/Tests/Test_CommonScriptMethods.cpp` - `Source/Tests/Test_ScriptBuiltins.cpp` - `Source/Tests/Test_ScriptEntityOps.cpp` - `Source/Tests/Test_ServerScriptMethods.cpp` The AngelScript suites cover its compiler/runtime, bytecode, call bridge, attributes, builtins, and baker. Managed C# has separate evidence layers: `Test_ManagedScriptBaker.cpp` covers native baker generation; `BuildTools/tests/test_managed_*.py` covers runtime setup, payloads, platforms, callbacks, GC roots, and packaging; `Source/Scripting/Managed/Analyzers/Tests` exercises the Roslyn synchronization analyzer; and `Source/Scripting/Managed/Tests` exercises CoreScripts and generated API bootstrap. Run `CompileManagedScripts` and then a real Managed resource bake to prove the embedding project's configured sources, target assemblies, and `ManagedRuntime/` payload. These layers complement rather than substitute for the backend-neutral entity/script-method tests. ### Bakers and tools - `Source/Tests/Test_AngelScriptBaker.cpp` - `Source/Tests/Test_BakerSetup.cpp` - `Source/Tests/Test_ConfigBaker.cpp` - `Source/Tests/Test_EffectBaker.cpp` - `Source/Tests/Test_ImageBaker.cpp` - `Source/Tests/Test_MapBaker.cpp` - `Source/Tests/Test_Mapper.cpp` - `Source/Tests/Test_MetadataBaker.cpp` - `Source/Tests/Test_ModelBaker.cpp` - `Source/Tests/Test_ModelBounds.cpp` - `Source/Tests/Test_ModelMeshData.cpp` - `Source/Tests/Test_ModelAnimationData.cpp` - `Source/Tests/Test_ModelAnimationConverter.cpp` - `Source/Tests/Test_ModelAnimationPoseProcedural.cpp` - `Source/Tests/Test_ModelAnimationRuntime.cpp` - `Source/Tests/Test_ModelSpriteLayout.cpp` - `Source/Tests/Test_ModelSkeletonCompatibility.cpp` - `Source/Tests/Test_ModelSourceLoader.cpp` - `Source/Tests/Test_OzzAnimation.cpp` - `Source/Tests/Test_ProtoBaker.cpp` - `Source/Tests/Test_ProtoTextBaker.cpp` - `Source/Tests/Test_RawCopyBaker.cpp` - `Source/Tests/Test_TextBaker.cpp` - `Source/Tests/Test_TextureAtlas.cpp` The model-animation tests divide the production contract explicitly. `Test_ModelMeshData.cpp` exercises the mandatory `LFMODMSH` schema-1 mesh-only header and complete recursive payload codec. It covers geometry, skin palettes, children, structural validation, trailing data, every truncated header size, rejection of old headerless data, and exact byte compatibility with the original schema-1 writer layout. `Test_ClientEngine.cpp` also bakes a position-only OBJ through `ModelMeshBaker` and preloads the resulting bytes through the real `ModelManager` parser. This crosses the `BakerLib`/`ClientLib` boundary and catches payload-layout drift that a second test-only parser could reproduce instead of detecting. `Test_ModelSourceLoader.cpp` covers complete source validation, real minimal OBJ/ASCII-FBX extraction, per-call cache single-flight behavior, shared results, exception fan-out, and missing inputs. `Test_ModelAnimationData.cpp` exercises the little-endian archive, joint-remap, and rig-manifest contracts, including truncation, count/length bombs, ordering, metadata mismatches, and bindings. `Test_ModelAnimationConverter.cpp` covers canonical conversion and the per-instance runtime pose: unaligned/owned loading, body blending, movement replacement, reverse and nearest sampling, stable storage, canonical resolution, and numeric limits. `Test_ModelAnimationPoseProcedural.cpp` covers bounded procedural pre-rotations and exact world-matrix overrides; `Test_ModelAnimationRuntime.cpp` covers the validated direct-model rest path, canonical contributed-joint lookup, and cross-model joint-link resolution without physical bones. `Test_ModelBaker.cpp` covers source-backed model-info generation, dependency-mtime invalidation, exact animation-geometry exceptions, `Base`, reverse, case-insensitive lookup, and clip deduplication. `Test_ModelAnimation.cpp` is the timeline/binding behavior gate: controller copies own mutable event state while sharing only immutable Ozz clip metadata. After source-loader, mesh-wire, or converter changes, `ForceBakeResources` is the positive real-content gate: it must parse the project's actual selected FBX sources and extract their animations successfully. Run ordinary `BakeResources` afterward to check that the dependency-mtime contract leaves an unchanged tree incremental-clean. ### Rendering/frontend smoke tests - `Source/Tests/Test_ImGui.cpp` — pins the backend-less widget-activation and window-state harness used for diagnostic-panel coverage. - `Source/Tests/Test_EffekseerParticleRuntime.cpp` — runs cooked legacy and modern Effekseer effects through the native runtime's real Sprite/Ring callbacks and validates deterministic multi-instance topology, FOnline geometry, atlas UVs, all three Z-sort modes, Ring index-budget chunking, and facade-level scale reapplication without respawn or timing reset. - `Source/Tests/Test_ParticleBaker.cpp` — covers `.efkproj` source discovery, `.spark`/`.efkproj` output-key mapping, generated binary validation, and rejection of authored `.spk`/`.efk` runtime inputs. The build/integration bake path exercises the native fixed-profile exporter on real XML projects. - `Source/Tests/Test_Rendering.cpp` ### Ownership summary | Area | Typical coverage and starting points | |---|---| | Essentials and low-level utilities | Logging, containers, serialization, filesystem, exceptions, memory, platform, smart pointers, stack traces, strings, time, and worker primitives. | | Configuration and data sources | Cache storage, config parsing, data sources, filesystems, and `Test_Settings.cpp`. | | Common runtime model | Metadata, entities/prototypes, geometry, map loading, movement, line tracing, and path finding. | | Networking and server/client integration | Network buffers, connection flows, ordered UDP, server engine/map operations, client runtime ABI, updater, and database behavior. | | Scripting and script-visible APIs | AngelScript and Managed C# backends, bakers, generated APIs, callbacks/async behavior, entity ops, exports, script methods, synchronization, and value semantics. | | Bakers and tools | Baker, metadata/resource packers, mapper/editor tools, asset processors, and tool-side regressions. | | Frontend and rendering | Application init, frontend/rendering smoke cases, headless behavior, and renderer-facing contracts. | Managed interop changes require both generated-shape and live-runtime evidence. `Test_ManagedScriptBaker.cpp` pins dense ABI ids, typed settings and scalar/value property routes, raw-byte fixed-value arrays, `FillInnerEntities`, generated callback adapters and wrapper factories, and inclusion of `*Abi.gen.cs` in the bake stamp. The native frame test deliberately passes an unaligned packed buffer through `ManagedAbiNativeFrame` and verifies aligned arguments, selective mutable/result copy-back, bounds checks, and value preservation. `CoreScripts/InteropProbe.cs` is the reusable live probe. It compares runtime invoke, classic thunk, and `UnmanagedCallersOnly` callback transports where dynamic code exists; exercises enum/bool/int64/value/`hstring`, exceptions, collections, nested entries, native threads, instance and virtual targets; and reports managed bytes plus per-thread native counters for handles, lookups, objects, and wrappers. Native allocation counts are available only in Tracy builds. Latency is observational rather than a CI threshold, but allocations, delivery counts, and transport checks are assertions. On browser and device clients set `ManagedScript.InteropProbeOnStart=True` and require the closing `INTEROP-TRANSPORT summary` to report zero failures; interpreter-only Web tests runtime invoke and skips transports that need compiled code. ## Validation routing by change type - Essentials utilities: start with [Essentials.md](../../reference/native/essentials.md) and the essentials tests listed above. - Config, file lookup, caches, resource packs: [Configuration and Data Sources](../../reference/settings/configuration-and-data-sources.md), parser/filesystem/cache tests, and affected bake/runtime consumers. - BuildTools/CMake/generation: [BuildTools Pipeline](../../reference/cmake-and-buildtools/pipeline.md), [GeneratedApiAndMetadata.md](../../reference/metadata/index.md), codegen/property/metadata tests, and at least one generated target. - Bakers/resources: [Baking Pipeline](../../explanation/content-pipeline/baking.md) and the matching baker tests. - Runtime entity/map/persistence/networking: [Entity Model](../../explanation/entity-and-property-model/), [Maps and Movement](../../explanation/maps-and-movement.md), [Persistence](../../explanation/persistence/), [Networking](../../explanation/authority-and-networking/), and the focused runtime tests. - Client/frontend/server: [Client Runtime](../../explanation/runtime/client.md), [Frontend and Rendering](../../explanation/rendering/), [Server Runtime](../../explanation/runtime/server.md), and the matching integration/smoke tests. - Scripting: [Scripting](../../explanation/scripting-runtime/), [Managed C# Scripting](../../how-to/scripting/managed-csharp.md), [Script Lifecycle and Concurrency](../../how-to/scripting/lifecycle-and-concurrency.md), [AngelScript Style and Refactoring](../../how-to/scripting/style-and-refactoring.md), [Script Methods Map](../../reference/script-api/method-ownership.md), [Nullability](../coding-contracts/nullability.md), and the script/baker/method tests. Route AngelScript attributes and mutable globals to its dedicated suites; route Managed generation, indexed ABI/native-frame transport, analyzers, async callbacks, runtime payloads, and packaging to the matching native, Python, and C# suites; route shared server cover/lock behavior to `Test_EntitySync` plus the affected script-method/entity tests. ## Adding or removing tests 1. Add the new `Source/Tests/Test_*.cpp` file with deterministic Catch2 tests. 2. Add it to `FO_TESTS_SOURCE` in `BuildTools/cmake/stages/EngineSources.cmake`. 3. Run `python BuildTools/docs_inventory.py --write` so the generated filename list and count stay current. 4. Update this page only when the new test changes an ownership group or validation route. 5. Run the focused test binary and, when practical, `RunUnitTests`. 6. If coverage behavior changed, verify the relevant coverage target. ## Validation checklist 1. `python BuildTools/docs_inventory.py --check` proves the generated filename list and count match `Source/Tests/`. 2. `python BuildTools/docs_validate.py` proves the generated artifact and documentation links are current. 3. Target names are described as generated from `FO_DEV_NAME`, not hard-coded as universal engine names. 4. If `TestingApp.cpp`, `FO_TESTS_SOURCE`, ownership groups, or coverage target wiring changes, update this page in the same change. ## See also - [Profiling](../../how-to/quality/profiling.md) for Tracy build modes, workload isolation, and performance captures. - [Native, AngelScript, and Managed C# Debugging](../../troubleshooting/debugging.md) for backend-specific diagnosis. ===== END DOCUMENT testing ===== ===== BEGIN DOCUMENT gameplay-testing ===== Source: Docs/en/how-to/testing/gameplay-and-integration.md Canonical URL: https://fonline.ru/Docs/en/how-to/testing/gameplay-and-integration.html Content SHA-256: c7d9307bc33c73e7a286d3e384222b191f1807108a2ba0481e5e1df707387121 --- layout: default title: Gameplay and Integration Testing locale: en document_id: gameplay-testing permalink: /Docs/en/how-to/testing/gameplay-and-integration.html --- # Gameplay and Integration Testing > Engine-owned documentation. This guide defines reusable test selection, deterministic fixture, process-runner, marker, deadline, cleanup, and evidence contracts for games that embed FOnline. ## Purpose Use this guide when a change must be proved beyond one native function but does not yet require a packaged release or device laboratory. The Engine supplies a project-neutral process runner and two kinds of proof: - [Gameplay Test Harness Fixture](../../../../Examples/GameplayTestHarness/README.md) proves the runner itself without compiled game binaries; - [Minimal Multiplayer](../../../../Examples/MinimalMultiplayer/README.md) proves the same runner against baked content, metadata, a headless server, a headless client, networking, map load, remote calls, replicated state, and an interaction. For the native Catch2 executable, generated targets, sanitizers, and coverage inventory, use [Testing](../../contributing/testing/). For a first end-to-end lesson, use [First Automated Test](../../tutorials/first-test.md). ## Test decision Use boundary-based test selection and run narrow-first so the first failing layer gives a focused failure location. For a process smoke, declare a `ready_marker`, ordered `required_markers`, `forbidden_markers`, and one common deadline; always retain command lines, logs, exit reasons, and cleanup results. The Engine owns this runner contract. The embedding project owns its script test registration API, fixture catalog, gameplay assertions, acceptance thresholds, accounts, databases, scenes, and release lanes. ## Select the narrowest owning boundary Choose the first layer that can observe the contract being changed. Add broader evidence only when the behavior crosses another boundary. | Change boundary | First proof | Add when | |---|---|---| | Pure native algorithm or data structure | focused Catch2 case | integration changes construction, serialization, threads, or runtime roles | | Parser, baker, or generated metadata | focused native test plus bake | a runtime consumes the generated result | | One script module or callback | project-owned script test in the affected side | state crosses module, entity, persistence, or client/server boundaries | | Authored prototype, map, text, or resource | bake plus semantic content assertion | a client must load, render, hear, or interact with it | | Server lifecycle or world setup | one headless server process | networking or a client-observable result is part of the contract | | Client/server protocol or gameplay interaction | ordered headless server/client smoke | presentation, package, device, or backend behavior is claimed | | Package, updater, platform, or release behavior | package/platform acceptance lane | production infrastructure or recovery policy is claimed | This is boundary-based test selection, not a fixed pyramid. A broad smoke cannot replace a focused failure location, and a unit test cannot prove a process, network, or baked-content boundary it never enters. Run narrow-first: prove the smallest owner, then add each crossed layer in dependency order. Stop on the first failure and diagnose that layer before increasing the scope. ## Deterministic fixture contract A gameplay fixture should make setup and completion explicit: 1. Start from an isolated working directory, storage namespace, account set, ports, and output path. 2. Select a named config or sub-config whose behavior is checked in with the fixture. 3. Create or load only the world, entities, and authored references required by the assertion. 4. Seed controllable randomness, or remove randomness from the test route. 5. Emit low-volume semantic markers at readiness and asserted outcomes. 6. Request normal shutdown after success so teardown is exercised. 7. Bound every wait and retain enough process output plus a structured result to diagnose failure. Do not treat a fixed sleep as success evidence. A delay may pace a deterministic fixture, but the pass condition must be an exit code, semantic marker, decoded artifact, state query, or visible result owned by the changed boundary. The test owns cleanup for every process and temporary resource it creates. Use unique ports or serialized CI jobs when a fixed runtime port is unavoidable. Never point a smoke test at production storage, credentials, accounts, or services. ## Process runner contract `BuildTools/gameplay_test_runner.py` executes a checked JSON manifest without a shell. Commands are arrays, so arguments retain their boundaries. Runtime paths and other environment-specific values use `{name}` placeholders supplied through repeated `--value name=value` options. Schema version 1 has this ownership: - root: suite `name`, `default_timeout_seconds`, optional `forbidden_markers`, and ordered `scenarios`; - scenario: stable `id`, optional timeout/forbidden-marker overrides, and ordered `processes`; - process: stable `id`, command array, optional working directory/environment, readiness contract, required/forbidden markers, and expected exit code. Unknown fields, duplicate ids or markers, invalid types, and unresolved placeholders are configuration errors. Environment values are added to the inherited process environment. Do not put secrets in manifests, placeholder values, commands, markers, or reports: command lines and process environments can be observable outside the runner. A minimal ordered server/client scenario looks like this: ```json { "schema_version": 1, "name": "project-smoke", "default_timeout_seconds": 60, "forbidden_markers": ["FATAL ERROR!", "ScriptException"], "scenarios": [ { "id": "server-client", "processes": [ { "id": "server", "command": ["{server}", "-ApplyConfig", "{config}"], "ready_marker": "project_server_ready", "ready_timeout_seconds": 20, "required_markers": ["project_server_ready", "project_server_passed"] }, { "id": "client", "command": ["{client}", "-ApplyConfig", "{config}"], "required_markers": ["project_client_connected", "project_client_passed"] } ] } ] } ``` Run it from the embedding-project root: ```bash python Engine/BuildTools/gameplay_test_runner.py \ --manifest Tests/gameplay-smoke.json \ --value server=Build/bin/Game_ServerHeadless \ --value client=Build/bin/Game_ClientHeadless \ --value config=Game.fomain \ --report Workspace/gameplay-smoke-report.json ``` Paths and target names are examples; each project owns its generated executable names and layout. ## Readiness and marker semantics Processes launch in manifest order. When a process declares `ready_marker`, the runner waits for that marker before launching the next process. This is the server-readiness gate for a client or dependent worker. `ready_timeout_seconds` is capped by the scenario's common deadline. After launch, the runner merges each process's standard error into standard output, decodes it as UTF-8 with replacement for invalid bytes, prefixes each displayed line with suite/scenario/process identity, and evaluates: - `required_markers`: every marker must appear in that process's output; - `forbidden_markers`: no process may emit its process, scenario, or suite forbidden markers; - `expected_exit_code`: defaults to 0 and must match exactly. Markers are test protocol tokens, not prose. Make them stable, unique, low-volume, and emitted only after the asserted state exists. Include data only when it is part of the assertion, for example `project_supply_collected=1`. Keep noisy diagnostics in normal logs and list broad catastrophic signatures as forbidden markers. A marker can prove that instrumented code reached a state; it cannot by itself prove unobserved pixels, sound, durability, package contents, or external service behavior. Add the owning artifact decoder, state check, screenshot, audible review, restart, or platform lane for those claims. ## Deadlines and cleanup Each scenario has one common monotonic deadline. Readiness and process completion consume the same budget, preventing a two-process test from silently receiving the full timeout twice. On failure, timeout, or incomplete startup, the runner visits launched processes in reverse order, requests termination, waits up to five seconds, then kills a process that did not stop. Timeout is a test failure, not permission to increase the limit immediately. First determine whether readiness was never reached, a dependency was unavailable, shutdown deadlocked, or the fixture depended on timing. Increase a deadline only when measured valid work on supported CI hosts requires it. ## Result contract The command returns: - `0`: every scenario passed; - `1`: a process, exit-code, marker, readiness, or timeout contract failed; - `2`: CLI or manifest configuration is invalid. With `--report`, the runner writes JSON schema version 1. It records suite/scenario status and duration, timeout state, reasons, process exit codes, and missing/forbidden markers. It deliberately excludes commands, environments, and full logs. CI should retain the report and its normal process log together: the report supports automation, while the prefixed log supplies diagnostic context. ## CMake and CI integration Wire a gameplay smoke as a named custom target that depends on the exact binaries and baked artifacts it consumes. Pass target paths with generator expressions, keep the manifest in `SOURCES`, use `VERBATIM`, and use a terminal when interleaved process output is useful. The [Minimal Multiplayer CMake file](../../../../Examples/MinimalMultiplayer/CMakeLists.txt) is the current executable pattern. CI should run the synthetic runner tests on every change to the runner or schema. Run at least one real headless server/client example on each platform claimed by the example. Product CI then adds its own script tests, content assertions, package lanes, persistence backends, visible checks, and release gates according to the boundaries it owns. ## Failure routing | Symptom | Inspect first | |---|---| | Configuration exit 2 | manifest schema, unknown fields, placeholder spelling, and `KEY=VALUE` arguments | | Process failed to start | resolved executable/working-directory path and host permissions | | Readiness marker missing | earliest process log, selected config/sub-config, startup dependencies, and marker ownership | | Required marker missing | the narrow assertion before it, then remote-call/entity/content state at the crossed boundary | | Forbidden marker found | first occurrence and its native/script source; do not whitelist a real failure signature | | Exit code mismatch after all markers | normal shutdown, teardown callbacks, worker/database drain, and runtime host result | | Timeout | last semantic marker from every process, then deadlock, unavailable dependency, and cleanup behavior | | Local pass but CI failure | ports, path case, locale/encoding, host capacity, graphics/audio assumptions, and undeclared state | ## Project-owned boundary The Engine does not define a game's script test registration API, fixture catalog, gameplay assertions, account/database policy, test filters, authored ids, ports, timing budgets, package matrix, device lab, or acceptance thresholds. Keep those with the embedding project and link here for runner semantics. Likewise, project-specific orchestration frameworks are evidence inputs, not Engine APIs. Promote only a reusable primitive after it has Engine-owned implementation, deterministic tests, an independent guide, and a real public example. An AiControl or MCP adapter can supply semantic observations and actions inside a project smoke, but it does not replace the process runner's deadline, cleanup, marker, and report contract. Keep endpoint schemas and gameplay tools in the project; use [AiControl Protocol](../ai-control-protocol.md) only for the common wire envelope and command lifecycle. Never count the protocol-only Python sample as proof that a native FOnline client drained a command or that the game server enforced normal authority. ## Source paths inspected - `BuildTools/gameplay_test_runner.py` - `BuildTools/tests/test_gameplay_test_runner.py` - `Examples/GameplayTestHarness/fixture_process.py` - `Examples/GameplayTestHarness/synthetic-smoke.json` - `Examples/MinimalMultiplayer/tutorial-smoke.json` - `Examples/MinimalMultiplayer/run_tutorial_smoke.py` - `Examples/MinimalMultiplayer/CMakeLists.txt` - `Examples/MinimalMultiplayer/Scripts/Tutorial.fos` - `Source/Applications/TestingApp.cpp` - `BuildTools/cmake/stages/Applications.cmake` - `.github/workflows/validate.yml` ===== END DOCUMENT gameplay-testing ===== ===== BEGIN DOCUMENT ai-control-protocol-guide ===== Source: Docs/en/how-to/ai-control-protocol.md Canonical URL: https://fonline.ru/Docs/en/how-to/ai-control-protocol.html Content SHA-256: 7c1fe61800bca34ceb303bb1e7d4e4e3c1e7e1e2833960a6b198740fa63a3cfe --- layout: default title: AiControl Protocol document_id: ai-control-protocol-guide locale: en permalink: /Docs/en/how-to/ai-control-protocol.html --- # AiControl Protocol FOnline projects can expose a development client to automated QA agents, local tools, or an MCP adapter without making one game's commands part of the Engine. This page defines that reusable boundary. The exact machine contract is the [generated AiControl protocol reference](../reference/ai-control-protocol/index.md), and the runnable transport example is [Examples/AiControlSample](../../../Examples/AiControlSample/README.md). The contract is **experimental**. Pin an Engine revision, version the project observation separately, and treat every listener as a security-sensitive development feature. ## Integration decision A complete integration keeps four boundaries visible: 1. Reuse only the Engine-owned UTF-8 newline-delimited TCP envelope, common methods and errors, size/queue/history bounds, authorization, accepted sequence ids, and terminal `command_completed` lifecycle. 2. Keep the native listener's application roles and enablement, observation fields and `schemaVersion`, action names and semantics, event extensions, MCP tool names and namespaces, launch recipes, agent policy, and redaction policy in the embedding project. Last Frontier and TLA are separate project examples; neither project's schema becomes Engine behavior. 3. Drain commands through a safe project client-loop handoff and ordinary authenticated gameplay paths. The game server remains authoritative; an AI adapter never creates a second authority channel. 4. Report evidence in layers. The protocol sample proves framing, bounds, authorization, and command lifecycle only; it is not a FOnline runtime proof. Separately prove the actual native client bridge with a real client, server rejection and authority behavior, and listener exclusion in shipping release artifacts. ## What the Engine owns The Engine repository owns: - the UTF-8 newline-delimited JSON over TCP envelope; - the `auth`, `ping`, `status`, `observe`, `events`, and `act` methods; - the common request/result/error shapes and error codes; - bounded line, command-queue, and event-history requirements; - accepted-command sequence ids and the terminal `command_completed` event; - the loopback-first security policy; - the standard-library reference client, protocol sample, generated contract, focused tests, and compatibility-diff policy. This is a protocol and integration contract, not a listener in the FOnline core runtime. The reference server deliberately stands in for a project client loop. ## What the project owns Every embedding project owns: - whether a native listener exists and which application roles compile it; - the compile-time shipping exclusion and runtime enable setting; - observation fields, `schemaVersion`, readiness gates, entity projections, and information-redaction policy; - action names, parameter semantics, normal gameplay validation, cancellation, and failure messages; - event types beyond `command_completed`; - MCP tool names, launch recipes, endpoint registries, memory, agent policies, prompts, and reports; - native/script integration tests against the actual project client. Last Frontier and TLA both have client bridges, but their observations, action catalogs, QA commands, and MCP namespaces are different. Those surfaces are evidence for this common envelope, not Engine behavior and not templates to copy unchanged. ## Architecture A normal integration has four ownership zones: 1. A project native extension owns a bounded TCP listener, connection-local authorization, envelope parsing, and thread-safe queues. 2. The project client loop publishes immutable observation snapshots and drains accepted commands at a safe script/native lifecycle point. 3. Project action handlers call ordinary client behavior or authenticated game RPCs. The game server remains authoritative and retains server authority. 4. An optional adapter turns project observations and actions into semantic MCP tools. It does not alter the wire contract. The listener thread must never retain or mutate script objects, entities, GUI nodes, or engine state. Pass plain copied values through a bounded queue. Read [NativeExtensions.md](../../NativeExtensions.md) for extension roles and lifecycle, [Script Lifecycle and Concurrency](scripting/lifecycle-and-concurrency.md) for script thread ownership, and [Nullability.md](../contributing/coding-contracts/nullability.md) for native/script handles. ## Wire protocol The bridge is a sequential request/response protocol over a TCP byte stream. Each message is one UTF-8 JSON object followed by LF. The JSON payload before LF must not exceed 1 MiB. A client sends one request and consumes the matching response before reusing the connection; multiplexing is not part of the contract. The envelope is JSON-RPC-shaped and uses `jsonrpc: "2.0"`, but the bridge does not claim complete JSON-RPC 2.0 behavior such as notifications, batches, discovery, or every standard validation rule. Request: ```json {"jsonrpc":"2.0","id":1,"method":"observe","params":{}} ``` Successful response: ```json {"jsonrpc":"2.0","id":1,"result":{"observationSeq":4,"observation":{"schemaVersion":1}}} ``` Error response: ```json {"jsonrpc":"2.0","id":1,"error":{"code":-32001,"message":"Unauthorized"}} ``` The response must echo the request id and contain exactly one of `result` or `error`. The generated [wire reference](../reference/ai-control-protocol/wire.md) owns the error-code table and exact framing rules. ### Authorization Authorization is connection-local. When a token is configured, `auth` is the only method accepted before successful authentication. A wrong token returns `{"authorized":false}` and leaves the same connection unauthorized; a later attempt may succeed. Reconnecting always starts a new authorization state. An empty token may be convenient for a loopback-only development listener. It must never authorize a listener bound beyond loopback. ### Methods The six protocol methods are intentionally small: | Method | Purpose | |--------|---------| | `auth` | Authorize the current connection with a shared token. | | `ping` | Prove transport liveness, not game readiness. | | `status` | Inspect listener state, bounded queue/history occupancy, observation sequence, and last bridge error. | | `observe` | Read the latest complete project-owned observation. | | `events` | Poll retained transient events after an exclusive sequence cursor. | | `act` | Queue one project-owned command and return its command sequence. | Use the generated [method reference](../reference/ai-control-protocol/methods.md) for exact parameter and result shapes. ### Observations and events `observe` returns an envelope around one project object: ```json { "observationSeq": 4, "observation": { "schemaVersion": 1, "ready": true, "availableActions": ["move"] } } ``` `observationSeq` changes when the published snapshot is replaced. It is not an event cursor. The project must publish a complete, internally consistent copy; clients should never need to combine fields from two snapshots. `events` uses `afterSeq` as an exclusive cursor and returns retained records in ascending order. The response also returns `latestSeq`, even when no retained event is newer. Event history is bounded: a slow adapter can miss old events and must resynchronize from `observe` instead of assuming an infinite log. ## Command lifecycle An `act` request has a required non-empty project command `type`. The protocol also standardizes optional convenience slots named `targetId`, `itemId`, `auxId`, `x`, `y`, `screenX`, `screenY`, `intArg`, `stringArg`, and `append`. Their units and meaning remain project-owned; a project may use an action-specific nested schema instead of forcing every operation through these slots. Queue acceptance is only the first phase: ```json {"jsonrpc":"2.0","id":7,"result":{"accepted":true,"commandSeq":12}} ``` After the owning client loop runs the command, it appends a correlated terminal event: ```json { "seq": 38, "event": { "type": "command_completed", "commandSeq": 12, "success": true, "message": "moved" } } ``` Every accepted command must eventually complete, including unknown, game-rejected, cancelled, and teardown-interrupted commands. Keep completion messages stable enough for diagnostics, but use explicit project event fields for behavior that an adapter must branch on. If the command queue is full, `act` returns `-32002`; it must not overwrite or silently drop a queued command. The adapter should wait for progress, refresh status/events, and retry only when retrying is safe for that project action. ## Security boundary An AiControl listener is a remote-control surface even when its intended caller is a local test process. Apply all of these rules: - Keep the feature disabled by default. - Remote operation requires a non-empty token. - Bind loopback by default. Require an explicit operator opt-in and a non-empty token before binding any non-loopback address. - Treat the token and payload as plaintext. This protocol has no TLS, replay protection, user identity, or authorization scopes. Prefer loopback; otherwise provide an independently authenticated encrypted tunnel. - Read tokens from a secret provider or environment variable. Do not put them in command lines, committed configs, logs, screenshots, fixtures, or reports. - Compile the listener and remote-command path out of production clients. A runtime setting alone leaves the security and antivirus heuristic surface in the binary. - Bound line size, pending commands, retained events, observation size, and observed entity counts. - Expose player-equivalent actions by default. Keep administrator/setup tools in a separate explicit project policy and record their use in QA evidence. - Redact secrets, hidden server state, other players' private state, and data a normal client should not know. The shared token is a minimal local-development gate, not a general security system. Do not publish this TCP endpoint directly on a LAN or the internet. ## Native project integration Use a project native extension when a real FOnline client needs the bridge: 1. Add a project CMake option that gates the complete listener implementation and defaults according to the project's development policy. Force it off in every release/package workflow. 2. Store only plain copied command, event, status, and observation values in the native bridge. Give all shared containers explicit locks and positive caps. 3. Start after the owning client/script module is ready. Refuse unsafe host, port, token, or capacity settings before creating the listener thread. 4. Parse and enqueue on the listener thread. Pull commands and publish snapshots from the client loop through narrow exported methods. 5. On teardown, stop accepting, close the active socket, wake the thread, join it, fail any accepted unfinished commands, unregister callbacks, and release project state before the client engine disappears. 6. Keep logs low-volume and secret-free. Report protocol failures through `status.lastError` and project diagnostics without copying arbitrary payloads. One active client connection is sufficient for the reference contract. A project may support more, but it then owns authorization isolation, event cursors, write serialization, observation fan-out, and load limits. Clients must not depend on that extension. The Engine does not currently provide a native listener helper. Promoting one would require a core runtime owner, cross-platform socket tests, shutdown and thread-safety proof, a stable configuration surface, and a separate security review. Do not describe a project `SourceExt` implementation as built-in Engine behavior. ## MCP adapter integration The reference client in `BuildTools/ai_control_client.py` is a small transport library and diagnostic CLI. An MCP adapter should build on the same rules but remain project-specific: 1. Connect to an explicit endpoint and authenticate once per connection. 2. Call `status` and `observe`; validate the project's observation `schemaVersion` before exposing tools. 3. Convert only current `availableActions` and visible entities into semantic tools. Avoid making the model synthesize raw command objects when a typed tool can validate them first. 4. Keep an independent event cursor per endpoint. Correlate every accepted command with `command_completed` and define timeouts/cancellation. 5. Separate process launch, endpoint selection, screenshots, logs, memory, and game-playing policy from the wire client. 6. Include bridge version, project schema version, Engine/project revision, and endpoint identity in reports, but never include the token. Do not publish Last Frontier's `lf_*` tools or TLA's `tla_*` tools as generic FOnline methods. A small project may expose only `observe`, `move`, and `interact`; a larger game may need dialogs, inventories, combat, parties, and project QA setup. Both can use the same envelope. ## Validation Run the project-neutral proof from the Engine root: ```powershell python Examples\AiControlSample\run_protocol_smoke.py python BuildTools\tests\test_ai_control_protocol.py python BuildTools\tests\test_docs_ai_control_protocol.py python BuildTools\docs_ai_control_protocol.py --check ``` The smoke starts an ephemeral loopback listener and proves wrong-token rejection, per-connection authorization, liveness, status fields, initial observation, invalid params, unknown methods, action acceptance, asynchronous completion, updated observation, and exclusive event cursors. Focused malformed-peer tests also reject mismatched ids, ambiguous responses, invalid JSON, and oversized lines. This sample is **not a FOnline runtime proof**. A project integration must also: - build every native role that includes or excludes the bridge; - run a real client and prove observation publication plus client-loop command draining; - exercise representative normal gameplay acceptance and server rejection; - test reconnect, queue saturation, event rollover, shutdown during an accepted command, and process cleanup; - inspect release artifacts to prove the listener cannot start and the project remote-command implementation is absent. For longer multi-process scenarios, compose project checks with the [gameplay test harness](testing/gameplay-and-integration.md) rather than adding process control to the protocol. ## Maintenance `BuildTools/AiControlProtocol.json` is the canonical structured contract. Update it in the same change as any framing, method, error, common field, security, integration, or validation behavior. Then run: ```powershell python BuildTools\docs_ai_control_protocol.py --write python BuildTools\docs_helper_cli.py --write python BuildTools\docs_contract_diff.py --baseline-git-ref origin/master --allow-missing-baseline --write --enforce python BuildTools\docs_public_api.py --write ``` The AiControl protocol is the eighteenth generated compatibility domain. A baseline-public removal, restriction, or stability withdrawal requires an entry in `Docs/contract-change-dispositions.json` under [Generated Contract Change Management](../contributing/contract-change-management.md). Project observation/action schema changes belong in project documentation and project tests unless the shared envelope itself changes. When Last Frontier, TLA, or another maintained project changes its bridge, re-audit the complete incoming project range. Promote only behavior supported by at least one independent Engine artifact and review; keep game schemas, MCP tool names, and QA policy in their owning projects. ===== END DOCUMENT ai-control-protocol-guide ===== ===== BEGIN DOCUMENT native-extensions-guide ===== Source: Docs/en/how-to/native-extensions.md Canonical URL: https://fonline.ru/Docs/en/how-to/native-extensions.html Content SHA-256: d47ba2f16bb6246de9ab1a3587eca03692e57105a62cd651af38df7a5f5cdac9 --- layout: default title: Native Extensions document_id: native-extensions-guide locale: en permalink: /Docs/en/how-to/native-extensions.html --- # Native Extensions Native extensions let an embedding project add C++ code to FOnline without moving game-specific behavior into the reusable engine repository. They are compiled from source as part of the same build, scanned by the same metadata/codegen pipeline, and linked into the selected engine role libraries. Use this guide for architecture, authoring, and validation. Use the generated [role reference](../reference/native-extension/roles.md), [hook reference](../reference/native-extension/hooks.md), [binding rules](../reference/native-extension/bindings.md), and [canonical JSON model](../../generated/native-extension.json) for exact current declarations. ## Contract status The native-extension interface is `experimental` and revision-pinned. The engine documents source composition and generated binding behavior for a pinned revision; it does not promise binary compatibility between an extension compiled against one revision and runtime libraries from another. The engine owns: - `AddEngineSources` role routing and source discovery; - metadata/codegen participation for every registered source; - supported engine hooks and generated fallbacks; - engine namespace, pointer, nullability, and script-export conventions; - core library/link relationships for the five roles. The embedding project owns: - extension implementation and state; - third-party libraries, include paths, compile definitions, and platform availability; - settings, persistence, database migrations, credentials, and external services; - package payloads, signing, deployment, and release compatibility policy; - project tests and documentation for player-visible behavior. `FO_NATIVE_SCRIPTING` selects a scripting backend. It is not the switch for project-native extensions and must not be used as an extension-availability test. ## Source paths inspected - `BuildTools/NativeExtensionInterface.json` - `BuildTools/Init.cmake` - `BuildTools/cmake/ProjectInterface.json` - `BuildTools/cmake/helpers/Build.cmake` - `BuildTools/cmake/helpers/Options.cmake` - `BuildTools/cmake/stages/EngineSources.cmake` - `BuildTools/cmake/stages/Codegen.cmake` - `BuildTools/cmake/stages/CoreLibs.cmake` - `BuildTools/codegen.py` - engine hook call sites under `Source/Applications/`, `Source/Frontend/`, `Source/Common/`, `Source/Client/`, `Source/Server/`, and `Source/Tools/` - `Examples/MinimalProject/CMakeLists.txt` - `Examples/MinimalProject/StarterServerExtension.cpp` - `Examples/NativeExtensionSample/CMakeLists.txt` - `Examples/NativeExtensionSample/SourceExt/ServerExtension.cpp` ## Build composition Register project sources after the ThirdParty stage and before the EngineSources entrypoint: ```cmake StartProjectGeneration() RegisterProjectOptions() AddThirdPartyLibraries() AddEngineSources( COMMON SourceExt/CommonExtension.cpp SERVER SourceExt/ServerExtension.cpp CLIENT SourceExt/ClientExtension.cpp MAPPER SourceExt/MapperExtension.cpp BAKER SourceExt/BakerExtension.cpp TESTS SourceExt/Test_ProjectExtension.cpp) RegisterEngineSources() SetupCodeGeneration() ``` Paths and globs resolve against the embedding project's contribution root. Arguments are role/path pairs, and an odd argument count is a configure-time error. The current helper does not reject an unknown role token: it creates an `FO__SOURCE` list and still adds the file to metadata inputs, but no Engine target consumes that source list unless the role is one of the six documented below. There are no consumed `EDITOR`, `ANIMATION_VIEWER`, or `PARTICLE_VIEWER` source roles. Mapper, both focused viewers, Baker, and ASCompiler link `BakerLib`, so reusable authoring/baking support normally belongs in `BAKER`; code that is truly shared by all applications belongs in `COMMON`. Project-native Catch2 translation units belong in `TESTS`, which compiles them directly into enabled unit-test and coverage executables without adding them to runtime libraries. Every resolved file is appended to both its role source list and `FO_SOURCE_META_FILES`. A header registered as `COMMON` is also added to generated common-header inputs. Register only files intended for codegen inspection: a vendored source tree belongs in a dedicated library target, not in a broad extension glob. ## Role selection Choose the narrowest role that owns the behavior: | Need | Role | Consequence | |---|---|---| | Process-wide/config/application behavior used by several applications | `COMMON` | Compiles into `CommonLib`; avoid client/server-only dependencies. | | Authority, persistence, server networking, server script methods | `SERVER` | Compiles into `ServerLib`; unavailable to client scripts and binaries. | | Rendering/input/client networking/client script methods | `CLIENT` | Compiles into `ClientLib`; mapper also receives client registrations through `ClientLib`. | | Mapper-only automation or mapper script methods | `MAPPER` | Compiles into `MapperLib`. | | Custom resource bakers and authoring support shared by Mapper/viewers/ASCompiler | `BAKER` | Compiles into `BakerLib`; `BAKER` is not a script export target. | | Project-native Catch2 regression translation units | `TESTS` | Compiles directly into enabled unit-test and coverage executables; it has no runtime or script export target. | Do not use `COMMON` merely to make a missing symbol link. Move the dependency to its owning role or split a small common interface from role-specific implementations. ## Project-Owned Game-System Formats Native extensions can implement complete game-system formats without making those formats Engine features. A typical project-owned authored system may use: - `COMMON` for a parser, shared records, exported script types, or a registry; - `BAKER` for syntax checks, metadata-aware validation, and generated resources; - `SERVER`, `CLIENT`, or project scripts for authoritative runtime behavior; - a project editor, audit tool, fixtures, and gameplay tests for authoring and behavioral proof. Registration through `AddEngineSources` grants build, metadata/codegen, and link integration. It does not transfer API, format, compatibility, security, or documentation ownership to FOnline. The embedding project must name the parser, baker, generated outputs, runtime consumers, validation layers, and migration policy in its own documentation. Do not add an Engine format guide for such a system until the reusable implementation and tests are present in this repository. If several games need the same system but it is not suitable for Engine core, publish a revisioned companion repository with an exact Engine compatibility range and its own minimal example. ## Includes and namespaces Start with `Common.h`, then include the smallest role header needed by the implementation: ```cpp #include "Common.h" #include "Server.h" FO_USING_NAMESPACE(); FO_BEGIN_NAMESPACE ///@ ExportMethod FO_SCRIPT_API int32_t Server_Game_ProjectValue(ptr server); FO_END_NAMESPACE int32_t FO_NAMESPACE Server_Game_ProjectValue(ptr server) { ignore_unused(server); return 1; } ``` Metadata declarations must compile with the engine namespace enabled or disabled. Keep declarations inside `FO_BEGIN_NAMESPACE` / `FO_END_NAMESPACE`, and qualify definitions with `FO_NAMESPACE`. `FO_SCRIPT_API` exports are codegen frontiers and intentionally do not start with `FO_TRACE_ZONE(Script)`. Ordinary non-exported project C++ functions keep the normal engine stack-trace convention. ## Script exports and metadata Registered extension files are parsed together with engine metadata. Project code can use supported `///@` declarations such as `ExportMethod`, `ExportEvent`, `ExportRefType`, `ExportSettings`, and `EngineHook`, subject to the same parser and nullability rules as engine declarations. The CMake source role and metadata target are related but not interchangeable: - a `SERVER` file normally declares `Server_*` exports; - a `CLIENT` file normally declares `Client_*` exports and those registrations are also available to mapper builds; - a `MAPPER` file declares mapper-only exports; - `COMMON` exports are registered on every applicable side; - `BAKER` can implement baker hooks but is not an `ExportMethod` target. Use `ptr` / `nptr` for engine handle borrows. Bare raw handle pointers are rejected by codegen. Keep argument defaults, nullability, ownership, and runtime side aligned with the generated script declaration. Rebuild and rebake after any native metadata change; do not copy generated registration files between engine revisions. Project remote calls remain project-authored script metadata and use the baked catalog described in [Remote Calls](../reference/scripting/remote-calls.md). They are not native-extension symbols. ## Engine hooks Hooks are optional named C++ entry points. Declare a hook with `///@ EngineHook` and the exact signature from the generated [hook reference](../reference/native-extension/hooks.md), in a file registered under its owning role. Codegen sees the declaration and omits that hook's fallback from `GenericCode-Common.gen.cpp`. If a project does not declare a hook, codegen emits its documented fallback. Most hook presence participates in generated compatibility state; `ApplicationShutdownHook` is the current exception. Hook names are closed: an unknown name is a codegen error. Implement each hook exactly once. Multiple declarations with one generated fallback decision can produce duplicate definitions or unresolved symbols. Keep the declaration adjacent to the implementation source and do not place ordinary comments between a `///@` tag and its declaration. Hook bodies run at lifecycle or policy boundaries. They must preserve engine invariants and follow the owning subsystem's exception contract. In particular: - shutdown hooks are called through guarded shutdown paths and should release resources without throwing; - visibility hooks execute on authoritative server paths and must not introduce unsynchronized mutable global state; - configuration hooks run while parsing settings and must return the documented changed/not-changed signal; - baker setup should append only requested project bakers and retain shared baking context ownership correctly. ## State and lifetime Prefer state owned by the engine instance or a project manager reachable from it. Client/server extension data can be attached through the engine's user-data ownership slot with an engine owning pointer and an explicit deleter. This keeps parallel test instances isolated and gives shutdown a deterministic owner. Do not use file-scope mutable statics for per-engine registries, sessions, caches, or extension state. Multiple engine instances can run in one process. A process-wide service is acceptable only when its semantics are genuinely process-wide, lifecycle hooks own initialization/shutdown, and tests can isolate or disable it. A project AiControl listener is a representative lifetime-sensitive extension: the socket thread may parse and copy plain values, but the owning client loop must publish observations and drain commands. Stop accepting, close sockets, wake and join the thread, fail accepted unfinished commands, and unregister callbacks before engine-instance state is released. The reusable envelope and security policy are documented in [AiControl Protocol](ai-control-protocol.md); the game's observations, actions, MCP tools, compile gate, and runtime tests stay project-owned. Use the engine pointer vocabulary: - `ptr` / `nptr` for borrowed engine objects; - `unique_*` / `refcount_*` and engine `shared_ptr` helpers for ownership; - raw pointers only at documented OS/SDK ABI boundaries, wrapped immediately on entry. ## Dependencies and platforms `AddEngineSources` does not infer dependencies. The embedding project must add libraries, include directories, compile definitions, generated headers, and platform frameworks before core libraries/applications are built. Keep third-party source in its own target and route it only through the current revision's narrowest consumed `FO_*_LIBS` list; [Project-Local Dependencies](native-extensions/project-dependencies.md) owns the complete selection, provenance, CMake, ABI, package, and update workflow. Platform-specific extensions need an explicit availability contract: 1. gate the real implementation with engine/project platform macros; 2. provide a compile-safe unsupported stub when a common script/native symbol must still exist; 3. reject unsupported runtime use clearly instead of silently emulating success; 4. keep package payloads and external runtime libraries synchronized with the compiled feature; 5. validate at least one enabled and one disabled build path. Never put credentials, API keys, signing material, or private service URLs into extension source, generated metadata, examples, logs, or documentation. ## Testing strategy Use the smallest route that proves the affected boundary: - CMake registration/role changes: `cmake -P BuildTools/tests/validate_native_extension_interface.cmake`. - Hook/metadata/codegen changes: regenerate and check the native-extension/API references, then run `BakeResources` in a real embedding project. - Reusable minimal server hook: run `python validate.py` from `Examples/MinimalProject`. - Complete native lifecycle, role-link, script-export, and focused-test path: run `python validate.py` from `Examples/NativeExtensionSample`. - Script export: add a script compile/bake assertion and a focused runtime test that calls the generated method on the correct side. - Client-visible extension: build/run a real standalone client path; a server-only or headless smoke cannot prove rendering, input, dynamic-library, or package behavior. - External SDK/platform bridge: exercise enabled, disabled, and packaged-runtime paths on the owning platform. The engine-owned minimal project is the normative starter. `Examples/NativeExtensionSample` is the focused complete native path: it keeps per-server state in `ServerEngine.UserData`, routes a small library through the current `FO_SERVER_LIBS` integration state, exports one server method, and runs both a native unit and runtime smoke check. A large game project is valuable integration evidence but does not define the reusable contract. ## Updating the engine revision Treat an Engine gitlink change as an extension compatibility event: 1. compare the old/new [canonical native-extension models](../../generated/native-extension.json) through [Generated Contract Change Management](../contributing/contract-change-management.md); 2. inspect hook signature/default/call-site, role/library, pointer/nullability, generated metadata, and compatibility-marker changes; 3. reconfigure so role validation and source globs are reevaluated; 4. rebuild every affected native role and rebake project metadata/resources; 5. update project extension docs/tests and migration/release notes where behavior changed; 6. never reuse native binaries or generated registration files from the previous Engine revision. ## Validation checklist 1. Every source is registered under the narrowest valid role before `RegisterEngineSources()`. 2. Every metadata declaration has the correct target, namespace macros, pointer/nullability vocabulary, and exact hook signature where applicable. 3. Per-engine state has an instance owner; process globals have an explicit process-wide lifecycle justification. 4. Dependencies, platform guards, disabled stubs, and package payloads match the compiled feature. 5. Generated API/native-extension references and the aggregate contract diff are current. 6. Structural CMake, codegen/bake, focused native/script tests, and the smallest real runtime path pass without warnings. 7. Project documentation records any settings, persistence, service, security, or release behavior that the engine guide intentionally excludes. ## See also - [Embedding Project](build/embedding-project.md) - engine/game repository ownership. - [BuildTools Pipeline](../reference/cmake-and-buildtools/pipeline.md) - stage and library composition. - [Generated API and Metadata](../reference/metadata/index.md) - metadata and generated script API. - [Smart Pointers](../contributing/coding-contracts/smart-pointers.md) - native pointer vocabulary. - [Nullability](../contributing/coding-contracts/nullability.md) - native/script nullability boundary. - [Exception Safety](../contributing/coding-contracts/exception-safety.md) - lifecycle and mutation exception contracts. - [Project-Local Dependencies](native-extensions/project-dependencies.md) - project-local library/SDK ownership, role linking, platform/package delivery, and maintenance. - [AiControl Protocol](ai-control-protocol.md) - project-neutral AI-control envelope, loopback threat boundary, command lifecycle, and native/MCP ownership split. - [ThirdParty Maintenance](../contributing/third-party/index.md) - engine-vendored dependency policy. ===== END DOCUMENT native-extensions-guide ===== ===== BEGIN DOCUMENT project-local-dependencies ===== Source: Docs/en/how-to/native-extensions/project-dependencies.md Canonical URL: https://fonline.ru/Docs/en/how-to/native-extensions/project-dependencies.html Content SHA-256: 2062a10908b13e067c7fb83accd407bcd6167a371556b32fe1be35c80c994d6b --- layout: default title: Project-Local Dependencies document_id: project-local-dependencies locale: en permalink: /Docs/en/how-to/native-extensions/project-dependencies.html --- # Project-Local Dependencies This guide owns the reusable contract for libraries, SDKs, frameworks, tools, and runtime payloads added by a game repository that embeds FOnline. It covers dependencies that the game needs but the reusable Engine does not own. Use [ThirdParty Maintenance](../../contributing/third-party/index.md) for source vendored in `Engine/ThirdParty/`. Use [Native Extensions](../native-extensions.md) for the C++ bridge that consumes a project dependency. The embedding project must keep its exact inventory, product-specific integrations, credentials, providers, and release policy in its own repository. ## Dependency decision Use this sequence for every project-local dependency: 1. Classify the owner first: Engine, embedding project, revisioned companion, project build tool, or operating-system prerequisite. Registration through Engine helpers does not transfer project ownership. 2. Select and pin the delivery model, then record version, provenance, integrity, license, supported platforms/toolchains, update path, and rollback pin in the project. 3. Create the project target after Engine third-party targets exist and before `BuildCoreLibraries()` consumes its revision-pinned library lists. Append it only to the required `FO_COMMON_LIBS`, `FO_SERVER_LIBS`, `FO_CLIENT_LIBS`, `FO_BAKER_LIBS`, or `FO_TESTING_LIBS` list. 4. Distinguish requested, compiled, and initialized-at-runtime states. Keep allocator ownership, exceptions, CRT/toolchain, architecture, generated headers, and C ABI boundaries explicit rather than treating header presence as runtime support. 5. Treat a development copy and a release payload separately. Declare runtime files through package declarations, including target paths, notices, runtime file hashes, and signatures or signing ownership. Then start and probe the packaged artifact from an isolated directory on every claimed platform. A complete release-delivery record names each evidence class separately: package declarations, licenses and notices, runtime-file hashes, signatures or signing ownership, and an isolated start of the packaged artifact. One of these is not shorthand for the others. Do not stop at a successful include or link. Acceptance must prove the intended requested, compiled, and initialized states; shared-library ABI and allocator ownership; package payload and integrity; and isolated runtime behavior. ## Contract Status The project-facing CMake interface is `experimental` and revision-pinned. The current Engine exposes no declared helper for role-scoped project-library registration. Embedding projects append targets to the current library lists, which are implementation state and may change with the Engine revision. Pin the Engine, re-audit the lists, and rebuild dependencies and extensions together after every pin change. The Engine owns: - the `COMMON`, `SERVER`, `CLIENT`, `BAKER`, and `TESTS` library lists; - the core-library targets that consume those lists; - the core-library graph that consumes each role list; - package declarations and generic package assembly mechanics; - the documented allocator, pointer, exception, and native-extension rules. The embedding project owns: - dependency selection, version, source, integrity, license, and support term; - the CMake target, feature gate, role assignment, and unsupported stub; - generated headers, build tools, platform prerequisites, and ABI compatibility; - runtime libraries, data files, notices, signing, and package acceptance; - vulnerability response, update cadence, rollback, tests, and release evidence. Registration never transfers ownership to the Engine. A library becomes an Engine dependency only through an explicit Engine change that moves the implementation, tests, maintenance record, and supported-platform obligation. ## Choose The Owner First Classify a dependency before adding files or CMake: | Need | Owner and location | Rule | | --- | --- | --- | | Reusable Engine runtime, format, renderer, or tool capability | Engine, normally `Engine/ThirdParty//` | Follow the Engine vendoring and public-contract review. | | Game-only native bridge, service client, proprietary SDK, or content runtime | Embedding project, commonly `SourceExt//` or `Dependencies//` | Keep its implementation, policy, and release evidence project-owned. | | Reusable but optional integration that should not be Engine core | Revisioned companion repository | Publish an exact Engine compatibility range, its own tests, and a minimal embedding example. | | Build-time generator or audit not linked into Engine roles | Project tooling tree | Pin its runtime/packages separately and do not add it to an Engine role. | | Operating-system framework or host library | Project platform configuration | Name the supported hosts and fail configure when a required prerequisite is absent. | Do not duplicate an Engine dependency in the project merely to reach its headers or target. If a project deliberately needs a different build or version, document symbol isolation, allocator/ABI boundaries, platform scope, and why the Engine copy cannot be reused. ## Select A Delivery Model Use the smallest model that gives deterministic builds and lawful delivery: 1. **Vendored source** is preferred when the project must build the library on all supported hosts, apply a small reviewed patch, or avoid host-version drift. Pin the upstream version and archive hash, preserve required notices, and record pruning. 2. **Imported SDK target** fits a proprietary or prebuilt SDK. Pin the SDK release, architecture, compiler/runtime compatibility, acquisition source, redistributable files, and license terms. Do not commit material that the license forbids distributing. 3. **System or platform library** fits an OS API or a deliberately supported host prerequisite. Keep the use behind explicit platform checks and prove the minimum supported host. A successful developer-machine lookup is not a portable dependency contract. 4. **Package-manager or fetched source** is acceptable only with an immutable version/commit and integrity lock. Release and CI builds must not silently select a newer package or depend on an unreviewed network response. 5. **Runtime-only payload** fits a shared library, helper executable, model, or data file that is not compiled. It still needs a version, provenance, platform/architecture mapping, license decision, package rule, and launch acceptance test. Do not use an unpinned branch, floating package range, ambient include path, or unregistered `find_package()` result as production input. ## Keep A Dependency Record Every project-local dependency should have one authoritative record in the embedding repository. It may be a table, manifest, or dependency-owned README, but it must answer: | Field | Required content | | --- | --- | | Identity | Upstream name, project target name, owner, and support contact. | | Version | Exact release/tag/commit and the in-source or package metadata used to verify it. | | Provenance | Official source URL or private artifact identity plus archive/commit hash. | | Delivery | Vendored, imported SDK, system, fetched, tool-only, or runtime-only. | | License | License identifier, retained files, attribution, redistribution and source-offer obligations. | | Integration | Consuming Engine roles, native bridge, feature flag, generated files, and allocator hook. | | Support | Platforms, architectures, toolchains, configurations, and unsupported behavior. | | Package | Runtime files, target paths, signing owner, and acceptance probe. | | Security | Advisory source, review cadence, secret boundary, and emergency disable/remove path. | | Update | Local patches, pruning record, compatibility-coupled assets/data, tests, and rollback pin. | The dependency source is authoritative for the version when it exposes one. Keep human inventories synchronized with that value; do not make a prose-only version string the build's source of truth. ## Integrate At The Project Boundary Create project dependency targets after the Engine ThirdParty stage has created reusable Engine targets and before `BuildCoreLibraries()` consumes the role lists. Register project source before `RegisterEngineSources()` as usual: ```cmake StartProjectGeneration() RegisterProjectOptions() # Register handlers needed by Engine or project third-party CMake before the # ThirdParty stage installs its find_package() interceptor. RegisterFindPackageHandler(OptionalBackend NotFoundFindPackage) AddThirdPartyLibraries() add_subdirectory(Dependencies/ProjectCodec EXCLUDE_FROM_ALL) # A project wrapper gives one stable target for upstream target-name changes, # include classification, compile definitions, and transitive requirements. add_library(ProjectCodec INTERFACE) target_link_libraries(ProjectCodec INTERFACE upstream_codec) target_include_directories(ProjectCodec SYSTEM INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/Dependencies/ProjectCodec/include") list(APPEND FO_CLIENT_LIBS ProjectCodec) list(APPEND FO_BAKER_LIBS ProjectCodec) AddEngineSources(CLIENT SourceExt/ProjectCodecBridge.cpp) RegisterEngineSources() SetupCodeGeneration() BuildCoreLibraries() ``` The current revision consumes `FO_COMMON_LIBS`, `FO_SERVER_LIBS`, `FO_CLIENT_LIBS`, and `FO_BAKER_LIBS` when creating the corresponding core libraries; `FO_TESTING_LIBS` feeds native test targets. Append before `BuildCoreLibraries()`, avoid duplicates yourself, and fail project configure for unsupported combinations. There is no dedicated mapper-only library list: `MapperLib` consumes `ClientLib`, so a client dependency reaches Mapper with the wider client role. A truly mapper-only dependency requires an explicit Engine interface change instead of an invented `FO_MAPPER_LIBS` variable. These lists are revision-pinned integration state, not declared helpers in `BuildTools/cmake/ProjectInterface.json`. Re-check `State.cmake` and `CoreLibs.cmake` on every Engine update. A helper's presence under `BuildTools/cmake` would not make it public unless the interface manifest also declared it. ## Route To The Narrowest Role Dependency roles follow the native source roles: | Role | Link owner | Typical consumers | Use for | | --- | --- | --- | --- | | `COMMON` | `CommonLib` | Every enabled runtime/tool role | A genuinely common process/config primitive. Avoid placing a client or server SDK here for convenience. | | `SERVER` | `ServerLib` | Server and native tests that include it | Authority, persistence, server transport, or backend SDK code. | | `CLIENT` | `ClientLib` | Client plus current server-controller, Mapper, viewer, Baker, ASCompiler, and test paths | Rendering, input, client transport, or client SDK code. Account for the wider current consumer graph. | | `BAKER` | `BakerLib` | Baker, Mapper, viewers, ASCompiler, and tests | Resource import, validation, conversion, or authoring support. | | `TESTS` | Native test targets | Engine-owned native tests | Focused test-only support; do not rely on it for runtime delivery. | Split a dependency wrapper when different roles need different headers, features, or runtime payloads. Do not route a library through `COMMON` just to repair a missing symbol. ## Control Package Discovery The ThirdParty stage intercepts `find_package()` so nested third-party CMake cannot silently use arbitrary host libraries. Register every expected package name before `AddThirdPartyLibraries()`: - map a package to a vendored/imported target in a project handler; - use `NotFoundFindPackage` for a supported optional backend that must remain disabled; - use `PassThroughFindPackage` only for an intentional host prerequisite whose installation and minimum version are documented; - let an unregistered lookup fail configure and then make the ownership decision explicitly. Do not disable the interceptor or add a broad fallback. A dependency's nested optional probes are part of its supply-chain and support surface. ## Isolate Headers, Warnings, And Generated Files Expose dependency headers through its target, preferably with `target_include_directories(... SYSTEM ...)`, instead of a repository-wide include path. Keep first-party bridge headers non-system so project warnings remain errors. Compile definitions and generated headers belong on the narrow wrapper target that needs them. Ensure generators run before consuming targets, emit into the build tree, and participate in clean builds. Do not register a vendored source tree with `AddEngineSources`: every registered file enters metadata/codegen inspection, which is intended for project extension declarations, not arbitrary third-party code. If warning-clean upstream source is impractical, keep any warning adjustment on the third-party target. Do not lower warnings globally or suppress diagnostics in the project bridge. ## Define The Platform Contract For every optional or platform-specific dependency: 1. expose a project feature option with a deterministic default; 2. check platform, architecture, headers, import/static library, and runtime payload at configure time; 3. define one availability macro from the final result; 4. compile a no-dependency stub when a shared script/native symbol must remain; 5. make unsupported runtime use explicit rather than returning false success; 6. validate at least one enabled and one disabled build; 7. keep the package matrix aligned with the compiled availability. Separate three states: requested, compiled, and initialized at runtime. The presence of headers does not prove that the matching runtime library loads, credentials are provisioned, or the external service is reachable. ## Package Runtime Payloads Static source dependencies may add no runtime file, but they can still add license obligations. Shared/imported SDKs usually require a platform- and architecture-specific library beside the application. Tools may require helper executables or data packs. Use the package declarations described in [Packaging and Release](../release/packaging.md) to include each required file and notice. A development post-build copy helps local launching but is not a release package rule. Package acceptance must start the artifact from an isolated directory and exercise the feature far enough to detect a missing or wrong-architecture payload. Record runtime file hashes in release provenance. Apply signing/notarization at the release-owned boundary and verify signatures after package assembly. Never place private SDK credentials, service tokens, signing keys, or license-server secrets in source, CMake cache defaults, generated metadata, examples, logs, or documentation. ## Respect ABI, Allocation, And Lifetime Build source dependencies with a compatible compiler, architecture, C/C++ runtime, exception, RTTI, and configuration policy. For a prebuilt SDK, use only the vendor-supported combination and fail configure for unsupported combinations. Do not transfer ownership of Engine containers, strings, exceptions, or owning pointers across an undocumented shared-library ABI. Convert at the bridge, keep allocator ownership on the side that allocated the object, and expose a small SDK-native or C ABI where possible. Inspect allocator hooks in the dependency implementation, not only its public declaration. If the library can use Engine allocation, wire a lifecycle-correct hook and test allocate/reallocate/free symmetry, aligned allocation, and shutdown order. Otherwise record that it uses a separate heap and keep its objects out of Engine ownership/statistics assumptions. Follow [Essentials](../../reference/native/essentials.md#third-party-allocators) and [Smart Pointers](../../contributing/coding-contracts/smart-pointers.md) at the bridge. Global SDK state must have explicit process-wide semantics. Per-client, per-server, or per-test-instance state belongs to an Engine/project instance; initialize and shut it down through the owning lifecycle path, including failed partial initialization. ## Review License And Supply-Chain Risk Before first use and every update: - obtain the release from the official upstream or approved private source; - verify the pinned commit/archive and stored hash; - review release notes, supported toolchains, license changes, and security advisories; - preserve licenses, notices, attribution, changelog, and source-offer material required by the distribution model; - inventory local patches and removed files; - scan package output for accidental source archives, credentials, debug-only helpers, and unapproved runtime files; - record an emergency disable, downgrade, or removal route. The Engine does not determine whether a dependency license is compatible with a game's commercial or distribution model. That is a project release/legal gate, not a successful-build inference. ## Update Workflow 1. Record the old/new dependency identity and current Engine/project revisions. 2. Stage the candidate outside the authored tree and verify provenance. 3. Review license, advisories, API/ABI changes, build requirements, and platform support before replacing files. 4. Reapply documented pruning and the smallest possible local patches. Mark project-local edits consistently in the project policy. 5. Update the authoritative version record, integrity hash, notices, feature gates, package rules, compatibility-coupled assets/data, and owning docs. 6. Reconfigure every affected platform lane so cached discovery cannot hide a missing prerequisite or stale target. 7. Build every consuming Engine role and run focused bridge tests. 8. Assemble an isolated package, verify runtime payload hashes/signatures, and exercise enabled and disabled behavior. 9. Record failures, evidence, and the rollback pin. Keep the previous approved artifact available until acceptance is complete. If an Engine update and dependency update happen together, audit them as two compatibility ranges. Do not attribute a passing final build to either change without a narrow check or bisectable evidence. ## Validation Matrix At minimum, capture: | Boundary | Required proof | | --- | --- | | Interface | `cmake -P BuildTools/tests/validate_project_interface.cmake` and current generated CMake reference. | | Configure | Clean configure for every affected platform/architecture and for required enabled/disabled feature states. | | Compile/link | Every core/test list changed by the project; warning-clean first-party bridge and target-scoped dependency policy. | | Codegen/script | Regenerated metadata plus resource bake when a native declaration or generated header changes. | | Runtime | Focused success, unavailable, initialization-failure, and shutdown paths. | | Package | Isolated launch, runtime payload presence/hash/architecture, notices, and secret-free artifact scan. | | Upgrade | Old/new Engine pin comparison, dependency compatibility review, and no reused native/generated binary from the old pin. | The Engine-owned minimal project compiles an `INTERFACE` project dependency by appending it to `FO_SERVER_LIBS`; its server extension fails compilation if the usage requirement is absent. This proves the current revision-pinned link path without pretending it is a stable helper or making an external SDK part of the fixture. ## Failure Routing | Symptom | Inspect first | | --- | --- | | Library does not reach its consumer | Selected `FO_*_LIBS` list, current `CoreLibs.cmake`, and stage order. | | Header found locally but not in CI | Wrapper target include scope and accidental ambient include paths. | | Unregistered `find_package()` failure | Nested dependency probe and the explicit handler decision. | | Extension compiles but another role fails to link | Narrow role assignment, transitive target requirements, and actual core-library consumer graph. | | Runtime library missing or wrong architecture | Package declaration, imported target location, post-build/package distinction, and artifact matrix. | | Crash during allocation or shutdown | Allocator/free pairing, ABI ownership, global-state lifetime, and partial-init cleanup. | | Feature says available but cannot initialize | Requested/compiled/runtime state separation and credential/service provisioning. | | Update passes compile but assets fail | Version-coupled authored data, generator/runtime format, and real runtime validation. | ## Source Paths Inspected - `BuildTools/Init.cmake` - `BuildTools/cmake/ProjectInterface.json` - `BuildTools/cmake/helpers/Build.cmake` - `BuildTools/cmake/helpers/State.cmake` - `BuildTools/cmake/stages/ThirdParty.cmake` - `BuildTools/cmake/stages/CoreLibs.cmake` - `BuildTools/cmake/stages/Packages.cmake` - `Examples/MinimalProject/CMakeLists.txt` - `Examples/MinimalProject/StarterServerExtension.cpp` ## See Also - [Embedding Project](../build/embedding-project.md) - Engine/game repository ownership. - [Native Extensions](../native-extensions.md) - project C++ roles, hooks, metadata, state, and testing. - [ThirdParty Maintenance](../../contributing/third-party/index.md) - Engine-owned vendored source and patch workflow. - [BuildTools Pipeline](../../reference/cmake-and-buildtools/pipeline.md) - public stages and selected helper boundary. - [Packaging and Release](../release/packaging.md) - package declarations, payloads, signing, and acceptance. - [Security and Secrets](../release/security-and-secrets.md) - credentials, redaction, trust, rotation, and incident handling. - [Engine Upgrade Guide](../migration/engine-upgrade.md) - revision update and compatibility reconciliation. ===== END DOCUMENT project-local-dependencies ===== ===== BEGIN DOCUMENT prototype-format-guide ===== Source: Docs/en/how-to/content/prototype-format.md Canonical URL: https://fonline.ru/Docs/en/how-to/content/prototype-format.html Content SHA-256: c23460464687030c5f15dedc6afcc6dc26907d414091e090fd25973474a03155 --- layout: default title: Prototype Format document_id: prototype-format-guide locale: en permalink: /Docs/en/how-to/content/prototype-format.html --- # Prototype Format FOnline prototypes are metadata-backed, named property sets baked separately for the server, client, and mapper. They define reusable entity defaults and project fixed definitions; they are not runtime save records, map placement records, or a game-specific content taxonomy. Use this guide for the authoring model, inheritance, references, migrations, and validation workflow. Use the generated [syntax reference](../../reference/prototype-format/syntax.md), [built-in property catalog](../../reference/prototype-format/properties.md), [validation rules](../../reference/prototype-format/validation.md), and [canonical JSON model](../../../generated/prototype-format.json) for exact declarations at the current engine revision. ## Contract status The prototype-format surface is `experimental` and revision-pinned. The engine owns: - file selection through `Baking.ProtoFileExtensions`; - section resolution, identity, inheritance, and side-specific binary output; - metadata property lookup and strict text-value conversion; - built-in `HasProtos` entity declarations and properties; - engine migration lookup and prototype-reference validation. An embedding project owns: - additional prototype extensions and content directory layout; - project entity declarations, `FixedType` declarations, properties, enums, and script callbacks; - concrete IDs, field combinations, gameplay semantics, balance, and localization; - migrations for project content and persisted project data; - semantic validators, tests, release policy, and public examples. The generated property catalog says whether the engine parser can load a key. It does not say that assigning the key in a prototype is meaningful or safe for a particular game system. Project documentation and tests must define those semantic constraints. ## Source paths inspected - `BuildTools/PrototypeFormatInterface.json` - `Source/Common/Settings.inc` - `Source/Common/ConfigFile.cpp` - `Source/Common/Properties.h` - `Source/Common/Properties.cpp` - `Source/Common/PropertiesSerializer.cpp` - `Source/Common/EntityProperties.h` - `Source/Common/EntityProtos.cpp` - `Source/Common/ProtoManager.cpp` - `Source/Common/ScriptSystem.h` - `Source/Server/EntityManager.cpp` - `Source/Tools/Baker.cpp` - `Source/Tools/ProtoBaker.cpp` - `Source/Tools/ProtoTextBaker.cpp` - `Source/Scripting/ServerCritterScriptMethods.cpp` - `Source/Scripting/ServerItemScriptMethods.cpp` - `Source/Scripting/ServerLocationScriptMethods.cpp` - `Source/Scripting/ServerMapScriptMethods.cpp` - `Source/Tests/Test_ConfigFile.cpp` - `Source/Tests/Test_Properties.cpp` - `Source/Tests/Test_EntityProtos.cpp` - `Source/Tests/Test_ProtoManager.cpp` - `Source/Tests/Test_ProtoBaker.cpp` - `Source/Tests/Test_ServerMapOperations.cpp` ## From source file to baked prototype `ProtoBaker` receives the complete resource-pack file list and keeps files whose extension occurs in `Baking.ProtoFileExtensions`. The engine default is `fopro`; a project may add `fomap` for top-level map prototypes and other type-oriented authoring suffixes. The extension does not choose the prototype type. Each section does: - `[Proto]` resolves to entity metadata named `` and requires `HasProtos`; - `[]` resolves to metadata declared with `///@ FixedType`; - every top-level `[ProtoMap]` anchor in a map container resolves to `Map`. Nested `[$Name/Critter]` / `[$Name/Item]` sections do not enter this stage. They belong to the map baker and the separate map-format contract. The legacy `[Header]` / `[Tiles]` / `[Objects]` layout is not an accepted authoring format. The baker first collects every declaration and registers an empty prototype for every resolved type/ID. It finalizes registration before applying property text, so a property may refer to a prototype declared later or in another file of the same pack input. Source order is not a dependency mechanism. Finally, the same source set is applied to server, client, and mapper metadata and emitted as `.fopro-bin-`. ## Configuration syntax Prototype text uses the shared configuration parser: ```ini # A configuration comment [ProtoItem] $Name = BaseContainer NoBlock = true [ProtoItem] $Name = SecureContainer $Parent = BaseContainer NoBlock = false ``` Use `key = value` for replacement and `key += value` only where appending text is part of the property's documented representation. A final backslash continues a logical line only when the character before it is a space or tab; the parser trims both physical lines and joins them with one space. `#` starts a comment outside quoted/escaped content. The reusable Engine no longer defines `Item.Count`, `Item.Stackable`, partial-count move/add/destroy overloads, or a built-in stack-change event. A game that needs fungible item stacks must declare its own count/stackability properties and own merge, split, transfer, destruction, synchronization, and notification rules in project scripts. Do not put the retired fields back into `.fopro` as custom-looking keys unless the project has explicitly declared matching metadata. `ProtoBaker` interprets `$Name` and `$Parent`, and property application skips every `$`-prefixed key. `$Text ...` belongs to the separate `ProtoTextBaker` contract; do not assume that an arbitrary `$` key has meaning merely because property loading ignores it. Keys beginning with `_` are also ignored by property application and should be reserved for project tooling with an explicit project contract. Every other key must resolve to metadata. ## Identity `$Name = ` sets the ID. Without it, the source file name without its extension is used. An ID must not contain `/` or `$`; both characters are reserved for nested-section addressing and are rejected before hashing and duplicate detection. Identity is scoped by resolved type. `Item/Foo` and `Critter/Foo` can coexist, but two `Item/Foo` declarations anywhere in the same pack input fail. IDs are hashed and passed through `Proto` migration rules before duplicate detection. Prefer one primary prototype per file and keep the file name equal to the ID. This preserves the useful default and makes source search, review, migration diffs, and generated examples predictable. Use explicit `$Name` whenever a file contains multiple sections or the file name is not the intended ID. Directories and extensions are organization and discovery choices only. Never infer type or gameplay behavior from either one. ## Inheritance `$Parent = ParentA ParentB` lists space-separated parents of the same resolved type. Parents may live in other files, but they must be present in the current pack input. The baker merges: 1. ancestors depth first; 2. direct parents from left to right; 3. the child last. Later values replace earlier values. Control keys beginning with `$` are not copied as properties. Among direct parents, the rightmost parent wins where both contribute the same key, and the child wins over all parents. An ancestor reached through several paths contributes only at its first reach; `Baking.AllowRepeatedProtoParents` (default `true`) skips later reaches, while `false` rejects the inheritance diamond. `ProtoBaker` and `ProtoTextBaker` use the same walk, so properties and `$Text` cannot diverge. Keep inheritance shallow and capability-oriented. Prefer a small base that represents a stable authored concept over long visual or balance chains. Multiple inheritance is useful for orthogonal defaults, but overlapping parent fields make the result order-sensitive and should be made explicit in the child. Parent graphs must be acyclic. Both prototype bakers track the active parent path and reject self-cycles, two-node cycles, and longer cycles regardless of the repeated-parent setting. Keep project-side structural validation for faster author feedback, but baking is the authoritative rejection boundary. ## Property applicability Property loading is side-aware: The unknown-property check is unconditional and occurs before side applicability can be considered. Side-specific skipping applies only after the metadata property is known; it never turns an unknown property into a valid one. - an unknown property fails; - a property disabled on the current side fails, except that a client-only property is skipped in server output and a server-only property is skipped in client/mapper output; - a virtual property fails; - a temporary property fails; - a valid property is parsed through its metadata type. A property is temporary when it is mutable or core-owned and is not persistent. The generated [property catalog](../../reference/prototype-format/properties.md) applies this exact rule to current built-in metadata and lists the active sides. Skipping a server-only property in client records does not remove its authored strings from the client hash dictionary: `ProtoBaker` collects the server pack's strings too, including FixedType values. Property access remains server-only. A client-only rebuild still needs server metadata for this collection; it parses an already-current server pack's sources without revalidating scripts or rewriting that pack. See [Baking](../../explanation/content-pipeline/baking.md) and [networking](../../explanation/authority-and-networking/index.md#unresolved-hash-recovery) for output and receiving-pool boundaries. Project metadata is not present in the engine-only catalog. A production embedding project should generate a companion catalog from its combined engine/project metadata and publish it in project documentation. Parser applicability is only the first gate. A project should separately classify fields as: - authored defaults intended for prototypes; - runtime state that should be created or mutated by gameplay; - structural links managed by another authoring tool; - derived/cache state that must never be authored; - project-required fields and valid field combinations. ## Text values and references `PropertiesSerializer` is the value authority. It rejects malformed collections, integer overflow, invalid enum values, non-finite floating-point values, and unresolved references. Booleans accept their declared text form or numeric `0`/`1`; numeric and enum acceptance must not be inferred from loose configuration parsing. `FixedType` and prototype-reference properties resolve through metadata and migration rules. A reference must name an existing target unless the property is nullable and the authored value is empty. Do not document guessed array/dictionary punctuation for a project property. Read its generated type declaration and prove the concrete value with the baker or a focused parser test. ## Init scripts The built-in `Item`, `Critter`, `Map`, and `Location` types expose a server-side, mutable, persistent `InitScript` property. A non-empty authored value names a global function: ```text void Function(Item item, bool firstTime) void Function(Critter critter, bool firstTime) void Function(Map map, bool firstTime) void Function(Location location, bool firstTime) ``` The property's `ScriptFuncType` metadata selects the exact signature. During server baking, `BaseBaker::ValidateProperties()` resolves every non-empty callback and rejects a missing function or mismatched signature. No callback attribute is required. Delegates are not valid persisted names. `CallInit` marks the entity initialized and fires the corresponding `Game.On*Init` event before invoking `InitScript`. A callback that destroys the entity prevents the later steps. A function name that cannot be resolved at runtime is a hard `ScriptException`; the engine does not silently skip it. The initialized flag is already set, so this remains the documented entity-lifecycle `Basic` guarantee rather than a rollback. An exception thrown by the script body itself is reported by `ScriptFunc::Call` and converted to a `false` result instead of propagating from the script body. `firstTime` is `true` for newly created entities and for the immediate call made by `SetupScript` / `SetupScriptEx`; restored world entities receive `false`. Those runtime methods invoke the callback first and persist its name only after a successful call. The typed overload rejects delegates, and both overloads throw when the function cannot be resolved or the call reports failure. Initialization ordering is not a project dependency mechanism. A newly created location initializes its child maps before the location; world loading initializes each location first and then recursively initializes its maps, critters, and items. Author callbacks so they depend only on the entity and explicitly established relationships available at that boundary. The callback starts with the initialized entity covered. It must acquire or widen synchronization before accessing unrelated entities. See [Script Lifecycle and Concurrency](../scripting/lifecycle-and-concurrency.md#entity-initscript-callbacks) for the runtime and concurrency contract. ## Migrations Prototype IDs can appear in declarations, parent lists, property references, runtime lookups, and persisted entities. Renaming or removing an ID is therefore a compatibility change, not a file move. Declare: ```cpp ///@ MigrationRule Proto Item OldContainer NewContainer ///@ MigrationRule Proto Item RemovedContainer __remove__ ``` The owning project decides where project metadata declarations live and how long rules are retained. A rename target must exist at the receiving revision. A removal is valid only when loading policy can safely discard the reference or entity; otherwise migrate to a compatible replacement. When changing a property name or type, follow the property/persistence migration policy of the owning metadata declaration. Prototype migration does not repair an incompatible property payload. ## Authoring practices 1. Start from the generated section and property references for the pinned engine revision. 2. Add project metadata and content rules in project-owned documentation instead of copying engine internals. 3. Keep IDs descriptive and stable; do not encode temporary folder structure, balance numbers, or release names into them. 4. Keep parent chains shallow, avoid overlapping multiple parents, and run a cycle validator. 5. Author only semantic defaults. Let runtime systems own transient position, ownership, cache, and relationship state unless the project explicitly documents otherwise. 6. Treat side-only behavior deliberately. A key being skipped from one output is not proof that the other side can function without a corresponding project contract. 7. Use prototype references instead of duplicated free-form IDs where metadata supports them, and validate every referenced target. 8. Add migrations in the same change as an ID/property compatibility change. 9. Run the real bake and the consuming subsystem's semantic tests; successful parsing alone does not prove usable content. 10. Keep each project extension focused on one content family for review and tooling, while remembering that the section and metadata, not the extension or directory, select the type. ## Validation workflow For an engine change: ```bash python BuildTools/tests/test_docs_prototype_format.py python BuildTools/docs_prototype_format.py --check python BuildTools/docs_contract_diff.py --baseline-git-ref origin/master --allow-missing-baseline --write --enforce ``` Also run the focused native tests for the changed parser, metadata, property, or baker boundary. For a project content change: 1. regenerate any project-owned metadata/property reference; 2. run the project's normal resource bake; 3. run structural validators for duplicate IDs, inheritance cycles, references, migrations, and required fields; 4. run focused tests for the consuming gameplay/editor system; 5. inspect every side that consumes a side-specific property. ## Updating an engine revision When an embedding project advances its Engine revision: 1. diff the old/new [canonical prototype-format models](../../../generated/prototype-format.json); 2. inspect `ProtoBaker`, `ConfigFile`, property serialization, metadata declarations, settings, and tests changed in the revision range; 3. regenerate the engine and project references; 4. update project format/semantic docs for added, removed, renamed, retyped, or side-changed properties; 5. add migrations and release notes for compatibility changes; 6. rebake all resource packs and run focused runtime/editor tests; 7. do not reuse baked prototype binaries from the previous revision. ## See also - [Baking Pipeline](../../explanation/content-pipeline/baking.md) - baker orchestration and output ownership. - [Entity Model](../../explanation/entity-and-property-model/) - entity metadata and runtime identity. - [Script Lifecycle and Concurrency](../scripting/lifecycle-and-concurrency.md) - callback invocation, synchronization, failure, and teardown rules. - [GeneratedApiAndMetadata.md](../../reference/metadata/index.md) - source metadata and generated API models. - [Generated Contract Change Management](../../contributing/contract-change-management.md) - aggregate contract diff and dispositions. - [Embedding Project](../build/embedding-project.md) - reusable engine/project ownership boundary. ===== END DOCUMENT prototype-format-guide ===== ===== BEGIN DOCUMENT map-format-guide ===== Source: Docs/en/how-to/content/map-format.md Canonical URL: https://fonline.ru/Docs/en/how-to/content/map-format.html Content SHA-256: 8a3a03955311abcf640034cf3dc1c0bbb5aec41ec3fdb510a83dbb0682a865ab --- layout: default title: FOnline Map Format document_id: map-format-guide locale: en permalink: /Docs/en/how-to/content/map-format.html --- # FOnline Map Format This guide defines the reusable engine contract for authored `.fomap` files. It covers source syntax, map and placement identity, property overrides, item ownership, mapper round-tripping, side-specific baking, and initial runtime materialization. Use the generated [map format reference](../../reference/map-format/index.md) for an exhaustive, revision-pinned contract: - [syntax and directives](../../reference/map-format/syntax.md); - [Map, Critter, and Item properties](../../reference/map-format/properties.md); - [ownership, baking, and runtime loading](../../reference/map-format/baking.md); - [validation rules with stable IDs](../../reference/map-format/validation.md); - [canonical JSON](../../../generated/map-format.json) for tools and AI agents. The engine owns this grammar and its load/bake behavior. An embedding project owns its map catalog, custom metadata, prototype IDs, visual kits, encounters, quests, balance, and level-design policy. ## Placement decision Give every authored Critter and Item placement an explicit positive `$Id`, and keep those IDs unique across both Critter and Item sections in the whole map. Every placement also needs `$Proto`; it selects the critter or item prototype and is independent of placement identity. Express ownership with explicit links. `CritterInventory` items carry the owning placement in `CritterId`; `ItemContainer` items carry the parent item placement in `ContainerId`. Never infer ownership from section order or nearby coordinates. ## Minimal Map ```ini [ProtoMap] $Name = SmallRoom Size = 80 80 WorkHex = 40 40 [$Name/Critter] $Id = 1 $Proto = Guard Hex = 38 40 Dir = 3 [$Name/Item] $Id = 2 $Proto = MetalDoor Hex = 42 40 ``` A configured map container has one or more `[ProtoMap]` anchors. Placement sections address an anchor as `[$Name/Critter]` and `[$Name/Item]`, or use the anchor's explicit map ID in place of `$Name`. Data before the first anchor, an unknown address, and every other section form are rejected. In particular, the current loader rejects bare `[Critter]` and `[Item]` sections. Legacy projects may still contain those or the older `[Header]`, `[Tiles]`, and `[Objects]` forms, but they are not valid current-engine input. The shared configuration parser provides `key = value`, repeated sections, `#` comments, backslash continuation, and `key += value` append syntax. Property types still determine which textual values and append operations are valid. ## Map Identity Each `[ProtoMap]` starts a map definition. `$Name` selects its Map prototype ID, the placement-section address, and the basename of both baked resources: ```text SmallRoom.fomap-bin-server SmallRoom.fomap-bin-client ``` When a container has one unnamed anchor, the source filename without `.fomap` is used. A multi-map container must give every anchor a unique explicit `$Name`; its placements then use that name, for example `[VaultEntrance/Critter]`. Keep the source basename and `$Name` equal for ordinary one-map files. A mismatch is legal and useful for a deliberate canonical rename, but it makes targeted baking and source lookup less obvious. `$Parent` uses ordinary Map prototype inheritance during prototype baking. It is not a mapper-safe source construct: mapper source loading does not resolve the parent chain, and mapper save emits its current full map property state without `$Parent`. Prefer explicit `[ProtoMap]` properties for maps that are edited in the mapper. If inheritance is required, validate both the baked runtime result and every mapper round-trip. `$Text ` fields belong to prototype-text baking. The mapper explicitly retains these fields as extra `[ProtoMap]` data. Other unknown `$` directives are not preserved by mapper save. ## Placement Identity Every addressed Critter and Item placement requires `$Proto`: ```ini [$Name/Item] $Id = 20 $Proto = Locker Hex = 45 42 ``` The prototype is resolved first. The remaining keys are applied as per-placement property overrides to a copy of that prototype's properties. `$Id` is optional at the loader level. Missing, non-positive, and duplicate values are replaced with the next available positive ID. That repair exists to load imperfect content; it is a poor authoring contract because ownership references can silently point at a different entity after repair. For production maps: 1. assign every placement an explicit positive `$Id`; 2. keep IDs unique across both Critter and Item sections, not only within one section type; 3. keep IDs stable across edits when another placement refers to them; 4. use an audit or bake gate to reject accidental duplicates before review. Textual interleaving is not an execution-order guarantee. The loader processes every Critter section first and every Item section second. Mapper save also normalizes ordering: critters and their inventory items come before map items and their direct children. ## Property Overrides Section properties use the receiver selected by the section: | Section | Receiver | Typical engine properties | | --- | --- | --- | | `[ProtoMap]` | `Map` | `Size`, `WorkHex`, `DayTime`, `DayColor` | | `[$Name/Critter]` or `[MapId/Critter]` | `Critter` | `Hex`, `Dir`, `Condition`, `InitScript` | | `[$Name/Item]` or `[MapId/Item]` | `Item` | `Hex`, `Ownership`, `Static`, `Hidden`, `Count`, `PicMap` | The generated [property catalog](../../reference/map-format/properties.md) is authoritative for built-in metadata at the pinned revision. It records type, runtime sides, flags, source, and whether text authoring is accepted. Project metadata can add more properties; document those in the project repository instead of extending this engine guide with one game's declarations. Unknown, virtual, temporary, malformed, or invalid-for-the-current-side properties fail during property loading or validation. Side-specific properties can be skipped from the opposite output. Resource-valued properties are validated by `MapBaker`, so a syntactically valid map can still fail because an image, model, script, sound, or prototype dependency is absent. ## Item Ownership `Ownership` determines where an authored item is materialized: | Ownership | Reference or position | Supported map use | | --- | --- | --- | | `MapHex` | `Hex` | Static fixture or generated non-static map item | | `CritterInventory` | `CritterId` | Direct inventory child of a placed critter | | `ItemContainer` | `ContainerId` | Direct child of a placed non-static map item | | `Nowhere` | none | Not supported for authored map placement | Example inventory item: ```ini [$Name/Critter] $Id = 100 $Proto = Guard Hex = 30 30 [$Name/Item] $Id = 101 $Proto = Rifle Ownership = CritterInventory CritterId = 100 ``` Example container and direct child: ```ini [$Name/Item] $Id = 200 $Proto = LootCrate Hex = 35 30 Static = false [$Name/Item] $Id = 201 $Proto = Ammo Ownership = ItemContainer ContainerId = 200 ``` The server creates critters and non-static map items first, records their authored-to-runtime ID mapping, then attaches child items. A child whose owner ID has no runtime mapping is skipped. Static items are not ordinary generated item owners, and child-of-child chains are not materialized by the current one-pass ID mapping. Keep authored containment one level deep and cover it with a runtime test when gameplay depends on it. ## Static And Dynamic Items A static item must use `MapHex` ownership. During server map loading it becomes an immutable static-grid entry. Its `Hex`, multihex geometry, `NoBlock`, `ShootThru`, trigger flags, and static scripts contribute to map collision and interaction state. A non-static `MapHex` item is a billet: the server creates a fresh runtime item for each map instance and remaps its authored ID. Placed critters follow the same per-instance generation model. Inventory and container children are created after those owners. Choose deliberately: - use static items for fixed map geometry, scenery, blockers, and triggers that do not need ordinary item lifecycle; - use non-static items for loot, containers, movable or mutable objects, and ownership roots; - do not mark an inventory or container child as static; - do not rely on a static item's authored ID as an `ItemContainer` runtime owner. ## Server And Client Outputs `MapBaker` produces a coupled pair: - the server binary contains map string hashes, placed critters, and every item; - the client binary contains string hashes and visible static item records; - dynamic entities are synchronized through the normal runtime entity path, not embedded in the client static layer. Hidden static items are omitted as client item records, but their client property strings are still collected into the hash dictionary. This lets server-only static logic retain identifiers needed by the client-side hash resolver without exposing a visible map entity. The client dictionary also contains all strings collected for the server map blob: authored `Server` property values and critter/dynamic-item overrides included. Only strings are added, not server entities or property records. They enter the client's pool only when this map loads; sending a map-only hash earlier still requires another declared receiving source. See [networking](../../explanation/authority-and-networking/index.md#unresolved-hash-recovery). Always regenerate and package both outputs after changing a map or a referenced prototype. Treat a one-sided stale result as invalid even when only one runtime role appears affected. ## Coordinates And Bounds `ProtoMap.Size` defines valid map coordinates. Every placed critter and every `MapHex` item must have a `Hex` inside that size. Server loading rejects out-of-range positions. Some mapper editing operations clamp moved entities and multihex coordinates back into bounds. That editor behavior is a convenience, not permission to commit invalid source. A project validator should parse and reject out-of-bounds authored coordinates before runtime packaging. `MultihexMesh` is a sequence of x/y coordinate pairs. Mapper save normalizes the property to backslash-continued pairs: ```ini MultihexMesh = \ 40 40 \ 41 40 \ 42 40 ``` An odd number of values fails serialization. Mapper load may also coalesce eligible items into multihex meshes according to prototype `MultihexGeneration`; saving after that operation can substantially rewrite placement layout. Review those diffs as semantic map changes. `MultihexLines` is prototype-owned directional geometry expanded around the placement anchor. Server materialization applies those line cells to static and dynamic map items, and also expands the lines around every valid `MultihexMesh` cell. All resulting cells point to the same item and participate in cached blocking/interaction flags. Runtime placement therefore treats the authored anchor, mesh cells, and their line expansions as one footprint; a project validator must check the complete expanded footprint, not only the anchor and explicit mesh pairs. ## Mapper Round-Trip Mapper save is deterministic normalization for the selected map, not byte-preserving serialization of that map. It: - writes `[ProtoMap]` first and records the selected map as `$Name`; - serializes the mapper's current full Map property state; - preserves `$Text*` extra fields; - omits the `$Parent` control directive; - emits placements as `[$Name/Critter]` and `[$Name/Item]`; - emits explicit `$Id` and `$Proto` for placements; - groups all critters before all map items; - places direct inventory/container children immediately after their owner group; - normalizes property order and `MultihexMesh` formatting; - may merge items according to `MultihexGeneration` before save. When the source container holds multiple maps, Mapper replaces only the selected map's section run and preserves every non-selected sibling map block byte-for-byte. That guarantee does not make the selected block byte-preserving: it is still normalized as described above. Before using the mapper on hand-authored or generated maps, commit or otherwise preserve a reviewable source snapshot. After saving, inspect the complete selected-block diff, confirm the emitted `$Name`, verify that sibling map blocks are unchanged, and rebake both outputs. For generators, prefer producing the canonical normalized form directly. Stable ordering and explicit IDs make later mapper diffs smaller and reduce ambiguity for humans and AI agents. ## Validation Workflow For an embedding project, the production gate should include: 1. parse the configured container and require one or more `[ProtoMap]` anchors, with a unique explicit `$Name` on every anchor in a multi-map file; 2. accept only `[$Name/Critter]` / `[$Name/Item]` or equivalent explicit-map-ID placement addresses that resolve to a declared anchor; 3. require explicit positive IDs unique across Critter and Item placements within each map; 4. resolve every `$Proto`, ownership value, `CritterId`, and `ContainerId`; 5. validate receiver properties, side availability, resources, and map bounds; 6. bake both server and client map resources for every declared map; 7. load representative maps in the server and client or mapper; 8. round-trip multi-map containers and verify untouched sibling blocks byte-for-byte; 9. run project-specific checks for quests, spawns, collision, visual kits, scripts, and gameplay routes. The engine unit tests cover parser strictness, ID repair, property application, canonical output naming, side payloads, hidden static items, mapper normalization, and server materialization. A project still needs content-backed validation because engine tests cannot know its metadata or map design rules. ## Best Practices - Keep filename, `$Name`, and project catalog ID aligned for one-map files; use explicit unique names in multi-map containers. - Put the first `[ProtoMap]` before content and use only addressed placement sections. - Use explicit unique positive IDs even though the loader can repair them. - Keep ownership shallow and references obvious in nearby sections. - Use static items only for fixed `MapHex` fixtures. - Keep map-level inheritance out of mapper-edited files unless its round-trip limitations are intentionally handled. - Generate canonical formatting and review every mapper normalization diff. - Validate source, both baked sides, and representative runtime loads in CI. - Document project-specific map conventions next to project content, linking here for reusable engine mechanics. ## Source Authorities The generated model tracks exact files and stable rule IDs. The primary implementation authorities are: - `Source/Common/ConfigFile.cpp` for shared text syntax; - `Source/Common/MapLoader.cpp` for section validation, placement IDs, and prototype resolution; - `Source/Tools/ProtoBaker.cpp` and `Source/Tools/ProtoTextBaker.cpp` for `[ProtoMap]` prototype and text handling; - `Source/Tools/MapBaker.cpp` for output identity, property application, validation, and side payloads; - `Source/Tools/Mapper.cpp` and `Source/Client/MapView.cpp` for mapper load/save normalization; - `Source/Server/MapManager.cpp` for static loading and per-instance materialization; - `Source/Client/MapView.cpp` for client static-map loading. When these files change, regenerate the [canonical model](../../../generated/map-format.json), compare its stable IDs with the previous revision, update this guide where behavior changed, and rerun the engine and embedding-project validation gates in the same change. ===== END DOCUMENT map-format-guide ===== ===== BEGIN DOCUMENT model-format-guide ===== Source: Docs/en/how-to/content/model-format.md Canonical URL: https://fonline.ru/Docs/en/how-to/content/model-format.html Content SHA-256: 343e310f1338b96c00857c939bb3b7581a8ca31a066ed278c6ad5e766de74a6e --- layout: default title: Model Format and 3D Composition document_id: model-format-guide locale: en permalink: /Docs/en/how-to/content/model-format.html --- # Model Format and 3D Composition FOnline uses `.fo3d` model descriptions to compose baked 3D meshes, converted source animations, layer-selected equipment, child models, particles, material overrides, and cut volumes into a client-side model instance. Use this guide for the authoring and runtime model. Use the generated [token reference](../../reference/model-format/tokens.md), [asset and limit reference](../../reference/model-format/assets.md), [validation rules](../../reference/model-format/validation.md), and [canonical JSON model](../../../generated/model-format.json) for the exact current-revision contract. ## Scope and authority The owning sources are: - `Source/Tools/ModelMeshBaker.cpp` and `Source/Common/ModelMeshData.*` for `.fbx` / `.obj` mesh import, validation, and the mesh-only `LFMODMSH` payload; - `Source/Tools/ModelSourceLoader.*`, `Source/Tools/ModelAnimationConverter.*`, and `Source/Common/ModelAnimationData.*` for source skeleton/clip extraction, compatibility analysis, Ozz conversion, and the native rig payload; - `Source/Tools/ModelInfoBaker.cpp` for `.fo3d` parsing, include expansion, dependency and source validation, model-info serialization, runtime-rig creation, and animation metadata; - `Source/Client/ModelManager.*`, `ModelHierarchy.*`, `ModelInformation.*`, `ModelInstance.*`, and `ModelAnimation.*` for strict runtime loading, shared immutable data, per-instance composition/pose state, animation controllers, and drawing; - `Source/Frontend/Rendering.h` plus the generated CMake project interface for compile-time model limits; - the model baker, mesh-data, source-loader, animation-data/converter/runtime, skeleton-compatibility, Ozz, and client-engine tests for executable grammar, wire, conversion, loading, and failure examples. This page is reusable Engine documentation. An embedding project owns concrete model names, layer meanings, model-layer values, animation enums, art direction, equipment rules, gameplay timing, and visible validation scenes. The exhaustive machine model is generated from `BuildTools/ModelFormatInterface.json`. Its generator compares the documented token set directly with `ModelDescriptionParser::ParseToken`; parser drift makes documentation validation fail. ## Pipeline overview The model pipeline has two ordered bakers and shared source/conversion modules: 1. `ModelMeshBaker` runs at order `4`. It imports `.fbx` and `.obj` source files and writes a versioned, mesh-only `LFMODMSH` resource at the same path and extension. Clips and mutable pose data are not stored in this hierarchy payload. 2. `ModelInfoBaker` runs at order `6`. It parses concrete `.fo3d` files, validates references and source freshness, loads the selected source skeletons/clips through the per-bake `ModelSourceAssetCache`, converts a canonical runtime rig, and writes versioned `LFMODINF` with a required `LFOZZRIG` payload at the same `.fo3d` path. It also emits `ModelAnimationInfo.foinfo` for common duration and bounds lookup. The client never parses authored text or source FBX/OBJ data. `ModelManager` loads shared mesh hierarchies, `ModelInformation` strictly loads one immutable model description and runtime rig, and each `ModelInstance` owns mutable controllers, pose matrices, children, particles, and render composition. Old headerless payloads and partial rig fallbacks are rejected. Files whose basename starts with `TEMPLATE_` are include-only. They affect concrete descriptions and bake timestamps, but are not emitted as independent `.fo3d` resources or `ModelAnimationInfo.foinfo` sections. ## Source mesh contract ### Supported inputs Current `ModelMeshBaker` scans only: - `.fbx` for skeletal or static meshes, skinning, material diffuse texture names, source skeletons, and animation clips; - `.obj` for static models, attachments, and cut volumes. Legacy `.x` and `.3ds` model paths are not current inputs. A file retaining one of those extensions is not selected by `ModelMeshBaker`. ### Import behavior The mesh baker and source loader use pinned `ufbx` through separate owners. The mesh baker emits hierarchy, bind, vertex, index, skin, and material data; the source loader extracts validated skeleton/TRS/clip data for animation conversion. Embedded files are ignored, skinning is evaluated, skin weights are cleaned, and missing normals receive deterministic handling in the mesh path. Author meshes with these constraints: - faces must already be triangles; - a concrete model must contain at least one drawable mesh; - node names become bone names, and nodes with attached geometry become drawable mesh names; - use one material per drawable node when deterministic texture ownership matters; - the first material's file-backed `DiffuseColor` texture becomes texture slot `0`; - texture filenames are stored without their original directory and later resolved relative to the baked mesh; - only the first skin deformer is consumed; - skin-cluster count must fit `FO_MODEL_MAX_BONES`; - only `FO_MODEL_BONES_PER_VERTEX` influences are retained per vertex, then normalized; - a non-skinned mesh receives a deterministic single-bone fallback; - animation stack names are the clip names used by `.fo3d` `Anim` entries, but those clips are loaded from source and converted into the model-info rig rather than serialized into `LFMODMSH`; - direct `.fbx` attachments must be rest-only: use a child `.fo3d` with explicit `Anim` mappings when an attached source contains clips; - external animation sources should contain hierarchy and animation only. Drawable geometry is rejected unless the exact selected file has a temporary `AllowAnimationGeometry` exception. The source loader rejects non-finite transforms, duplicate case-insensitive clip names, invalid durations or key times, excessive counts/depth, and malformed skeleton relationships before conversion. Animation sources may contribute compatible canonical joints that have no physical `ModelBone`; physical meshes and cuts remain in the base hierarchy. The current default limits are: | Project option | Runtime constant | Default | |---|---|---:| | `FO_MODEL_LAYERS_COUNT` | `MODEL_LAYERS_COUNT` | `30` | | `FO_MODEL_MAX_TEXTURES` | `MODEL_MAX_TEXTURES` | `8` | | `FO_MODEL_MAX_BONES` | `MODEL_MAX_BONES` | `54` | | `FO_MODEL_BONES_PER_VERTEX` | `MODEL_BONES_PER_VERTEX` | `4` | These are binary and shader shape contracts. If a project overrides them, its client binaries, baked model resources, `Critter.ModelLayers` data, effects, and packages must use the same values. ## Lexical syntax A `.fo3d` file is a sequence of whitespace-separated tokens: - `#` and `;` start a comment; - there is no quoted-string or escape syntax; - paths and names therefore cannot contain whitespace; - one line may contain multiple directives; - each directive consumes its required arguments and parsing continues with the next token; - unknown tokens and missing arguments are bake errors; - integer arguments accept numbers, explicit booleans, or enum names known to the baking metadata resolver; - float arguments must parse as finite numbers. Compact entries are legal: ```text Layer 1 Value 2 Attach Hat.fbx Link Head RotY 180 Texture 0 Hat.tga ``` For maintainability, keep structural selectors (`Layer`, `Value`, `Root`, `Attach`) before the modifiers that apply to them. ## Minimal descriptions A static model can be as small as: ```text Model Props/Crate.obj ``` A skeletal model with one animation and one layer-selected attachment: ```text Model Characters/Human.fbx RotationBone Spine Anim CritterStateAnim.Unarmed CritterActionAnim.Idle ModelFile Idle Anim CritterStateAnim.Unarmed CritterActionAnim.Walk ModelFile Walk Layer 1 Value 1 Attach Items/Hat.fbx Link Head ``` Enum names depend on the embedding project's metadata. Numeric examples in engine tests prove parser behavior but are not a recommended project vocabulary. ## Includes and templates `Include` parses another file inline: ```text Include TEMPLATE_Humanoid.fo3d mesh Human.fbx scale 0.9 ``` Arguments after the path are name/value pairs. Before tokenization, every literal `%name%` in the included text is replaced with its value: ```text # TEMPLATE_Humanoid.fo3d Model %mesh% Scale* %scale% ``` Important include rules: - the include path is relative to the file containing `Include`; - `Model`, `Attach`, and `Cut` paths inside the included text are relative to the included file; - an `Anim` file other than `ModelFile` is resolved later relative to the concrete `.fo3d` output; - include arguments consume the rest of their line, so do not place another directive after them; - replacements are plain text, not token-aware substitutions; - included content shares parser state with its caller; - recursive includes are rejected; - the newest timestamp in the complete include graph controls incremental rebaking. Prefer self-contained templates that establish their own `Root`, `Layer`, `Value`, and `Mesh` context. A template that silently depends on caller state is difficult for humans, AI agents, and validators to reason about. ## Parser state The parser tracks: - the selected `Layer`; - the selected `Value`; - the current link receiving modifiers; - the current `Mesh` selector used by `Texture` and `Effect`. At file start, the current link is the default root. Top-level transforms and material modifiers therefore apply to the base model even without an explicit `Root`. `Layer` or `Value`: - updates that selector; - clears `Mesh`; - redirects the current link to a dummy object. After selecting a layer/value pair, write `Root`, `Attach`, or `AttachParticles` before any transform, material, disable, or cut directive. Modifiers written while the dummy link is current are parsed but discarded. There is no directive that restores `Layer` to the initial `-1` state. Author all default-root declarations before the first `Layer`, or put them in an earlier include. `Root` also clears `Mesh`. `Attach` and `AttachParticles` create a new link and clear `Mesh`. Put `Mesh` after the link selector it should affect. `Link` is stored only for a non-default, non-dummy link. On the default root it is ignored. A `Link` on a layer `Root` is serialized but has no child to attach; omit it. ## Layers and values `Layer` selects an index in the fixed model-layer array. The valid range is: ```text 0 <= layer < FO_MODEL_LAYERS_COUNT ``` `Value` selects an exact project-defined integer. Zero means inactive and cannot create a layer `Root`, `Attach`, or `AttachParticles` entry. At runtime, `ModelInstance::PlayAnim`: 1. copies the supplied model-layer array, or reuses the previous array; 2. applies exact `AnimLayerValue` overrides for the requested state/action pair; 3. resets the model to default-root data; 4. finds entries whose `Layer` and `Value` match; 5. applies root modifiers and material changes; 6. creates or retains child models and particles; 7. removes no-longer-selected children and particles; 8. regenerates combined meshes when composition changed. A layer value is rendering composition state, not merely cosmetic metadata. Changing it can alter transforms, animation speed, visible geometry, materials, draw effects, cuts, child models, particles, and batching. The meaning of every layer index and value belongs in the embedding project's documentation and tests. ## Root modifiers `Root` selects the base model's default link when no layer has been selected: ```text Root Scale 0.9 RotX 90 ``` With a selected non-zero layer/value pair, it creates a conditional root modifier: ```text Layer 3 Value 2 Root DisableMesh Torso Texture 0 Armor.tga ``` Conditional root links can: - add transforms and speed multipliers; - override textures and effects; - disable other layers; - disable meshes; - add cut volumes. They do not create a child model. ## Model attachments `Attach` requires a selected layer and non-zero value: ```text Layer 1 Value 4 Attach Weapons/Rifle.fo3d Link RightHand ``` The child path is relative to the declaring file. ### Single-bone attachment With `Link `, the complete child is parented to one validated bone: ```text Attach Hat.fbx Link Head ``` The link's rotation, translation, scale, speed, materials, disables, and cuts apply inside the child model instance. ### Shared-skeleton attachment Without `Link`, runtime pairs same-named child and parent bones: ```text Attach ArmorTorso.fbx ``` Use this only for clothing or body-part assets authored against the same skeleton. Runtime creation fails if no common bones exist. ### Child descriptor versus direct mesh Use `Attach child.fo3d` when the child needs its own: - base mesh selection; - nested layers or attachments; - default material/effect policy; - cuts; - animation declarations; - rendering flags. Use direct `.fbx` / `.obj` attachment for a simple baked hierarchy. A child `.fo3d` is baked and validated independently; the parent validates that the descriptor exists and that the parent `Link` bone is valid. A direct attachment has no description-level scale correction. Its static maximum-axis extent must remain within `Baking.ModelAttachmentMinExtent` .. `Baking.ModelAttachmentMaxExtent`; otherwise baking fails with the measured extent and limit. Use a child `.fo3d` when an explicit scale is part of the composition. Mesh nodes with a negative transform determinant are rejected earlier by `ModelMeshBaker`: reset/freeze mirrored geometry to a positive transform before export instead of relying on the baker to flip normals and winding. ## Particle attachments `AttachParticles` is layer-selected: ```text Layer 8 Value 1 AttachParticles Particles/Jet.spk Link Backpack MoveY 0.15 RotY 90 ``` The particle path is a global baked-resource path, not relative to the `.fo3d` file. Reference the generated `.spk` or `.efk` resource rather than its `.spark` or `.efkproj` authoring source. Always provide a valid `Link` bone; runtime particle creation requires it. The link's `MoveX`, `MoveY`, `MoveZ`, and `RotY` feed particle placement. The particle instance remains alive while the exact layer/value link stays active and is removed when composition changes. The attached resource's XML, registered SPARK objects, renderer fields, effects/textures, runtime cache, and visible validation are owned by [Particle Format And Runtime](particle-format.md). Model-bone particles use the direct 3D composition path rather than `ParticleSprite`'s atlas/direct-scene selector. ## Transforms and speed Per-link fields are: - `RotX`, `RotY`, `RotZ` in degrees; - `MoveX`, `MoveY`, `MoveZ` in model coordinates; - `ScaleX`, `ScaleY`, `ScaleZ`; - `Speed` as a playback multiplier. `Scale` sets all three scale axes. Every field has assignment, additive, and multiplicative forms: ```text Scale 0.9 Scale+ 0.1 Scale* 1.5 RotY 90 RotY+ 15 RotY* 0.5 ``` The `+` and `*` forms use special zero initialization: - if the current field is zero, the operand becomes the field value; - otherwise addition or multiplication is applied normally. This lets a template use `Scale* 0.9` or `Speed* 1.2` without requiring an earlier assignment. It also means declaration order is observable. At runtime, zero means identity/no contribution. Non-zero transforms are multiplied into the current model transform. A negative final `Speed` is rejected during baking; zero means no speed contribution. ## Meshes, textures, and effects `Mesh` selects a drawable node by name: ```text Mesh Torso Texture 0 Armor.tga Effect Effects/Armor.fofx ``` `Mesh All` clears the selector, so following material directives target every drawable mesh in the current link's model. `Subset` is obsolete. The parser consumes its argument and logs a warning, but does not select anything. Never use it in new content. ### Textures ```text Texture ``` The slot must be in `[0, FO_MODEL_MAX_TEXTURES)`. For a normal texture name: - the target `Mesh` must exist and be drawable; - the texture path is resolved relative to the current target mesh file; - the texture must exist in baked resources. An imported material's diffuse texture is the default for slot `0`. All other slots start empty unless assigned. Inside an attached child, `Parent` copies the first matching parent's current texture at the same slot. `Parent_` copies it from the named parent mesh: ```text Texture 0 Parent_Torso ``` Do not use `Parent` on a root description. When a parent has several meshes, prefer the explicit suffix. ### Effects ```text Effect Effects/SkinnedArmor.fofx ``` Effect paths are global baked-resource paths loaded for model usage. `Parent` and `Parent_` copy the parent's current effect using the same attached-child rules as textures. Meshes can share one combined draw batch only when effect, texture set, and bone capacity are compatible. Material overrides may therefore change batching and should be measured on representative composed models. ## Disabling layers and meshes `DisableLayer` accepts hyphen-separated layer indices: ```text DisableLayer 5-6-7 ``` When the link is active, those layer slots are skipped inside the affected model instance. `DisableMesh` accepts hyphen-separated drawable node names: ```text DisableMesh Hair-HelmetBase ``` `DisableMesh All` stores a wildcard and disables every mesh in the affected model instance. Use disables to express mutually exclusive composition, but keep project layer ownership explicit. Cyclic or order-dependent exclusion policy quickly becomes difficult to test. ## Cut volumes `Cut` removes geometry from selected composed-mesh layers: ```text Cut CutVolumes/Helmet.obj All HeadVolume - - - ``` The six arguments are: 1. cut-volume `.fbx` / `.obj` path, relative to the declaring file; 2. hyphen-separated target layers, or `All`; 3. hyphen-separated drawable shape names from the cut file, or `All`; 4. first unskin bone, or `-`; 5. second unskin bone, or `-`; 6. unskin shape, `~shape` for reversed behavior, or `-`. `All` layers expands to every compile-time layer except the currently selected layer. At default-root scope, all layers are included. `All` shapes selects every drawable shape except the separately named unskin shape. The current runtime classifies a cut shape by its baked vertex count: - exactly `36` vertices: axis-aligned box bounds; - any other count: sphere radius derived from the X extent. This is an Engine format rule, not a general mesh heuristic. Author dedicated simple cut assets and validate the result visually. Both unskin bones must be provided together. An unskin shape requires both bones. All referenced bones and drawable shapes are checked during baking. Applying any cut disables normal culling for the composed model. Treat cuts as a correctness feature with a rendering cost; avoid using detailed production meshes as cut volumes. ## Rendering controls ### Automatic model-sprite layout `DrawSize` and `ViewSize` are removed legacy directives. `ModelInfoBaker` writes aggregate, idle-priority view/name, and per-animation bounds to `ModelAnimationInfo.foinfo` version 2. At runtime the client projects those bounds for each direction, extends them with enabled child models and layers, and derives the offscreen frame, visual anchor, lighting envelope, and interaction/view rectangle. Every non-particle child link serializes a validated root-space AABB; the default link and particle links carry no geometry payload. The baker includes disabled meshes, nested descriptions, link transforms, and the parent's sampled animations when calculating that envelope. Runtime framing unions the active animation bounds with the selected link envelopes and projects only their corners—there is no per-frame weighted-vertex sweep. Live particles can still force bounded expansion and rerender when they exceed the baked geometry envelope. Authors therefore tune source transforms, animation reach, attachments, and `Render.ModelProjFactor`, not fixed pixel rectangles inside `.fo3d`. Validate every direction and representative animation in a visible client. Use `Game.DumpAtlases()` or the mapper's **Dump atlases** command when diagnosing clipping, unexpected empty space, polygon edges, or crop placement. See [Baking Pipeline](../../explanation/content-pipeline/baking.md#shared-animation-metadata) and [Frontend and Rendering](../../explanation/rendering/#sprite-and-model-atlas-geometry) for the binary and runtime contracts. ### Other flags - `DisableShadow` disables model shadow drawing. - `DisableAnimationInterpolation` selects nearest-key sampling when the baked runtime rig is loaded. - `DisableBackwardAnim` selects forward walk/run instead of `WalkBack` / `RunBack` and aligns look direction with movement. - `RotationBone ` enables the movement overlay controller and directional torso/head rotation around a validated body bone. - `FastTransitionBone ` resets transition state for a newly attached child using that link bone. ## Animation boundary The `.fo3d` animation directives are: ```text Anim AnimSpeed AllowAnimationGeometry AnimLayerValue StateAnimEqual ActionAnimEqual FastTransitionBone RotationBone DisableAnimationInterpolation DisableBackwardAnim ``` Use [Model Animation](model-animation.md) for: - first-entry-wins tuple behavior; - `ModelFile`, `Base`, and reversed `~clip` lookup; - source skeleton compatibility, conversion into the required runtime rig, and the temporary external-animation geometry exception; - one-step aliases; - effective duration; - common versus loaded-client lookup; - animation substitutions and validation. `AnimLayerValue` applies to the exact requested pair before model composition. Alias resolution belongs to animation lookup; do not assume an alias also rewrites the key used for layer overrides. `AllowAnimationGeometry` is a narrow migration aid, not a normal asset policy. It names one exact external file selected by `Anim`, resolves from the final concrete `.fo3d`, and is consumed only by baker validation. It is not serialized. Duplicate paths, duplicate resolved targets, non-selected files, and exceptions left behind after geometry removal are hard errors. Repair the source into a geometry-free animation export while preserving required helper/bone hierarchy, then remove the exception in the same asset migration. 3D skeletal animation is separate from the 2D `NextX` / `NextY` contract in [Sprite Root Motion](sprite-root-motion.md). ## Runtime loading and caching `ModelManager::CreateModel(name)` accepts: - a baked `.fo3d` description, which creates full model information and composition behavior; - a baked mesh path, which creates a basic rest-pose hierarchy-backed model without `.fo3d` declarations or an animation controller. Model descriptions and mesh hierarchies are cached by resource name. Immutable animation clips, remaps, bindings, and canonical skeleton data belong to `ModelInformation`; mutable timelines, sampled poses, matrices, linked children, and procedural transforms belong to each `ModelInstance`. Layer changes reuse active child links by stable baked link id and remove children and particles that no longer match. Combined mesh generation merges compatible visible meshes until effect, texture, or bone-capacity differences require a new batch. Cuts are applied after parent and child meshes have been combined. Do not mutate or parse the baked binary `.fo3d`, `.fbx`, or `.obj` payloads from project scripts. Their binary layout is a private baker/runtime contract. ## Failure behavior `ModelInfoBaker` rejects or reports: - missing `Model`; - missing, unreadable, stale, or malformed baked meshes and their source files; - a primary mesh with no drawable geometry; - missing default diffuse textures; - missing explicit textures, effects, particles, child descriptions, or cut files; - invalid layer or texture indices; - zero layer values for `Root` / `Attach`; - missing bones or drawable mesh references; - malformed include graphs or replacement pairs; - invalid or non-finite numbers; - negative link `Speed`; - non-positive `AnimSpeed`; - unknown animation enums, missing clips, incompatible source skeletons, or invalid runtime-rig conversion; - direct attached FBX files with clips, external animation files with unexpected drawable geometry, and duplicate/non-selected/stale `AllowAnimationGeometry` exceptions; - mirrored mesh nodes and direct FBX/OBJ attachments outside the configured Engine world-unit extent; - invalid cut layer/shape/unskin combinations; - unknown tokens. Runtime loading repeats critical binary and range checks. Runtime exceptions indicate corrupted/stale baked data or a validation gap; do not catch them and substitute an unrelated model as a silent fallback. ## Legacy content warning Do not infer current support from old FOnline project files. In particular: - `.x` and `.3ds` are not selected by the current mesh baker; - `AnimEqual` was replaced by the domain-specific `StateAnimEqual` and `ActionAnimEqual`; - `CalculateTangentSpace`, `RenderFrame`, and `RenderFrames` are not current tokens; - `Subset` is accepted only as an obsolete warning path and has no selection effect. Port legacy assets by first translating them to the current source formats and grammar, then validating the result against the current Engine revision. Legacy project content is evidence of historical usage, not a normative format specification. ## Authoring practices For maintainable model descriptions: 1. Keep default-root declarations before the first `Layer`. 2. Give every concrete description exactly one intentional final `Model`. 3. Prefix include-only files with `TEMPLATE_`. 4. Make templates establish their own selector context instead of inheriting caller state. 5. Use enum names for state/action and project layer constants where metadata exposes them. 6. Document every project layer index, allowed value, owner, and conflicting layer. 7. Use one drawable node per independently overridden material or effect. 8. Use direct mesh attachments for simple props and child `.fo3d` descriptions for reusable composed objects. 9. Always give particle attachments an explicit `Link`. 10. Use exact case in paths, bone names, mesh names, animation stacks, and effects. 11. Keep cut volumes simple and purpose-built. 12. Exercise the full layer combination matrix, not only each attachment in isolation. 13. Export external animation files without drawable geometry; preserve required helper/bone hierarchy and tracks when repairing older files. 14. Treat every `AllowAnimationGeometry` line as temporary migration debt with a named source-repair owner, then remove it as soon as the export is clean. 15. After source-loader, converter, mesh-wire, or animation-source changes, run a full force bake followed by an incremental bake to prove both conversion and dependency timestamps. For AI-authored content, record the intended parser state before emitting each modifier: ```text current layer = 3 current value = 2 current link = Attach Armor.fo3d current mesh = Torso next directive = Texture 0 Parent_Torso ``` If that state cannot be stated unambiguously, split the compact line or make the selectors explicit. ## Validation workflow After changing model assets or descriptions: 1. Regenerate and validate the format reference: ```powershell python BuildTools\docs_model_format.py --write python BuildTools\docs_model_format.py --check python -m unittest BuildTools.tests.test_docs_model_format ``` 2. Run focused Engine model tests: ```powershell .\Binaries\Tests-Windows-win64\LF_UnitTests.exe "ModelBaker*" ``` 3. Rebake the embedding project: ```powershell cmake --build Build\Auto --config RelWithDebInfo --target BakeResources ``` 4. Launch a visible client scene that covers: - automatic framing, view/name anchoring, and atlas crop bounds; - every authored layer/value; - single-bone and shared-skeleton attachments; - particles; - texture/effect inheritance; - mesh/layer disables; - cuts; - idle, movement, turn, backward, and action animations; - shadows and animation interpolation. 5. Record project-specific layer semantics, expected screenshots, and regression routes in the embedding project's documentation. A clean bake proves grammar, asset closure, enum/range validity, and baked serialization. It does not prove pose quality, scale, clipping, material appearance, animation blending, cut geometry, interaction bounds, or performance. ## Change routing When changing: - `.fo3d` tokens, parser state, include behavior, path rules, or validation: update this guide, `BuildTools/ModelFormatInterface.json`, generated model-format outputs, and focused tests; - `.fbx` / `.obj` import, skinning, material extraction, animation-stack conversion, or model limits: update the asset and limit contract and run native mesh-baker tests; - model layers, attachments, particles, transforms, textures, effects, cuts, batching, or rendering flags: update composition/runtime sections and validate a visible client scene; - animation tuples, aliases, speed, duration, or script lookups: update [Model Animation](model-animation.md); - 2D frame offsets or walk/run sprite phase: update [Sprite Root Motion](sprite-root-motion.md); - project model catalogs or layer meanings: update only the embedding-project documentation while linking back to this reusable contract. ===== END DOCUMENT model-format-guide ===== ===== BEGIN DOCUMENT text-and-localization-guide ===== Source: Docs/en/how-to/content/text-and-localization.md Canonical URL: https://fonline.ru/Docs/en/how-to/content/text-and-localization.html Content SHA-256: 530625de381a099a604b158b2f7f8fa695508cdd1c2f76f44ac6ac44579a06c6 --- layout: default title: Text and Localization locale: en document_id: text-and-localization-guide permalink: /Docs/en/how-to/content/text-and-localization.html --- # Text And Localization This guide documents the reusable FOnline Engine contract for raw `.fotxt` files, prototype-localized `$Text` fields, language baking, runtime lookup, and renderer-owned inline color tags. Use the generated [text-format reference](../../reference/text-format/index.md) and [canonical JSON model](../../../generated/text-format.json) for the exact current-revision rule set. Bitmap-font descriptors, slot binding, glyph coverage, measurement, wrapping, and the complete renderer flag contract are owned by [Font Formats And Text Layout](font-format.md). An embedding game owns its language priorities, pack catalog, semantic key names, translation workflow, and any formatter layered on top of retrieved strings. Engine documentation must not depend on one project's packs or lexems. ## Text pack model Every text value is stored under a `TextPackKey`: ```text Collection + Key1 + Key2 + Key3 ``` `Collection` is a `TextPackName`; the remaining fields are `hstring` values. Language is deliberately not part of the key. The same key may therefore exist in each baked language pack. The backing container is a vector sorted by the complete key. Multiple adjacent values under one key are variants. Every load, merge, and fallback-repair path restores the order before returning; read methods verify the invariant and use one binary-search range rather than mutating or repairing shared state. This keeps concurrent reads safe. `Game.GetTextCount(key)` reports the variant count and `Game.GetText(key, index)` performs zero-based indexed selection. ## Raw `.fotxt` files Name every source: ```text ..fotxt ``` The basename must contain exactly those two dot-separated segments. The Engine treats the language suffix as an opaque string and does not require a four-character locale code. Projects may adopt a naming convention, but it is not part of the reusable parser contract. Each logical entry has three brace-delimited fields: ```text {Key1}{Key2}{Text} ``` For example: ```text {Welcome}{}{Welcome to the wasteland.} {QuestName}{Short}{A difficult choice} {LongMessage}{}{First line Second line} ``` The filename supplies `Collection`; raw entries supply `Key1`, `Key2`, and the value. `Key3` remains empty. `Collection` and `Key1` must be non-empty. `Key2` and the text value may be empty. Only the third field may continue across physical lines. The parser appends newline characters until it finds the first closing brace. There is no escape for a literal closing brace inside the value, and trailing data after the third field is not part of the entry. ### Comment boundary Raw text parsing has no comment-token grammar. A physical line is skipped only when the parser cannot find the first opening brace. A project may use brace-free headings or `#` lines for readability, but a supposed comment that contains brace groups can become an entry or make the bake fail. Treat this as a content-review rule: - keep comments and headings free of `{` and `}`; - demonstrate key shapes in fenced documentation, not inside `.fotxt` comments; - reject editor tooling that inserts annotation braces into text-pack sources. ### Variants Duplicate complete keys remain separate variants: ```text {Ambient}{Dust}{The wind scrapes across the road.} {Ambient}{Dust}{A sheet of dust hides the horizon.} ``` The script API does not choose randomly by default. `Game.GetText(key)` selects the first variant. For deliberate random selection, query `Game.GetTextCount(key)`, select an index in project code, and pass that index to `Game.GetText(key, index)`. Do not assume that every language has the same number or ordering of variants. Language normalization aligns key presence, not duplicate cardinality. ## Language normalization `Baking.BakeLanguages` is ordered and must not be empty. Each entry is either a bare `language` or `child:parent`. The first entry is the default base language; an explicit parent must appear earlier, and language names must be unique. A bare later language falls back to the first language. For example, `russ engl ru18:russ en18:engl` creates two adult-overlay fallback chains. The Engine default is `engl`; embedding projects normally override it in their `.fomain`. For a changed raw pack, `TextBaker` gathers all configured language files for that pack. The base-language source must be present. Unsupported filename suffixes are warned and skipped. `TextPack::FixPacks` then normalizes every non-base language against its explicit parent, or against the first language when no parent is declared: 1. remove languages not listed in `Baking.BakeLanguages`; 2. add missing configured languages; 3. remove packs that do not exist in the selected fallback language; 4. copy packs missing from the child language; 5. add keys missing from a child pack using fallback-language values; 6. remove keys that are absent from the fallback pack. This fallback is completed during baking. Runtime lookup does not consult the base language after a binary pack is loaded. The binary name is: ```text ...fotxt-bin ``` `TextPack::LoadFromResources` requires exactly those three basename segments. ## Prototype `$Text` fields Prototype sections can author localized values without separate raw text files: ```ini [ProtoItem] $Name = LaserRifle $Text engl Name = Laser rifle $Text engl Desc Short = Compact description $Text russ Name = Localized name ``` The grammar is: ```text $Text [Language] [Key2] [Key3] = Value ``` There may be at most four key tokens including `$Text`. The prototype id becomes `Key1`. When `Language` is omitted, the field uses the first `Baking.BakeLanguages` entry. Values pass through `StringEscaping::DecodeString`, so sequences such as `\n` become actual newlines. Parent `$Text` fields are collected recursively before child fields. A child inherits missing exact keys and replaces an inherited value when it defines the same `$Text Language Key2 Key3` key. `ProtoTextBaker` emits five packs for every configured language: | Prototype type | Generated pack | |---|---| | Item | `Items` | | Critter | `Critters` | | Map | `Maps` | | Location | `Locations` | | Other non-exported entity or fixed type with `HasProtos` | `Protos` | All five outputs exist even when some are empty. Unsupported `$Text` languages are warned and omitted. If multiple prototype sources would generate the same complete key in one pack and language, baking fails instead of depending on iteration order. ## Runtime script API The generated [method reference](../../../generated/api/methods.md) owns exact exported signatures. The important behavioral contract is: | API | Sides | Behavior | |---|---|---| | `Game.GetLanguage()` | server, client, mapper | Return the current `Language` setting as `LanguageName`. | | `Game.GetText(key)` | client, mapper | Return the first variant from the current language. A missing key returns an empty string. | | `Game.GetText(key, index)` | client, mapper | Return the zero-based indexed variant from the current language. Missing or out-of-range returns an empty string; negative index throws. | | `Game.GetText(langName, key)` | client, mapper | Use the current pack when `langName` is empty or current; otherwise load/cache that language and return its first variant. No runtime fallback is applied for an absent non-empty language. | | `Game.GetTextCount(key)` | server, client, mapper | Return variant count, or zero when absent. | | `Game.IsTextPresent(key)` | server, client, mapper | Return whether at least one variant exists. | | `Game.ChangeLanguage(langName)` | client, mapper | Replace the current runtime pack; the immutable startup setting is unchanged. | The client loads `Client.Language` during startup, then owns the current language as live engine state exposed by `Game.CurrentLanguage` / `Game.GetLanguage()`. `Game.ChangeLanguage` does not validate the identifier and does not invoke a game GUI refresh callback. An embedding project owns its allowed-language selector, persistence policy, and refresh/rebuild sequence. The server loads one pack from the startup `Client.Language` setting and then owns the same current-language state. It exposes only presence and count queries to scripts; there is no server-side script `Game.GetText` overload in the Engine contract. ## Engine and project formatting boundary `TextPack` stores opaque strings. The Engine does not define `@pname@`, `@nname@`, `@sex@`, `@rnd@`, `@arg@`, `@text@`, variant separators, named argument serialization, or dialog-specific lexem expansion. Those features, when present, belong to the embedding project's scripts and documentation. The client font renderer does own inline color tags. The exact byte-order and flag interaction is also pinned in the generated [font rendering reference](../../reference/font-format/rendering.md): ```text @color:BBGGRR@ @color:AABBGGRR@ @color:0xBBGGRR@ @color:0xAABBGGRR@ @color@ ``` Six hex digits set a color without an explicit alpha byte; eight include alpha. The empty `@color@` tag restores the previous color. Tags are stripped during font formatting. With `FontFlag.NoColorize`, valid tags are still stripped but all text uses the base color. If a project adds another formatting pass, document and test: - which side performs it; - whether it runs before or after text retrieval; - whether nested lookups are allowed; - escaping and malformed-input behavior; - random-selection ownership; - interaction with renderer color tags. ## Authoring workflow 1. Choose the owning text pack and a semantic complete key. 2. Author the base language first. 3. Add configured translations without inventing keys absent from the base. 4. Use duplicate keys only when the caller deliberately handles variants. 5. Keep raw `.fotxt` comments brace-free. 6. Use prototype `$Text` for prototype-owned names and descriptions. 7. Bake resources and treat every parser, missing-base, or intersection error as a source-content failure. 8. Exercise language switching and every project formatter in a visible client. ## Validation workflow Engine maintainers changing `TextPack`, `TextBaker`, `ProtoTextBaker`, language settings, script text methods, or inline color parsing must update `BuildTools/TextFormatInterface.json`, this guide, and generated outputs in the same change: ```powershell python BuildTools\docs_text_format.py --write python BuildTools\docs_text_format.py --check python -m unittest BuildTools.tests.test_docs_text_format ``` Run focused native tests for `TextPack`, `TextBaker`, and `ProtoTextBaker` when their behavior changes. Validate color-parser changes through the nearest rendering tests and a visible client. Then rebake an embedding project. Project-owned pack catalogs, translation guards, lexem formatters, and GUI refresh behavior require project tests rather than Engine fixtures. ## Maintenance routing - raw syntax, key identity, variants, binary loading, or normalization: `Source/Common/TextPack.*`; - filename selection, incremental pack completion, or raw output: `Source/Tools/TextBaker.cpp`; - prototype `$Text`, inheritance, pack routing, or intersections: `Source/Tools/ProtoTextBaker.cpp`; - script lookup and switching: `Source/Scripting/*GlobalScriptMethods.cpp` plus `Source/Client/Client.cpp` or `Source/Server/Server.cpp`; - inline color tags and all font layout/rendering behavior: `Source/Client/FontManager.*` and [Font Formats And Text Layout](font-format.md); - project pack names, language order, translations, lexems, or GUI refresh: the embedding project. ===== END DOCUMENT text-and-localization-guide ===== ===== BEGIN DOCUMENT effect-format-guide ===== Source: Docs/en/how-to/content/effect-format.md Canonical URL: https://fonline.ru/Docs/en/how-to/content/effect-format.html Content SHA-256: dce3f7ec64a38feb365c46e8fb1a36c0fd1b89ddc976d03d7720b6437b6da145 --- layout: default title: Effect Format And Shader Runtime locale: en document_id: effect-format-guide permalink: /Docs/en/how-to/content/effect-format.html --- # Effect Format And Shader Runtime FOnline uses `.fofx` files for authored GPU effects. One source combines render state, one or more vertex/fragment shader passes, Engine-owned shader resources, and the data needed to bake backend-specific shader artifacts. Use this guide for authoring and runtime behavior. Use the generated [effect-format reference](../../reference/effect-format/index.md), its focused [render-state](../../reference/effect-format/render-state.md), [resource](../../reference/effect-format/resources.md), [baking](../../reference/effect-format/baking.md), and [runtime](../../reference/effect-format/runtime.md) pages, plus the [canonical JSON model](../../../generated/effect-format.json), for the exact current-revision contract. ## Scope and authority The owning sources are: - `Source/Tools/EffectBaker.cpp` for `.fofx` parsing, shader compilation, reflection, backend flavors, and diagnostics; - `Source/Frontend/Rendering.h` and `Source/Frontend/Rendering.cpp` for vertex layouts, built-in buffers, pass/render state, and reflected-resource loading; - `Source/Client/EffectManager.cpp` for path caching, default effects, `ScriptValueBuf`, and per-frame buffer updates; - `Source/Client/Client.cpp` and `Source/Scripting/ClientGlobalScriptMethods.cpp` for script-selected effects and script-value APIs; - `Source/Frontend/Rendering-*.cpp` for backend shader loading, pipelines, descriptor binding, and drawing; - `Source/Tests/Test_EffectBaker.cpp` for executable bake and failure examples. This page is reusable Engine documentation. An embedding project owns its effect catalog, resource-pack override order, shader quality profile, art direction, concrete `EffectType` assignments, and every semantic meaning assigned to `ScriptValueBuf`. `BuildTools/EffectFormatInterface.json` is the source-backed structured contract. `BuildTools/docs_effect_format.py` validates its source anchors, derives compile-limit defaults from the CMake project interface, and renders the generated reference. Parser, baker, resource, or runtime drift must update that model in the same change. ## Minimal effect A one-pass untextured quad effect can be as small as: ```ini [Effect] [VertexShader] layout(set = 0, binding = 0, std140) uniform ProjBuf { mat4 ProjMatrix; }; layout(location = 0) in vec3 InPosition; void main(void) { gl_Position = ProjMatrix * vec4(InPosition, 1.0); } [FragmentShader] layout(location = 0) out vec4 FragColor; void main(void) { FragColor = vec4(1.0); } ``` The file must have `[Effect]` and usable vertex/fragment source for every declared pass. The baker supplies the version directive, precision qualifier, and Engine compile-time defines. ## File structure `.fofx` uses the Engine `ConfigFile` parser in content-collection mode. Config keys belong in `[Effect]`; shader section bodies are collected as raw text. ### `[Effect]` This required section owns: - `Version`; - `Passes`; - `ShadowPass` in 3D builds; - `BlendFunc`; - `BlendEquation`; - `DepthWrite`; - `DepthFunc`; - `DepthVariants`; - `CullVariants`; - the `_PassN` variants of blend and depth keys. The runtime reparses this section from the baked copy of the original `.fofx` file. Render state is therefore not encoded only in shader binaries or reflection metadata. ### `[ShaderCommon]` This optional raw-text section is prepended to both stages of every pass. Use it for constants and helper functions shared by the effect's stages/passes. There is no `.fofx` include directive. Keep reusable code inside `[ShaderCommon]`, duplicate deliberately between separate effect resources, or generate project effects outside the Engine format if the project owns such a workflow. ### Shader stages and pass fallback For pass `N`, the baker resolves stages in this order: 1. `[VertexShader PassN]`, then `[VertexShader]`; 2. `[FragmentShader PassN]`, then `[FragmentShader]`. An empty pass-specific section is treated as absent and falls back to the generic section. If neither source exists for a stage, baking fails. Pass numbering is one-based. Keep the spelling distinction clear: - shader section: `[FragmentShader Pass2]`; - state key: `BlendFunc_Pass2`. ## Render state ### Pass count and shader version `Passes` defaults to `1` and must be in `1..FO_EFFECT_MAX_PASSES` (Engine default `6`). The value controls every stage compile, metadata artifact, and backend pass object. `Version` defaults to `310`. The baker emits: ```glsl #version 310 es precision highp float; ``` Do not write a second `#version` directive in authored shader text. ### Blend state `BlendFunc` defaults to: ```ini BlendFunc = SrcAlpha InvSrcAlpha ``` It must contain exactly two factors, source first: `Zero`, `One`, `SrcColor`, `InvSrcColor`, `DstColor`, `InvDstColor`, `SrcAlpha`, `InvSrcAlpha`, `DstAlpha`, `InvDstAlpha`, `ConstantColor`, `InvConstantColor`, or `SrcAlphaSaturate`. `BlendEquation` defaults to `FuncAdd`. Accepted values are `FuncAdd`, `FuncSubtract`, `FuncReverseSubtract`, `Max`, and `Min`. Use `BlendFunc_PassN` and `BlendEquation_PassN` for pass-local overrides. Unknown values fail effect construction. ### Depth state `DepthWrite` defaults to `True`. `DepthFunc` defaults to `Always`; accepted comparisons are `Always`, `Never`, `Less`, `LessEqual`, `Equal`, `GreaterEqual`, `Greater`, and `NotEqual`. Use `DepthWrite_PassN` and `DepthFunc_PassN` for pass-local overrides. Depth state is active for `EffectUsage::QuadSprite` and, in 3D builds, `EffectUsage::Model` when the target has a depth attachment. UI, primitives, lights, and final blits may draw to targets where depth state has no effect. `DepthVariants` defaults to `False`. Enable it only when a runtime caller must select depth test/write behavior per draw, as Effekseer does for emitter nodes. An opted-in effect builds four states: test/write, test/no-write, no-test/write, and no-test/no-write. The test variants retain the authored `DepthFunc`; no-test uses `Always`. Without the opt-in, a draw may use only the state resolved from the authored `DepthWrite` and `DepthFunc` values. See [Frontend and Rendering](../../explanation/rendering/) for map depth ordering and backend-specific rendering behavior. ### Per-draw culling `CullVariants` defaults to `False`. The default accepts only `CullModeType::None`. Enable it when model or particle callers need to choose `None`, `Back`, or `Front` face culling per draw. Backends that bake culling into device state or pipelines then build the additional variants; a request for a variant the effect did not opt into fails instead of silently drawing with the wrong state. ### Shadow pass In a 3D build, `ShadowPass` defaults to `-1`. Set it to a one-based pass index to mark that pass as the model shadow pass. The index is validated against the compiled pass limit. Runtime model drawing can disable marked shadow passes without disabling the other passes. ## Shader compiler environment Each pass is parsed and linked through glslang as GLSL for Vulkan 1.0 and SPIR-V 1.0. Before `[ShaderCommon]` and the stage body, the baker supplies: ```glsl #version es precision highp float; #define MAX_SCRIPT_VALUES ``` When `FO_ENABLE_3D` is active, it also supplies: ```glsl #define MAX_BONES #define MAX_TEXTURES ``` The linked program must build reflection successfully. Vertex outputs and fragment inputs therefore need compatible locations/types even if one backend would otherwise accept looser source. ## Vertex input contract Effect usage is fixed when a path is first loaded. The shader's input locations must match that usage. ### ImGui, QuadSprite, and Primitive These usages share `Vertex2D`: | Location | GLSL type | Native field | Meaning | |---|---|---|---| | `0` | `vec3` | `PosX`, `PosY`, `PosZ` | position/depth | | `1` | `vec4` | `Color` | normalized vertex color | | `2` | `vec2` | `TexU`, `TexV` | texture coordinate | | `3` | `vec2` | `EggFlags` | egg or draw-path auxiliary data | A shader may omit unused inputs. Keep declared locations/types compatible with the table. ### Model `EffectUsage::Model` uses `Vertex3D`: | Location | GLSL type | Meaning | |---|---|---| | `0` | `vec3` | position | | `1` | `vec3` | normal | | `2` | `vec2` | primary texture coordinate | | `3` | `vec2` | base/secondary texture coordinate | | `4` | `vec3` | tangent | | `5` | `vec3` | bitangent | | `6` | `vec4` | blend weights | | `7` | `vec4` | blend indices | | `8` | `vec4` | normalized vertex color | Model effects exist only in 3D builds. Keep `FO_MODEL_BONES_PER_VERTEX = 4`; active backend layouts assert that shape. ## Descriptor and binding contract ### Native convention Author resources for the native Vulkan convention: - descriptor set `0`: uniform buffers; - descriptor set `1`: combined image samplers. Bindings are explicit integers. Bindings need not be dense for the native path, but they must be unique within one shader stage and resource class. The EffectBaker tests contain sources that omit `set = ...`; glslang can still compile those fixtures. Production effects should state sets explicitly. The native Vulkan backend consumes the original SPIR-V and does not repair an incorrect authored descriptor set. ### SDL_GPU remap The baker makes a copy of native SPIR-V and rewrites descriptor decorations to: | Stage/resource | Descriptor set | |---|---:| | vertex samplers | `0` | | vertex uniform buffers | `1` | | fragment samplers | `2` | | fragment uniform buffers | `3` | Within each class/stage, authored bindings are sorted and rewritten to dense slots `0..N-1`. Each stage is limited to `16` samplers and `4` uniform buffers. The remapped module is emitted as `spv_sdl`; the SDL Metal source is also compiled from this remapped module. Missing bindings, duplicate same-stage bindings, storage images, and dead descriptor declarations fail the bake because the remap cannot be made complete and deterministic. ## Engine-provided textures The runtime recognizes these sampler names: | Sampler | Availability | Producer | |---|---|---| | `MainTex` | all builds | current sprite/render-target/model primary texture | | `IndoorMaskTex` | client map paths | current indoor mask | | `BackgroundTex` | caller-provided direct-scene paths | snapshot of the scene before a refracting draw | | `ModelTex0..ModelTexN-1` | 3D builds | model texture slots | Unknown sampler names are reflected, but `RenderEffect` has no Engine producer for them. A source can therefore compile while the runtime leaves the sampler unbound. Treat the table as the authoring allowlist unless a renderer/runtime change adds a producer in the same change. ## Built-in uniform buffers Uniform block names and byte layouts are fixed. EffectBaker compares reflected sizes to the native structures and rejects unknown blocks. ### General buffers | Block | GLSL shape | Producer/meaning | |---|---|---| | `ProjBuf` | `mat4 ProjMatrix` | current 2D or 3D projection | | `MainTexBuf` | `vec4 MainTexSize` | width, height, reciprocal width/height | | `EggBuf` | `vec4 EggData[3]` | two egg masks plus transition parameter | | `SpriteBorderBuf` | `vec4 SpriteBorder` | sprite atlas UV rectangle | | `ParticleSamplingBuf` | `vec4 ParticleSampling` | per-draw particle sampling, atlas-clamp, distortion, and background-orientation controls | | `TimeBuf` | `vec4 FrameTime; vec4 GameTime` | `.x` seconds, session-relative, wrapped at `8192` | | `RandomValueBuf` | `vec4 RandomValue` | four per-frame random values in `[0,1]` | | `ScriptValueBuf` | `vec4 ScriptValue[MAX_SCRIPT_VALUES / 4]` | project-controlled float slots | | `CameraBuf` | `vec4 MapAnchorScreenPos; vec4 ChunkScreenAnchor` | world/screen affine UV bases | For `CameraBuf`, evaluate either basis as: ```glsl vec2 uv = Basis.xy + TexCoord * Basis.zw; ``` `MapAnchorScreenPos` is world anchored and zoom invariant. `ChunkScreenAnchor` is screen anchored. Do not reduce either to a subtraction; the scale terms account for padded/chunked render targets. `ParticleSamplingBuf` is populated by particle runtimes rather than by the general sprite path. Its component meanings belong to the selected particle effect/runtime pair; the stock Effekseer effects use them for point sampling, atlas-safe clamping, distortion intensity, and vertical orientation of `BackgroundTex`. ### Model buffers | Block | GLSL shape | Meaning | |---|---|---| | `ModelBuf` | `vec4 LightColor; vec4 GroundPosition; mat4 WorldMatrices[MAX_BONES]` | lighting, ground anchor, skin matrices | | `ModelTexBuf` | `vec4 TexAtlasOffset[MAX_TEXTURES]; vec4 TexSize[MAX_TEXTURES]` | atlas transforms and texture dimensions | | `ModelAnimBuf` | `vec4 AnimNormalizedTime; vec4 AnimAbsoluteTime` | normalized and looped absolute animation time | Storage buffers and storage images are unsupported. ## ScriptValueBuf ownership and lifetime `FO_EFFECT_SCRIPT_VALUES` defaults to `16`; an embedding project may override it. The value must be positive and divisible by four. It is a compiled Engine shape and must match the generated/baked shader define. The runtime behavior is: 1. The first load of an effect that declares `ScriptValueBuf` creates a zeroed buffer. 2. Script writes mutate the cached `RenderEffect` object. 3. Values remain until overwritten or explicitly cleared. 4. `Game.SetEffect(...)` changes the selected object but does not clear either object's values. 5. Returning to a previously loaded path returns its previous buffer contents. 6. `Game.ClearEffectScriptValues(...)` zeroes the selected object's whole buffer. The cache key is the resource path only. If the same path is selected in multiple compatible slots, they share one buffer. Projects should assign each slot range one owner and document collisions. Re-pushing values after a variant swap is still a good project practice when variants use different paths or buffer declarations, but it is not a reset guarantee from `SetEffect`. SetEffect does not reset cached values. ## Runtime loading and cache identity `EffectManager::LoadEffect(usage, path)` returns an existing cached object when the path was loaded before. The cache key does not include `EffectUsage`. Therefore the first load fixes the object's usage and backend pipeline/input layout assumptions. Do not reuse one path across incompatible categories: - `ImGui`; - `QuadSprite`; - `Primitive`; - `Model`. Separate resources may contain identical shader text when they need different usages. Path clarity is more important than avoiding a tiny source duplicate. The runtime loads: - the baked `.fofx` source for pass count and render state; - `.fofx-N-info` for reflected native and SDL resource slots; - the backend flavor required by the active renderer. Missing source, metadata, or shader flavor is a load error. ## Script API The client/mapper script surface is: ```angelscript Game.SetEffect(effectType, effectSubtype, effectPath); Game.SetEffectScriptValue(effectType, effectSubtype, valueIndex, value); Game.SetEffectScriptValues( effectType, effectSubtype, valueStartIndex, values, valuesOffset = 0, valuesCount = -1); Game.ClearEffectScriptValues(effectType, effectSubtype); ``` `SetEffect` uses an empty path to restore the slot's default. A non-empty path loads using the default slot's usage. Script-value calls resolve the currently selected target and fail when: - the type/subtype is unsupported or invalid; - the target entity/offscreen slot does not exist; - the effect is not loaded; - the effect does not declare `ScriptValueBuf`; - an input or destination range is out of bounds. Use the ranged method for parameter blocks updated together. It validates `valuesOffset`, derives the remaining count when `valuesCount = -1`, and makes one native write. Per-font ScriptValue writes are not supported; `EffectType::Font` accepts only subtype `-1` for the shared font effect. `GenericSprite` and `CritterSprite` can target the shared slot with subtype `0` or a live entity by id. Offscreen subtypes must have been registered and loaded. ## Baking outputs For each pass and stage, EffectBaker emits: | Flavor | Consumer | |---|---| | `spv` | native Vulkan; source for GLSL/ES/HLSL cross-compilation | | `spv_sdl` | SDL_GPU Vulkan path | | `glsl` | OpenGL desktop (`330`) | | `glsl_es` | OpenGL ES/WebGL (`300 es`) | | `hlsl` | Intermediate HLSL Shader Model `4.0`, compiled at bake time | | `dxbc` | Direct3D 11 bytecode, compiled from HLSL by vendored vkd3d-shader | | `msl_mac` | SDL_GPU Metal on macOS | | `msl_ios` | SDL_GPU Metal on iOS | With `Baking.Direct3DLevel9Shaders = True`, the baker also embeds an `Aon9` level-9.3 bytecode chunk in non-model effects. A shader that exceeds that profile fails baking; 3D-model effects do not receive it. The default is off. This is an opt-in for 2D-only Direct3D builds, not support for 9.1/9.2. Naming is: ```text .fofx--- .fofx--info ``` `[EffectInfo]` stores program-wide reflected bindings and proves built-in uniform-buffer sizes. `[EffectInfoSdl]` stores stage-local dense slots and sampler/UBO counts. The original source is copied to the baked resource path. ## Resource packs and overrides The Engine provides minimal-profile effects in `Resources/Core/Effects/` and bootstrap effects in `Resources/Embedded/Effects/`. An embedding project may provide a later resource with the same path to shadow an Engine default. Keep reusable Engine defaults conservative. Richer project copies may target a higher hardware profile, but the project must validate every shipped backend and preserve a deliberate fallback. See [Frontend and Rendering](../../explanation/rendering/) for the current default slot map and minimal-profile policy. ## Authoring practices - Start from the closest Engine effect with the same `EffectUsage`, vertex inputs, and built-in buffers. - Declare descriptor sets and bindings explicitly, even when a test fixture demonstrates that glslang can infer a set. - Keep pass count small. Every pass multiplies stage compilation, metadata, backend objects, and draw work. - Remove unused samplers and uniform blocks. Dead descriptor declarations are rejected for SDL remapping. - Use only recognized buffer names and copy their layouts exactly. - Keep `ScriptValueBuf` slots centralized in project code/docs, with one owner per range and stable meanings across shader variants. - Prefer `SetEffectScriptValues` for contiguous snapshots and explicit `ClearEffectScriptValues` when reset semantics matter. - Do not use the same path for incompatible effect usages. - Keep animation time periodic. `TimeBuf` wraps at `8192` seconds, and script-maintained float clocks should wrap before precision degrades. - Test depth and blending on a target that actually has the relevant attachments and draw order. - Validate the lowest hardware/profile the project claims to support; a successful cross-compile is not a visual or driver guarantee. ## Failure guide | Symptom | Likely boundary | |---|---| | missing Effect/vertex/fragment error | section spelling, `Passes`, or fallback source | | shader compiler diagnostic | GLSL syntax, version/profile, or stage interface | | invalid uniform buffer size | block field order/type/array length differs from `RenderEffect` | | invalid uniform buffer | unknown block name | | explicit/duplicate binding error | missing or colliding stage-local binding | | unused resource error | declared sampler/UBO is optimized out or never read | | SDL stage-limit error | more than 16 samplers or 4 UBOs in one stage | | effect loads but texture is empty | sampler name has no Engine producer or wrong set/binding | | script-value write throws | wrong target, unloaded effect, no `ScriptValueBuf`, or bad range | | script value appears in another slot | both slots resolve to the same cached path | | switching back restores old tuning | expected path-cache persistence; clear or overwrite explicitly | | works on one renderer only | backend flavor/profile/descriptor/depth difference | ## Validation workflow After changing an Engine effect-format source, buffer, backend, or built-in effect: ```powershell python BuildTools\docs_effect_format.py --write python BuildTools\tests\test_docs_effect_format.py python BuildTools\docs_effect_format.py --check python BuildTools\docs_contract_diff.py --help ``` Run the focused native baker tests through the configured embedding-project unit-test target. `Source/Tests/Test_EffectBaker.cpp` is the owning test file. Renderer state changes also need the relevant backend and visible scene. For an embedding project: 1. regenerate/configure when a compile limit changed; 2. bake resources; 3. run project validators for effect paths, `EffectType` assignments, and ScriptValue ownership; 4. launch representative visible scenes for every affected slot; 5. test every shipped renderer/backend and minimum hardware profile; 6. compare default/fallback and overridden project effects. ## Change checklist When the Engine contract changes, update together: - `BuildTools/EffectFormatInterface.json`; - `Docs/en/how-to/content/effect-format.md`; - generated `Docs/generated/effect-format.json` and reference pages; - `BuildTools/tests/test_docs_effect_format.py`; - `Source/Tests/Test_EffectBaker.cpp` or the owning renderer/runtime test; - [Baking Pipeline](../../explanation/content-pipeline/baking.md) when output/compiler behavior changes; - [Frontend and Rendering](../../explanation/rendering/) when runtime/backend behavior changes; - [GeneratedApiAndMetadata.md](../../reference/metadata/index.md) and [Generated Contract Change Management](../../contributing/contract-change-management.md) when the structured contract or aggregate diff surface changes; - embedding-project docs/tests for paths, slot semantics, fallbacks, and visual validation. ===== END DOCUMENT effect-format-guide ===== ===== BEGIN DOCUMENT image-format-guide ===== Source: Docs/en/how-to/content/image-format.md Canonical URL: https://fonline.ru/Docs/en/how-to/content/image-format.html Content SHA-256: 7677cbe13a997e201531807611dbd70830341ebe3aa65d594090a3090abad56f --- layout: default title: Image And Sprite Formats locale: en document_id: image-format-guide permalink: /Docs/en/how-to/content/image-format.html --- # Image And Sprite Formats FOnline bakes authored and legacy images into a compact client sprite container. The baker can import static RGBA sources, legacy indexed formats, animations, direction sheets, and text `.fofrm` compositions. It can also replace the ordinary full-frame quad with a validated indexed silhouette and crop the serialized RGBA canvas to that geometry. At runtime the stock client reads the versioned container, places concrete frames in a texture atlas, and creates either an `AtlasSprite` or a `SpriteSheet`. Use this guide for authoring decisions and operational behavior. Use the generated [image-format reference](../../reference/image-format/index.md), its [source-format](../../reference/image-format/formats.md), [FOFRM](../../reference/image-format/fofrm.md), [filename-option](../../reference/image-format/options.md), [baking](../../reference/image-format/baking.md), [runtime](../../reference/image-format/runtime.md), and [validation](../../reference/image-format/validation.md) pages, plus the [canonical JSON model](../../../generated/image-format.json), for the exact current-revision contract. ## Scope and authority The owning sources are: - `Source/Tools/ImageBaker.cpp` and `ImageBaker.h` for source discovery, format decoding, FOFRM composition, output naming, per-pack `SpriteInfo`, mesh integration, and serialization; - `Source/Tools/SpriteMeshing.cpp` and `SpriteMeshing.h` for mask construction, candidate generation, scoring, triangulation, and coverage validation; - `Source/Common/SpriteResource.cpp` and `SpriteResource.h` for the versioned shared decoder, frame/mesh records, and `SpriteInfo` index format; - `Source/Client/DefaultSprites.cpp` and `DefaultSprites.h` for baked-container loading, `AtlasSprite`, `SpriteSheet`, frame offsets, and atlas upload; - `Source/Client/SpriteManager.cpp` for extension dispatch and sprite caches; - `Source/Client/TextureAtlas.cpp` for atlas allocation; - `Source/Tests/Test_ImageBaker.cpp` and `Test_TextureAtlas.cpp` for executable import, failure, and allocation examples. `BuildTools/ImageFormatInterface.json` is the source-backed structured contract. `BuildTools/docs_image_format.py` validates every declared source anchor, derives the live baker and runtime extension lists, verifies that the legacy FOFRM `Effect` field is still absent from serialized output, and renders the generated reference. Importer, descriptor, container, factory, atlas, or cache drift must update that model in the same change. This page is reusable Engine documentation. An embedding project owns its concrete asset catalog, licenses, resource-pack precedence, source-file policy, visual style, animation substitutions, hit-test expectations, movement tuning, and visible acceptance baselines. ## Choose a source format | Need | Preferred source | Notes | |---|---|---| | Ordinary static image or transparent sprite | PNG | Recommended project-authored lossless input. Palette, low-bit grayscale, `tRNS`, and 16-bit channels are normalized to RGBA8. | | Existing TrueColor pipeline output | TGA | Use the supported type 2/type 10, 24/32-bpp, no-image-ID, bottom-left-origin subset. | | Multi-frame or directional project sprite | FOFRM referencing PNG/TGA | Keeps composition, offsets, frame deltas, and timing explicit and reviewable. | | Existing Fallout FRM/FR0 asset | FRM or FR0 | Preserve only when the project can redistribute the source and has a visual regression route. | | Existing Tactics ART/SPR asset | FOFRM referencing ART/SPR | Filename options select palettes, frames, mirrors, colors, and sequences. SPR should normally be wrapped because of the default runtime boundary. | | Existing Infinity Engine asset | ZAR/TIL/MOS/BAM, normally through FOFRM when selection is needed | Treat as an import path, not a recommended format for new art. | The built-in image baker does not support JPEG, BMP, GIF, DDS, WebP, AVIF, or SVG. Convert such inputs to PNG or the supported TGA subset before baking. Do not infer support from a renderer or third-party library that happens to know a format; `ImageBaker` and the selected runtime `SpriteFactory` are the contract. ## Pipeline overview The normal path is: 1. Resource packs expose source files through `FileCollection`. 2. `ImageBaker` scans its registered extensions or receives one target path. 3. The selected loader returns a `FrameCollection` containing one common frame count/timing value and either one sequence or a full direction set. 4. `BakeCollection` optionally builds and scores a silhouette mesh for each unique frame, pads or crops the RGBA canvas, and resolves its logical root. 5. It writes the private RGBA/mesh frame container under the source path or a loader-provided `NewName`, and maintains `SpriteInfo/.foinfo`. 6. At client runtime, `SpriteManager` selects a factory from the lowercased extension and `DefaultSpriteFactory` reads the baked bytes. 7. Concrete frames enter the requested texture atlas; animation/direction metadata becomes a `SpriteSheet` when needed. Source decoders do not run in the stock client. A file named `Sprite.png` in a baked resource pack contains the Engine sprite container, not original PNG bytes. The retained extension is dispatch identity, not a promise about the baked payload format. ## FOFRM grammar FOFRM uses the Engine `ConfigFile` parser. The root/default section describes a single-direction sequence. Directional sheets use `[dir_N]` or `[Dir_N]` sections. ### Minimal static image ```ini count = 1 fps = 10 frm = Icon.png ``` The unnumbered `frm`/`Frm` alias is accepted only for reference zero. Numbered keys are clearer and scale to animations: ```ini fps = 8 count = 3 frm_0 = Idle_00.png frm_1 = Idle_01.png frm_2 = Idle_02.png ``` References resolve relative to the `.fofrm` directory. Keep related source frames beside the descriptor or in a stable relative subtree; do not rely on a developer machine's current directory. ### Sequence placement and frame deltas The root or each direction can set signed placement offsets with either naming style: ```ini offs_x = -24 offs_y = -63 ``` or: ```ini OffsetX = -24 OffsetY = -63 ``` Direction offsets inherit the values held while the previous direction was parsed when omitted. This can be useful for legacy data, but it is easy to misread. Production directional descriptors should write both offsets in every direction section. Per-reference deltas use `next_x_N` / `next_y_N` or `NextX_N` / `NextY_N`: ```ini next_x_0 = 2 next_y_0 = -1 ``` The descriptor delta is added to each imported child frame's own `NextX` and `NextY`. These values are not ordinary image placement offsets. Multi-frame runtime sheets retain them as per-frame displacement consumed by specialized presentation code. The detailed walk/run projection and authoritative-movement boundary are documented in [Sprite Root Motion](sprite-root-motion.md). ### Direction sheets A directional descriptor must contain either one sequence or every direction from zero through `GameSettings::MAP_DIR_COUNT - 1`: ```ini fps = 10 count = 2 [dir_0] offs_x = -24 offs_y = -63 frm_0 = Walk_NE_00.png frm_1 = Walk_NE_01.png [dir_1] offs_x = -24 offs_y = -63 frm_0 = Walk_E_00.png frm_1 = Walk_E_01.png # Continue every configured map direction. ``` Every direction must flatten to the same final frame count. A partial direction set, a missing later section, or a different flattened count fails baking. ### Nested references and flattening Each `frm_N` may reference any registered image-loader extension and may include `$` filename options. If a child has several frames, FOFRM appends the child's `Main` sequence to the parent. It does not compose the child's direction sheets or child sequence-level `OffsX`/`OffsY`. A nested child frame that is already a shared record is rejected because its index is not rebased during flattening. The distinction between descriptor count and flattened frame count matters: ```ini fps = 10 count = 2 frm_0 = FirstCycle.bam frm_1 = SecondCycle.bam ``` FOFRM computes whole duration as `1000 * count / fps`, while the runtime divides that duration by the flattened frame count. If each BAM reference contributes several frames, playback is faster than an author may expect. For predictable cadence, reference one static frame per descriptor slot or calculate timing from the actual flattened count and verify it in a visible client. `fps = 0` creates zero whole ticks and deliberately disables normal playback. For a playing multi-frame sheet, keep integer `AnimTicks / frame_count` at least one millisecond; the runtime update loop divides by that value. ### Legacy `Effect` key The parser accepts `effect` and `Effect` into `FrameCollection::EffectName`, but the baker does not serialize or apply it and the stock runtime does not select a shader from it. Treat the key as ignored compatibility input. Select project effects through the owning renderer, prototype, GUI, or script surface documented by [Effect Format](effect-format.md). ## Legacy filename options Options appear after `$` and before the extension. `LoadAny` strips them from the physical lookup path and passes them to the selected loader. ### ART ```text Actor$1THF5-7.art ``` - `0` through `3` select a palette; the last selector wins and an unavailable palette falls back to palette zero. - `T` derives alpha from the maximum RGB component; palette index zero remains fully transparent. - `H` and `V` mirror horizontally and vertically. - `F5` selects frame 5; `F5-7` and `F7-5` select inclusive ascending or descending ranges. Bounds clamp to the available frame table. - Option letters are case-insensitive. Unknown characters are ignored. ART's static flag forces one rotation. Eight-rotation input is remapped to the current map geometry and becomes a complete Engine direction sheet. ### SPR ```text Actor$[1,12,0,0][2,0,-8,4]Walk.spr ``` Parts are `0` other, `1` skin, `2` hair, and `3` armor. RGB offsets are added and clamped to `0..255`. An out-of-range part value applies its RGB values to all parts. Text after the final bracket selects a sequence case-insensitively; an empty name selects the first sequence. SPR imports layered pixels, removes invalid sequence frame indices, and emits shared records for repeated indices. Its cadence is fixed at 10 fps. The stock `DefaultSpriteFactory` does not register `.spr`, even though `ImageBaker` can bake it. Normally reference SPR from a `.fofrm`, so the composed baked output has the registered `.fofrm` extension. Direct `.spr` runtime paths require an explicit custom sprite factory. ### BAM ```text Spell$1.bam Spell$1-3.bam ``` The first integer selects a cycle. An optional integer after `-` selects one frame. Out-of-range cycle or frame values fall back to zero. Omitting the frame selector imports the entire cycle. Negative selected-frame values are treated as the whole-cycle form. ## Source-format details ### PNG PNG is the default recommendation for project-authored images. The loader: - strips 16-bit channels to 8-bit; - expands low-bit grayscale and palette pixels; - expands `tRNS` transparency; - fills missing alpha with 255; - emits one RGBA8 frame. Corrupt input fails through the libpng error callback. Keep source color-space and premultiplication policy explicit in the embedding project; ImageBaker does not provide a project color-management workflow. ### TGA The supported production subset is intentionally narrow: - TrueColor type 2 (raw) or type 10 (RLE); - 24-bit BGR or 32-bit BGRA pixels; - no image ID; - bottom-left origin, because the implementation always flips rows. Indexed, grayscale, and other bit depths fail. The loader does not honor the descriptor origin bit or skip an image ID, so exporting top-origin or ID-bearing files can produce incorrect or rejected output. Prefer PNG unless an existing toolchain has a tested TGA preset. ### Fallout FRM and FR0 FRM reads big-endian frame rate/count, sequence offsets, frame deltas, and one or complete direction tables. A same-basename `.pal` overrides the built-in Fallout palette. With the default palette, animated palette indices can expand one source into a generated color cycle; verify final frame count and cadence. FR0 is the entry point for split `fr0`, `fr1`, and later direction siblings. Once multiple directions begin, a missing later file is an error. Critter paths normalize to lowercase `.fofrm`; other split sets normalize to `.frm`. ### RIX, ZAR, TIL, MOS, and BAM - RIX emits one opaque embedded-palette frame. - ZAR emits one palette-backed raw/RLE frame with alpha. - TIL imports nested ZAR frames as a 10 fps sequence. - MOS/MOSC imports one tiled image, decompressing MOSC first; palette green `0x00FF00` is transparent. - BAM/BAMC imports a selected cycle/frame, decompresses BAMC, supports RLE, derives frame deltas, and treats palette blue 255 as transparent. These are compatibility importers. Do not choose them for new project art when PNG plus FOFRM expresses the same authored intent more clearly. ## Baked container boundary The baked byte stream is a private agreement between `ImageBaker` and `DefaultSpriteFactory`, not a public project serialization format. Conceptually it contains: 1. `SPRITE_RESOURCE_MAGIC` (`43`) and `SPRITE_RESOURCE_VERSION` (`2`); 2. little-endian `uint16` frame count and whole animation ticks; 3. `uint8` direction count (`1` or `GameSettings::MAP_DIR_COUNT`); 4. for every frame in every direction, a shared flag; 5. for a concrete frame, signed `int16` draw offset, `uint16` cropped width/height, signed `int16` `NextX`/`NextY`, and exactly `width * height * 4` RGBA bytes; 6. a `SpriteMeshKind`; a mesh record additionally stores vertex/index counts, logical source size and cropped origin, fixed-width local vertices, and triangle indices; 7. for a shared frame, a `uint16` earlier-frame index; 8. trailing `SPRITE_RESOURCE_MAGIC`. Do not parse or generate this stream in project scripts or external content tools. Feed supported source files through the pinned Engine baker. A container layout change may be coordinated inside one Engine revision without preserving cross-revision byte compatibility. ## Runtime loading, atlas, and caches `SpriteManager` lowercases the path extension and selects a registered `SpriteFactory`. The default factory delegates the complete byte span to `ReadSpriteResource`, which validates magic, version, records, mesh geometry, footer, and trailing data. The factory then validates the one/full-direction invariant. A one-frame, one-direction resource becomes `AtlasSprite`. Its sequence `OffsX`/`OffsY` becomes the sprite placement offset; serialized `NextX`/`NextY` is read but ignored. A multi-frame or directional resource becomes `SpriteSheet`. Each direction owns a parallel sheet and each concrete or shared frame retains its separate displacement in `_sprOffset`. `SpriteSheet` can select a direction, randomize the initial frame with `Prewarm`, set normalized time, and play looped or reversed. `Play` does nothing for one frame or zero whole ticks. Direction changes do not themselves rewrite the common animation clock. Concrete RGBA frames are allocated in the requested `AtlasType`. The factory requires positive dimensions, uploads the image, duplicates one-pixel edges for linear filtering, and creates a hit mask from alpha through `Settings.SpriteHitValue`. Atlas type is also part of the copyable-sprite cache key, so the same path can have separate interface/map/one-image instances. With `SpriteMesh.Enabled`, `ImageBaker` treats `alpha > AlphaThreshold` as the visible mask, searches candidates up to `MaxTriangles`, and scores saved source frame area against submitted triangles using `AreaSavingsWeight`. Every selected mesh must cover all visible pixel cells. Invalid or unprofitable candidates keep the regular quad; an empty mask records explicit empty geometry. Mesh frames may use a cropped or slightly padded texture canvas, but their serialized offset, logical source size, and source origin preserve placement, scaling, map-light interpolation, and hit-test coordinates. The baker validates the complete `SpriteMesh.*` group even when mesh generation is disabled. Standalone project configs must therefore declare a positive `MaxTriangles`, an `AlphaThreshold` in `0..254`, and a finite non-negative `AreaSavingsWeight`; [the minimal project config](../../../../Examples/MinimalProject/FOnlineStarter.fomain) shows the explicit disabled defaults. Ordinary full-image sprite draws submit the baked indexed triangle list. Region crops, tiled patterns, padded custom-effect or outline draws, fonts, blits, runtime model sprites, and particles continue to use their rectangular paths. Use [Frontend and Rendering](../../explanation/rendering/#sprite-and-model-atlas-geometry) for exact draw and atlas-dump behavior. Use [Baking Pipeline](../../explanation/content-pipeline/baking.md#image-and-sprite-mesh-statistics) for candidate policy and baking-report fields. `SpriteInfo/.foinfo` version 1 is a compact per-pack index of duration, directions, frame bounds, offsets, and shared references. Common `EngineMetadata` loads it on server and client without decoding RGBA payloads. Adding or losing that aggregate index is a full-rebake condition. Copyable cache entries store a prototype and return `MakeCopy()` so callers do not share animation state. Missing files, missing extensions, unknown factories, and load failures are separately memoized by path in `_nonFoundSprites`. `CleanupSpriteCache()` does not clear that set. After adding a resource that the running client already failed to load, recreate the sprite manager or restart the client before testing the same path again. ## Authoring practices For new game content: 1. Prefer PNG for source pixels and FOFRM for animation/direction composition. 2. Use lowercase extensions and stable case-correct paths even though extension dispatch itself is lowercased. 3. Keep one semantic animation per descriptor. Avoid deeply nested animated references because count, flattening, and timing become harder to review. 4. Write `fps`, `count`, every `frm_N`, and both offsets explicitly. In a directional file, write offsets in every direction. 5. Keep all direction sections complete and frame counts symmetrical. 6. Treat `NextX`/`NextY` as presentation data. Validate root motion visually and never use it as authoritative movement or collision state. 7. Wrap SPR imports in FOFRM unless the embedding project intentionally owns a custom factory. 8. Convert unsupported or ambiguous TGA exports to PNG. 9. Keep original legacy assets and custom palettes only when licensing permits redistribution; document provenance in the embedding project. 10. Review dimensions, transparent borders, atlas filtering, click/hit masks, direction mapping, cadence, and stop/start behavior in a visible client. 11. After changing `SpriteMesh.*`, container code, or mesh policy, run `ForceBakeResources`; source timestamps cannot prove that existing outputs used the same settings. Inspect `BakingReport.json` and `Game.DumpAtlases()` before accepting triangle cost, padding, crop, or fallback changes. An AI author should prefer explicit FOFRM keys and one-frame PNG references. Before changing an existing legacy option string, inspect the generated option reference and the focused native fixtures rather than guessing from filenames. ## Diagnostics | Symptom | Likely boundary | First checks | |---|---|---| | Targeted bake writes nothing and reports no error | Unsupported/missing target or `BakeChecker` skip | Confirm extension, source pack, path case, timestamp/cache policy, and whether a full scan sees the file. | | `Image file not found` | FOFRM relative reference | Resolve from the descriptor directory and remove `$...` before checking the physical path. | | `FOFRM file invalid data` | Missing slot, unequal directions, empty child, or partial direction set | Compare `count`, every `frm_N`, all direction sections, and flattened child counts. | | `FOFRM file invalid data (shared index)` | Nested child already uses shared records | Flatten from concrete sources or remove the nested animation layer. | | Direct `.spr` path reports unknown extension | Stock runtime factory boundary | Load the baked `.fofrm` wrapper or register a project-owned custom factory. | | Sprite remains missing after the file is added | `_nonFoundSprites` memoization | Restart/recreate the client sprite manager and confirm baked output exists. | | Animation does not play | One frame, zero ticks, or invalid effective cadence | Inspect flattened frame count and `AnimTicks`; calculate integer ticks per frame. | | Direction changes show wrong framing | Inherited/missing offsets or source-direction remap | Make offsets explicit in every section and inspect all directions visibly. | | TGA is flipped or shifted | Unsupported origin/image-ID assumptions | Re-export with the supported preset or convert to PNG. | | Alpha fringe or incorrect hit area | Source alpha, atlas border, or `SpriteHitValue` | Inspect raw alpha and the final atlas-backed sprite in the intended client profile. | During a scan, independent image failures are logged as `Image baking error` and the baker throws one aggregate `Errors during images baking` exception after all selected work completes. Fix every underlying file; the aggregate count is not the root cause. ## Validation workflow For documentation/model changes: ```powershell python BuildTools\docs_image_format.py --write python BuildTools\docs_image_format.py --check python -m unittest BuildTools.tests.test_docs_image_format python BuildTools\docs_contract_diff.py --help python BuildTools\docs_validate.py ``` For importer, container, atlas, or playback changes, run the focused native image-baker and texture-atlas coverage through the configured `RunUnitTests` target. `Test_ImageBaker.cpp` covers targeted/scan baking, BakeChecker behavior, PNG/TGA, every legacy importer, options, FOFRM flattening/directions, shared records, output renaming, and malformed inputs. `Test_TextureAtlas.cpp` covers allocator split/search/free behavior. Then validate an affected embedding project: 1. regenerate the image-format model/reference and review the aggregate `image-format` contract diff; 2. rebake affected resources with the project's pinned Engine revision; 3. run the narrow project resource/prototype checks that resolve those paths; 4. launch a visible client scene that exercises every changed image, animation, direction, mirror, alpha edge, hit area, and supported renderer/profile; 5. for locomotion offsets, also follow [Sprite Root Motion](sprite-root-motion.md) and inspect straight movement, turns, direction changes, and stop/start transitions. Native tests prove decoding and container invariants. They do not prove project art framing, resource-pack precedence, visual cadence, filtering, clickability, or perceived foot sliding. ## Change checklist When changing an image surface: - update `BuildTools/ImageFormatInterface.json` and this guide in the same Engine change; - regenerate `Docs/generated/image-format.json` and all generated image pages; - run `BuildTools/tests/test_docs_image_format.py`, the complete documentation suite, and `docs_contract_diff.py` against the intended base; - run focused native tests for the touched loader/runtime/atlas boundary; - update [Baking Pipeline](../../explanation/content-pipeline/baking.md), [Client Runtime](../../explanation/runtime/client.md), or [Sprite Root Motion](sprite-root-motion.md) only when their owned behavior changes; - update embedding-project docs only for concrete assets, policy, integration, and visible validation changes; - keep public examples pinned to an exact Engine revision and do not promise support for a format or option that their validation route does not exercise. ===== END DOCUMENT image-format-guide ===== ===== BEGIN DOCUMENT particle-format-guide ===== Source: Docs/en/how-to/content/particle-format.md Canonical URL: https://fonline.ru/Docs/en/how-to/content/particle-format.html Content SHA-256: 4cd0f52678a178f1a7664a8df5eabe17f0709873328ef7f0b91ca8906d8be4c0 --- layout: default title: Particle Authoring And Runtime locale: en document_id: particle-format-guide permalink: /Docs/en/how-to/content/particle-format.html --- # Particle Authoring And Runtime FOnline provides two optional particle backends behind one client runtime: SPARK and Effekseer. They have different authoring tools and source formats, but both are compiled by `ParticleBaker`, exposed as particle sprites, and controlled through the same `ParticleSystem` facade. Use this guide for reusable Engine behavior. Use the generated [particle-format reference](../../reference/particle-format/index.md) and [canonical JSON model](../../../generated/particle-format.json) for the exact source-backed contract. Use [Particle Authoring Tools](../tools/particle-authoring.md) for the operational Mapper/SPARK/Effekseer/Viewer workflow and versioned screenshots. An embedding project must separately document its enabled backend, particle catalog, visual policy, resource provenance, performance budgets, and acceptance scenes. ## Build-time selection Both backends default to `OFF`. Select them explicitly before project generation: ```cmake SetOptionValues( FO_SPARK_PARTICLES ON FO_EFFEKSEER_PARTICLES OFF ) ``` The options are independent: - `FO_SPARK_PARTICLES` enables `.spark` baking, `.spk` runtime loading, and the Mapper SPARK authoring subeditor. - `FO_EFFEKSEER_PARTICLES` enables `.efkproj` compilation and `.efk` runtime loading. `CreateParticleRuntimeBackends` is the feature-aware composition point. The backend-neutral `ParticleManager`, `ParticleSystem`, `ParticleSprite`, and Mapper preview do not choose a backend themselves; they discover the runtime extensions reported by the enabled backends. Do not enable a backend merely because the Engine can compile it. A production project should ship only backends covered by its content, platform, packaging, performance, and visible-rendering tests. ## Resource pipeline The authored and runtime forms are deliberately different: | Backend | Authored source | Baked runtime | Runtime reference | |---|---|---|---| | SPARK | `.spark` XML | `.spark` -> `.spk` | `.spk` | | Effekseer | `.efkproj` XML | `.efkproj` -> `.efk` | `.efk` | Authored `.spk` and `.efk` files are rejected. Generated binaries belong only in baking output and packages; all editable changes return to `.spark` or `.efkproj`. `ParticleBaker` participates in ordinary full and target-specific baking. A runtime request never falls back to an authoring file. When a backend is disabled, its source format is not baked and its runtime extension is not advertised by `ParticleSpriteFactory`. Both baked forms carry mandatory measured bounds. The baker simulates a deterministic instance, records the particle-position box separately from the largest camera-facing billboard radius, and stores the result in `.spk` or in an Engine trailer appended to `.efk`. Runtime sprite framing and model visibility use these values instead of an authored draw rectangle. Rebake particles after updating to a revision that introduces or changes this contract; an old `.efk` without a valid bounds trailer is rejected. ### Incremental Effekseer baking Effekseer projects may reference textures, models, curves, and other files. After a successful compile, the baker stores a project-and-dependency snapshot under the baking cache. A changed, removed, or renamed dependency invalidates each project that references it without forcing unrelated effects to rebuild. The snapshot follows the physical directory source selected for the project. Effekseer project sources therefore must come from a directory-backed resource source, and dependency paths must remain relative and inside that source. After changing `EffekseerCompiler` behavior, force a full resource bake so all generated `.efk` files are refreshed. ## SPARK authoring A `.spark` file is a SPARK object graph serialized as XML. The normal graph has one `System`, one or more `Group` objects, emitters and modifiers, and the Engine-owned `SparkQuadRenderer`. The baker: 1. loads the source through the vendored SPARK XML loader; 2. validates renderer texture paths; 3. serializes the graph to deterministic `.spk` binary data. Use only object types registered by the Engine build and exposed by the Mapper SPARK editor. An upstream SPARK type is not an FOnline authoring contract merely because its implementation exists in the dependency. The editor decodes texture previews through the same versioned `SpriteResource` reader as the runtime-facing image path. It requires exactly one direction and one frame, rejects shared-frame records, and reconstructs the logical image when mesh cropping is present. Do not duplicate baked sprite magic values or private byte offsets in a tool parser. ### Paths and rendering `SparkQuadRenderer` stores the FOnline effect and texture names, atlas dimensions, orientation data, and the `DrawInScene` route. Texture paths must: - be relative; - contain no tab or line-control characters; - remain inside the resource source that owns the `.spark` file. Use the atlas route for ordinary sprite particles. Use `DrawInScene` when the effect must share map projection and depth behavior with the scene. If any renderer in the system requests direct-scene drawing, the complete particle sprite uses that route. There is no manual `draw size` attribute. Baking runs a throwaway copy across its bounded simulated lifetime and rejects a SPARK system that never produces a visible particle. The measured position box and billboard radius determine the atlas frame automatically. Effect state and image decoding follow [Effect Format](effect-format.md) and [Image And Sprite Formats](image-format.md). ## Effekseer authoring Author Effekseer effects as text `.efkproj` files with the bundled standalone Effekseer Editor. Its Windows payload is built outside the normal Engine target graph through: ```powershell $env:FO_OUTPUT = (Get-Location).Path python BuildTools\buildtools.py build-auxiliary effekseer-editor Release ``` The exact arguments are part of the BuildTools CLI and may be wrapped by an embedding project's tasks. The authoring editor is not a runtime dependency and must not be included in production game packages. `EffekseerCompiler` consumes the project XML through a fixed supported profile, emits raw `SKFE` data, and `ParticleBaker` verifies the result with the vendored Effekseer Core before publishing `.efk`. Supported drawing nodes are `Sprite`, `Ring`, `Ribbon`, `Track`, and `Model`; `Root` and `None` may organize the graph. The callback renderers support: - Normal, Add, and Sub blending; - nearest or linear filtering and Clamp or Repeat wrapping; - per-node depth test/write, with Engine effect depth variants; - stable camera-depth sorting for Sprite and Ring; - static Model resources and per-model face culling; - scene distortion on Sprite nodes through a deferred background snapshot. Unsupported features fail closed instead of silently degrading. The runtime rejects GPU particles, normal textures, sounds, custom materials, external curves, procedural models, multiply blending, mirrored wrapping, advanced texture/material slots, soft particles, falloff, flipbook interpolation, Z-sorted strips or models, and distortion on non-Sprite nodes. A referenced static model must also pass Engine validation. The editor's preview proves authoring-tool behavior only. The FOnline runtime uses Effekseer CPU simulation with Engine-owned callback geometry, effects, textures, depth, and graphics backends. Always repeat the check in Mapper and a real client scene. ## Mapper workflow Mapper has two distinct particle tools: - **Particle preview** is backend-neutral. It lists baked `.spk` and `.efk` resources reported by enabled backends and runs them through the same `ParticleSystem` and renderer used by the client. - **SPARK particle editor** browses raw `.spark` sources, edits their graph, saves XML, rebakes the matching `.spk`, invalidates cached data, and recreates the preview. The preview supports resource filtering, placement at the mouse hex or map center, restart, removal, explicit seed, transient scale and offset, and optional prewarm. Its temporary `MapSprite` is not serialized and must not dirty the map. Use the standalone Effekseer Editor for `.efkproj`; Mapper does not edit those sources. Mapper is nevertheless the required final authoring preview because it exercises the generated `.efk` and the real FOnline rendering bridge. See [Particle Authoring Tools](../tools/particle-authoring.md) for the complete operator workflow and [Mapper Tools](../tools/mapper.md#interactive-particle-preview) for automation/integration details. ## Runtime contract `ParticleRuntimeBackend` owns extension routing, resource creation, and cache invalidation. `ParticleRuntimeSystem` owns backend-specific simulation and drawing. `ParticleSystem` adds the shared timing and control facade: - `Setup` applies projection, world transform, position/view offsets, look direction, scale, map camera angle, and projection tilt; - `Respawn(seed)` supports deterministic playback; - `Prewarm` advances an effect before ordinary playback; - `SetScale` reapplies runtime setup without replacing the effect; - `GetBakedBounds` exposes the validated position box and billboard radius; - `GetLiveBounds` returns transformed baked bounds only while particles are visible; - `ComputeSpriteFrame` projects baked bounds through the map camera and derives sprite allocation, emitter offset, and world transform; - `GetDrawInScene` selects the atlas or direct-scene rendering route. `ParticleBounds3D` deliberately separates two quantities. The position box follows the complete emitter placement. The billboard radius follows placement scale but remains camera-facing, so callers add it as view-plane padding and do not rotate it as another world-space point. Attached model particles expand the model frame only while their backend reports live particles or instances. The same seed is deterministic only with the same resource, backend revision, setup, and update sequence. Do not use seeded replay as a cross-version visual compatibility promise. Effekseer currently uses direct-scene rendering. SPARK may use either atlas or direct-scene rendering. Both routes still depend on the embedding project's effect state, image resources, camera settings, and draw order. ## Integration `ParticleSpriteFactory` exposes all enabled runtime extensions to the generic sprite loader. A map sprite or client script therefore references a baked `.spk` or `.efk` path and receives normal particle routing. Client script exports provide seeded playback, prewarm, and scale controls. `Critter.RunParticle` starts a live particle on a model bone in 3D-enabled builds. Model descriptions attach a baked particle with `AttachParticles`: ```text Layer 8 Value 1 AttachParticles Particles/Jet.spk Link Backpack MoveY 0.15 RotY 90 ``` The model baker validates that the baked particle exists and that the target bone is valid. The client creates an independent runtime system and updates its transform from the owning joint. See [Model Format](model-format.md) for the full attachment grammar. ## Production practices - Pick one backend unless a migration or comparison has an explicit end date. Every additional backend multiplies package, platform, test, and support work. - Keep authored sources and dependencies together with stable relative paths. Never hand-edit or version generated `.spk` and `.efk` files. - Use explicit seeds in previews and regression scenes so visual comparisons are reproducible. - Keep fast authoring preview separate from acceptance. Validate gameplay lifetime, transforms, depth, clipping, visibility, and frame cost in a representative client scene. - Record asset provenance and licenses in the embedding project. Vendored runtimes do not grant rights to third-party particle samples. - Define budgets for active systems, emitted particles, callback geometry, overdraw, atlas area, texture memory, and prewarm time. - Treat warnings, missing textures, unsupported capabilities, non-finite geometry, and stale dependency output as release blockers. ## Validation After changing the particle contract or documentation: ```powershell python BuildTools\docs_particle_format.py --write python -m unittest BuildTools.tests.test_docs_particle_format python BuildTools\docs_contract_diff.py --base-ref ``` After changing native particle code, configure both relevant feature lanes and run the focused native tests, including `Test_ParticleBaker.cpp` and, for Effekseer runtime changes, `Test_EffekseerParticleRuntime.cpp`. An embedding project must then: 1. rebake the affected resources; 2. run its particle/content validation; 3. inspect the baked resource in Mapper; 4. inspect every affected sprite, map, script, and model-bone route in a visible client scene; 5. compare performance against its documented production budget. A clean bake proves source conversion and static validation. It does not prove that an effect looks correct, integrates with gameplay timing, or fits a production frame budget. ===== END DOCUMENT particle-format-guide ===== ===== BEGIN DOCUMENT particle-authoring-tools ===== Source: Docs/en/how-to/tools/particle-authoring.md Canonical URL: https://fonline.ru/Docs/en/how-to/tools/particle-authoring.html Content SHA-256: 9dbc060c7f534201f014e8fe86db20133ec83a55bf01eda4a437a2401535dc8c --- layout: default title: Particle Authoring Tools locale: en document_id: particle-authoring-tools permalink: /Docs/en/how-to/tools/particle-authoring.html --- # Particle Authoring Tools > Engine-owned workflow for Particle Preview, the built-in SPARK source > editor, external Effekseer authoring, and focused viewers. Project particle > catalogs, art direction, budgets, asset licenses, and acceptance scenes > belong in the embedding game. Use [Particle Format](../content/particle-format.md) for the exact `.spark`, `.spk`, `.efkproj`, `.efk`, baker, bounds, cache, and runtime contract. Use this page to operate the authoring tools. Use [Viewer Tools](animation-particle-viewers.md) for the standalone Particle Viewer. ## Complete authoring route 1. In Mapper, keep the Map browser, Controls, Workspace, Inspector, and History surfaces visible while selecting and placing content. 2. For SPARK source, open the built-in editor, use Adding mode or Removing mode, and finish explicitly with Save or Discard. Effekseer source remains in the pinned external editor. 3. Re-bake, then pin the backend, resource, seed, prewarm, direction, and replay state before comparing the focused preview and runtime routes. 4. For review evidence, capture the visible Mapper application window with a platform screenshot tool. Keep that UI proof separate from the map-only TGA produced by `Game.SaveMapperScreenshot`. ## Choose the backend before authoring Enable only the backend the project intends to ship: ```cmake SetOption(FO_SPARK_PARTICLES ON) SetOption(FO_EFFEKSEER_PARTICLES OFF) ``` | Backend | Editable source | Baked runtime | Authoring UI | |---|---|---|---| | SPARK | `.spark` XML | `.spk` | Mapper -> Windows -> SPARK particle editor | | Effekseer | `.efkproj` | `.efk` | Pinned external Effekseer editor | Particle Preview and Particle Viewer are backend-neutral: they list baked runtime resources from every enabled backend. The SPARK editor is source-specific. It edits `.spark`; never hand-edit `.spk` or `.efk`. After changing a source, bake before runtime or Mapper preview validation: ```bash cmake --build Build/ --config RelWithDebInfo --target BakeResources ``` Use `ForceBakeResources` when dependency invalidation is under investigation, not as the normal substitute for a correct resource graph. ## Particle Preview in Mapper Open **Windows -> Particle preview**. The window appears only when at least one particle factory extension is active. The preview: - searches baked `.spk` and `.efk` resources; - can refresh the list after a bake; - places the selected effect at the mouse hex or current view center; - accepts scale from `0.01` through `100`; - applies X/Y offsets; - accepts a deterministic integer seed when the backend supports it; - optionally prewarms the system; - exposes **Play**, **Restart**, and **Remove**; - displays the active map hex. Scale, offset, seed, and prewarm changes take effect on **Play** or **Restart**, not by mutating an already-running instance in place. Use **Remove** before comparing unrelated effects so old systems do not overlap. Middle-click rotates the preview direction in the map context. Controls also shows the current preview direction. Map placement is useful for occlusion, depth, lighting, and scale checks that an isolated viewer cannot provide. ### Reproducible startup preview The Mapper settings can open one baked effect automatically: ```ini Mapper.StartMap = TutorialMap Mapper.ParticlePreviewEffect = Documentation.spk Mapper.ParticlePreviewScale = 1.0 Mapper.ParticlePreviewSeed = 20260731 Mapper.ParticlePreviewPrewarm = True ``` Use a fixed seed and capture viewport for documentation and visual regression evidence. A deterministic seed does not make frame timing deterministic across all renderers; record backend, warmup, resolution, and Engine revision too. ## SPARK source browser Build with `FO_SPARK_PARTICLES`, then open **Windows -> SPARK particle editor**. The source browser scans raw resource inputs for `.spark`, not baking output. It shows the source count and number of open editors, supports case-insensitive filtering, and refreshes after source tree changes. Select a source to open one editor per asset path. Selecting an already-open source brings its editor to the front instead of creating a second mutable copy. For deterministic automation or documentation capture, set the authored source explicitly: ```ini Mapper.SparkEditorSource = Documentation.spark ``` Mapper validates the value against raw `.spark` inputs and fails startup with a source-specific error when the asset is absent. The setting opens the editor directly without also leaving the source browser visible. If a `.spk` appears but its `.spark` source does not, the project has lost the editable authority or configured the wrong input roots. Restore the source; do not reverse-engineer the baked blob as the normal workflow. ## SPARK editor
FOnline SPARK Particle Editor at 1280 by 800 showing Documentation.spark, Adding and Removing modes, Auto replay and direction controls, a live 200-pixel preview, and the expandable Groups hierarchy with DocumentationGroup.
The Mapper opens the authored Documentation.spark source, not the baked .spk output. The editor combines a live preview with adding, removing, and naming modes plus the editable System and Group object hierarchy.
The header controls: | Control | Purpose | |---|---| | **Adding mode** | Shows controls for adding supported child objects and collection entries. | | **Removing mode** | Shows remove controls beside editable objects and entries. | | **Naming mode** | Exposes object-name editing where SPARK supports names. | | **Auto replay** | Respawns the preview after the effect completes. | | **Elapsed** | Shows preview time. | | **Dir angle** | Rotates preview direction. | | **Respawn** | Recreates the preview immediately. | The editor keeps a source backup when opened. **Save** serializes the current SPARK system back to the raw `.spark`, reindexes resources, and invalidates the corresponding `.spk` so the next bake cannot silently reuse stale runtime data. **Discard** restores the open backup. Closing a changed editor asks whether to save, discard, or cancel. ### Object hierarchy The root is a SPARK `System` containing Groups. A Group owns particle capacity, lifetime, initializers/interpolators, emitters, modifiers, actions, and one renderer. Transformable objects expose position/orientation fields where the SPARK type supports them. The built-in editor covers: - System and Group; - default, random, simple, and graph float/color initializers and interpolators; - Point, Sphere, Plane, Ring, Box, and Cylinder zones; - Static, Random, Straight, Spheric, and Normal emitters; - Gravity, Friction, Obstacle, Rotator, Collider, Destroyer, Vortex, EmitterAttacher, PointMass, RandomForce, and LinearForce modifiers; - ActionSet and SpawnParticlesAction; - `SparkQuadRenderer`. Adding mode creates a valid default object and inserts it into the selected owner. Removing mode can sever references; review emitter/action/group relationships before save. Naming mode helps large systems, but names are not a replacement for a project particle catalog and stable source path. ### Texture and effect selection `SparkQuadRenderer` stores the effect and texture resource names consumed by the baked/runtime system. The editor's texture picker enumerates `.tga` files beside the `.spark` source. An existing valid PNG reference can still load and preview after baking, but it is not offered by that picker. Prefer a project convention that matches the tool, or edit/review the source deliberately when retaining PNG. Texture preview uses the canonical baked sprite parser. It requires exactly one direction and one frame, then reconstructs the logical image when sprite mesh cropping is enabled. A raw PNG/TGA byte stream is not a baked sprite resource and must not be fed to that loader. Effects must be baked `.fofx` resources compatible with particle rendering. Validate alpha/depth/blend state in [Effect Format](../content/effect-format.md), then inspect the result on every supported renderer. ## Minimal SPARK source The smallest useful loop has a System, one Group, an emitter, and a `SparkQuadRenderer`: ```xml ``` This is the checked-in `Examples/MinimalMultiplayer/Particles/Documentation.spark` fixture. The negative tank means unlimited emission; positive flow controls emissions per second. Production effects should choose capacity, lifetime, flow, bounds, and renderer state from measured visual/performance requirements rather than copy these tutorial values. ## Effekseer authoring FOnline does not embed the Effekseer editor. Use the Engine-pinned Effekseer `1.80.5` toolchain: 1. Open or create `.efkproj` in the pinned external editor. 2. Keep referenced textures/models/materials in project-owned resource inputs. 3. Save the editable `.efkproj`. 4. Run `BakeResources`; `EffekseerCompiler` emits `.efk` plus mandatory Engine bounds metadata. 5. Open the baked `.efk` in Mapper Particle Preview. 6. Open it in Particle Viewer for isolated playback, camera, background, wireframe, and viewport checks. 7. Validate it in a representative runtime map and on every supported backend. Do not substitute a newer editor/compiler merely because it opens the file. Project files and compiled payloads are version-sensitive. Upgrade the pinned toolchain as a reviewed Engine dependency change with fixture rebakes and visual comparison. The Mapper has no Effekseer source editor. A missing `.efkproj` cannot be fixed inside Mapper; restore the project source. ## Particle Viewer Particle Viewer can run standalone or inside Mapper. It provides a focused resource list, playback/restart, deterministic seed, prewarm, scale, offset, direction, viewport/background controls, and wireframe inspection without map content. Use it for: - effect-local framing and measured bounds; - repeatable backend comparisons; - spotting clipping, unexpected billboard size, and depth artifacts; - separating particle failures from map/prototype/lighting failures. Use Mapper preview afterward because isolated success does not prove map placement, occlusion, or world scale. Full controls and target names are in [Viewer Tools](animation-particle-viewers.md). ## Validation workflow For each changed effect: 1. Validate the source XML/project in its owning editor. 2. Run `ForceBakeResources` once when proving a clean source-to-runtime path. 3. Confirm the expected `.spk` or `.efk` appears and no authored runtime blob exists in source. 4. Preview with a fixed seed and documented prewarm. 5. Restart several times; inspect one-shot and loop completion. 6. Test minimum and maximum project scale/offset/direction. 7. Inspect Particle Viewer bounds and wireframe. 8. Place it on a representative indoor and outdoor map where applicable. 9. Test every supported renderer/platform. 10. Review logs for missing effect/texture, parser, bounds, cache, and native backend failures. 11. Record screenshot/video evidence with revision and asset provenance for a user-visible release change. `BakeResources` proves conversion. It does not prove artistic timing, visual readability, overdraw, platform performance, correct ownership, or cleanup. ## Common failures | Symptom | Likely cause and next check | |---|---| | Particle Preview is absent | No particle backend was enabled at configure time. | | Source exists but runtime resource is absent | Wrong resource pack, disabled backend, failed bake, or source extension mismatch. | | `.spk`/`.efk` exists in source control | Generated runtime output was authored or copied back; remove it and restore editable source. | | SPARK browser has no entry | `.spark` is outside raw resource input roots or filter text excludes it. | | SPARK editor opens but preview fails | Missing baked `.spk`, effect, or texture; invalid source; wrong baked sprite cardinality; inspect the log. | | Effect disappears before capture | One-shot tank/lifetime completed; use Auto replay, Respawn, or a deliberate loop fixture. | | Scale/seed changes seem ignored | Use Play or Restart after changing controls. | | Effekseer source cannot open in Mapper | Expected: Mapper previews `.efk`; edit `.efkproj` in the pinned external editor. | | Effect clips in Viewer or model attachment | Rebake measured bounds and inspect source scale, billboard radius, model link, and old cached runtime output. | | Different result on another renderer | Compare effect state, texture filtering/orientation, depth, timing, and backend support using the same seed and viewport. | ## Production practices - Keep source and runtime extensions visually distinct in docs, scripts, and resource packs. - Give effects stable descriptive paths; do not encode temporary task IDs. - Keep texture/effect dependencies close enough for ownership and license review. - Prefer bounded capacities and measured loops over unconstrained visual load. - Use fixed seeds only for repeatable evidence; preserve intended runtime randomness where the game needs it. - Test cleanup when maps unload, entities disappear, viewers restart, and editor windows close. - Keep a small permissively licensed example effect independent of any game project. The minimal multiplayer fixture serves that role. - Reconcile the Particle Format guide, this manual, focused tests, examples, and screenshots whenever particle/editor source changes or an Engine pin moves. ## Ownership boundary The Engine owns backend integration, source/runtime formats, the baker, Particle Preview, SPARK editor, focused Viewer, script/runtime facade, and reusable diagnostics. The embedding game owns: - which backend is enabled and supported; - concrete effects and dependencies; - style, readability, accessibility, and content ratings; - GPU/CPU/overdraw budgets; - asset licenses and provenance; - map/model attachment policy; - platform acceptance scenes and release evidence. Examples and screenshots demonstrate the tool contract. They do not define a production game's visual policy. ===== END DOCUMENT particle-authoring-tools ===== ===== BEGIN DOCUMENT mapper-interactive-manual ===== Source: Docs/en/how-to/tools/mapper-interactive.md Canonical URL: https://fonline.ru/Docs/en/how-to/tools/mapper-interactive.html Content SHA-256: 8984833f19d34fa517cfb1e44de7682c5f0b5528a60f586d90a576da06d37202 --- layout: default title: Mapper Interactive Manual locale: en document_id: mapper-interactive-manual permalink: /Docs/en/how-to/tools/mapper-interactive.html --- # Mapper Interactive Manual > Engine-owned manual for the stock interactive Mapper. Project-specific map > catalogs, prototypes, editor tabs, scripts, validation rules, and release > acceptance belong in the embedding game. Use this page when editing maps by hand. Use [Mapper Tools](mapper.md) for mapper-side AngelScript, headless map processing, render capture, and automation APIs. Use [Map Format](../content/map-format.md) for the serialized `.fomap` contract and [Particle Authoring Tools](particle-authoring.md) for the particle windows shown below. ## Build and launch Enable the Mapper in the embedding project's configure preset and build both resources and the application: ```bash cmake --build Build/ --config RelWithDebInfo --target BakeResources _Mapper ``` Launch the generated Mapper with the project's main config. A startup map is optional: ```bash /Binaries/Mapper--/_Mapper \ -ApplyConfig \ -Mapper.StartMap ``` The executable consumes baked client resources for rendering and the raw input directories declared by resource packs for map and source-asset editing. If a prototype, image, effect, particle, or script changed, bake before judging the editor. A successful executable launch does not prove that its resources are current. `Mapper.StartMap` names a declared `[ProtoMap]`, not necessarily a filename. `Mapper.StartHexX` and `Mapper.StartHexY` can move the initial camera when both are positive. Project launch tasks should own exact paths and subconfigs rather than asking every author to reconstruct them. ## Screen orientation
FOnline Mapper at 1280 by 800 showing the Workspace and Controls windows, the Particle Preview panel with Documentation.spk selected, deterministic seed and prewarm controls, and the live radiation particle centered on TutorialMap.
Mapper capture from the minimal multiplayer example. Particle Preview selects the baked Documentation.spk resource, places it at the view center, and exposes deterministic scale, offset, seed, and prewarm controls beside the normal Workspace and Controls windows.
The main viewport is the map. The menu bar and floating ImGui windows are tools over that viewport; they are not serialized into the map. Window positions and visibility are per-user tool settings. A restored layout may therefore differ from the first launch. `Windows -> Settings -> Reset layout` returns tool windows to their first-use positions without changing map data. The normal work surfaces are: | Surface | Purpose | |---|---| | **Map browser** | Filter every declared map, open it, and distinguish the current (`*`) and already loaded (`+`) entries. | | **Controls** | Inspect the current map, mouse hex, time, FPS, tile layer, zoom, visibility, selection policy, roof preview, and critter direction. | | **Workspace** | Filter and place item, tile, critter, and project-defined prototypes through tabs and subtabs. | | **Content** | Inspect container contents, loaded maps, map creation/loading/saving, resize controls, and mapper messages. | | **Inspector** | Edit the selected entity's typed properties, arrays, and structs; reset values to the prototype and optionally apply an edit to all selected peers. | | **History** | Inspect and jump through the current map's undo/redo history. | | **Console** | Run mapper commands and review command history. | Map browser and Controls start visible in a fresh layout. Workspace remains available from the **Windows** menu; Content uses `Shift+F7`. `F7` hides or restores the entire ImGui interface so the map can be inspected or captured without editor chrome. Because the menu bar is hidden too, `F7` is the only way back from that mode. The Inspector appears with `F9` when an entity or container item is selected. ## Menu reference ### File | Command | Behavior | |---|---| | **Save current** (`Ctrl+S`) | Serialize the current map through its resolved source container. | | **Reset changes** | Reload the current map from its last saved source state. | | **Exit** | Request normal Mapper shutdown. | A dirty current map adds a visible `*** Save ***` button at the right edge of the menu bar. Treat that marker as unsaved authored state. Save intentionally; do not assume process exit commits it. ### Windows The menu opens Workspace, Content, Console, Critter animations, Animation viewer, Particle viewer, Script call, Map browser, Controls, History, particle backend tools, and Settings. Backend entries are conditional: - **Particle preview** appears when at least one particle runtime backend is enabled. - **SPARK particle editor** appears only with `FO_SPARK_PARTICLES`. - Effekseer authoring remains in the external pinned Effekseer editor; Mapper previews its baked `.efk` output. The focused standalone viewers are documented in [Animation and Particle Viewers](animation-particle-viewers.md). ### Edit Undo and redo show the current operation label when one exists. Select all, clear selection, delete, copy, cut, and paste operate on mapper entities and participate in map history. Copy/paste uses the Mapper's in-process entity buffer, not an interchange format or the operating-system clipboard. ### View Visibility toggles cover Items, Scenery, Walls, Critters, Tiles, Roof, and Fast. Changing one rebuilds the current map so the result is immediate. `Axial grid selection` chooses the selection lattice. `Select entire entity` controls whether selection expands from a visual component to its owning entity. Visibility is an authoring aid. It does not remove content or prove runtime visibility, blocking, lighting, or ownership. When `Map.ScrollAxialArea` is nonzero, camera clamping keeps one complete map hex inside each configured edge, so the boundary row itself is outside the view. A zero rectangle leaves the whole map scrollable. ### Tools | Command | Intended use | |---|---| | **Rebuild map** | Recreate current map presentation after relevant data or visibility changes. | | **Mark blocked hexes** | Visualize blocked cells for authoring inspection. | | **Reverse lights** | Run the mapper reverse-light command. | | **Merge by command / Break by command** | Run project command handlers for item composition. | | **Merge multihex items / Break multihex items** | Convert between compatible item sets and multihex meshes. | Merge and break are structural changes. Review selection, ownership, offsets, blocking, and undo history before saving. ### System System toggles fullscreen (`F11`), minimizes (`F12`), dumps texture atlases, and controls edge scrolling for the active window mode. Atlas dumps are diagnostic output; they are not authored resources. ## Open and inspect a map 1. Bake the current project resources. 2. Launch the project-owned Mapper task or executable. 3. Open **Map browser**, filter by declared map name, and select the map. 4. Confirm the map name in **Controls**. 5. Inspect size, work hex, fixed/outside behavior, and authored sections against [Map Format](../content/map-format.md). 6. Toggle one content class at a time when a dense map is hard to read. 7. Open **Content** to confirm loaded-map state and source destination before editing. The Mapper may keep multiple maps loaded while showing one current map. Current, loaded, and saved are separate states. Closing a tab or changing the current map does not silently make another map's dirty state authoritative. ## Place and edit entities Workspace tabs are driven by loaded prototypes and project script customization. The stock modes include Item, Tile, Critter, Fast, Ignore, Inventory, Messages, Maps, and ten custom slots. 1. Open Workspace from the **Windows** menu. 2. Choose a tab and subtab. 3. Filter by prototype name when the collection is large. 4. Select a prototype preview to enter placement mode. 5. Left-click a valid map hex to place it. 6. Right-click or press `Escape` to leave placement mode. 7. Select the new entity and press `F9` to inspect instance properties. 8. Save only after validating direction, offsets, ownership, blocking, and project-required fields. The Inspector parses values through the same property system used by mapper automation. It supports scalar values, arrays, and registered structs. Invalid text is not a partial edit. `PageUp` and `PageDown` move through property rows; `Escape` first cancels the active property edit, then clears selection or placement. Use **Apply to all** only for a deliberately homogeneous selection. A property with the same display name can still carry prototype-specific meaning in the embedding project. ## Selection, movement, and clipboard Left-click selects or places according to the current mode. Drag selection and movement are committed as history entries when the interaction finishes. Right-drag pans the map and preserves inertial motion; a right click without a pan cancels placement or selection context. Arrow keys scroll. Middle-click invokes the current context action: it can rotate selected critters or the particle preview direction, and returns zoom to `1.0`. Use Controls when direction or current zoom must be explicit. Copy, cut, and paste preserve mapper entity data in an internal buffer. After pasting: - confirm the destination hex and offsets; - inspect placement IDs and ownership; - verify multihex and blocking behavior; - check references that may not be valid outside the source map; - review the resulting history entry before save. For serialized placement identity and section ownership, use [Map Format](../content/map-format.md). ## Keyboard reference Hotkeys are suppressed while an ImGui text field is active. | Key | Action | |---|---| | `F1` .. `F6` | Toggle Items, Scenery, Walls, Critters, Tiles, and Fast visibility. | | `F7` / `Shift+F7` | Hide or restore the complete ImGui interface / toggle Content. | | `F8` | Toggle edge scrolling for the current fullscreen/windowed mode. | | `F9` | Open Inspector for selection, or clear selection when Inspector is visible. | | `F10` | Toggle the mapper hex overlay. | | `F11` / `F12` | Toggle fullscreen / minimize. | | `Shift+F11` | Dump texture atlases. | | `Shift+0` .. `Shift+4` | Choose tile layer 0 through 4. | | `Tab` / `Shift+Tab` | Toggle axial-grid selection / whole-entity selection. | | `Delete` | Delete selected entities. | | `Escape` | Cancel property edit, clear selection, or leave placement mode. | | Numpad `+` / `-` | Shift map time by plus/minus one hour when nothing is selected. | | `Ctrl+Z` / `Ctrl+Y` | Undo / redo. | | `Ctrl+A` | Select all. | | `Ctrl+C` / `Ctrl+X` / `Ctrl+V` | Copy / cut / paste. | | `Ctrl+S` | Save current map. | | `Ctrl+D` | Toggle camera scroll checking for the current map. Keep it enabled during normal interactive work; disable it deliberately for overscan inspection. | | `Ctrl+B` | Mark blocked hexes, equivalent to **Tools -> Mark blocked hexes**. | | `~` | Toggle Console. | | Arrow keys | Scroll the current map. | ## Undo, reset, and save discipline History is per current map and bounded; it is not a source-control replacement. Use it for local authoring operations, then review the serialized diff. Before saving: 1. Confirm the intended map and source container. 2. Check the dirty marker and most recent History entries. 3. Inspect changed entities and their placement IDs. 4. Rebuild the map if visibility or composition changed. 5. Save with `Ctrl+S`. 6. Review the text diff. 7. Run project format/prototype/map validation and rebake. 8. Validate the map in a runtime scene, not only in Mapper. **Reset changes** discards the current map's unsaved edits and restores the source version. Source control remains the recovery path after a saved mistake. ## Settings and layout recovery Settings lists the current resolution, fullscreen state, popular resolutions, and **Reset layout**. Window layout is stored separately from baked resources: the registry under `HKCU\Software\FOnline\Mapper` on Windows and the per-application user-data store on other platforms. Reset layout when windows are off-screen after a monitor or DPI change. Do not delete resource caches or rebake merely to repair an ImGui layout. ## Particle and viewer windows Use Particle Preview for map-context placement, deterministic seed/prewarm, scale, offsets, restart, and removal. Use Particle Viewer for isolated playback and viewport diagnostics. Use the SPARK editor only on authored `.spark` sources. The complete choice and validation workflow is in [Particle Authoring Tools](particle-authoring.md). ## Screenshot and automation contract The mapper script API exposes one screenshot method: | Method | Frame contents | Completion | |---|---|---| | `Game.SaveMapperScreenshot(path)` | Current map render target and mapper script interface drawing; excludes the later application-level ImGui composition. | Synchronous PNG write. | There is no Engine script method for full-window UI capture. The minimal multiplayer example provides a reproducible visible profile; after its windows settle, capture the application window with a platform screenshot tool: ```powershell cmake --build Build\windows --config Release --target ForceBakeResources FOMM_Mapper Build\windows\Binaries\Mapper-Windows-win64\FOMM_Mapper.exe ` -ApplyConfig FOnlineMinimalMultiplayer.fomain ` -ApplySubConfig MapperDocumentationCapture ``` The profile opens `TutorialMap`, starts `Documentation.spk` with a fixed seed, and fixes the viewport at `1280x800`. The checked-in PNG and complete source hashes are recorded in [generated/screenshots.json](../../../generated/screenshots.json). Use `Render.HeadlessWindow = True` for off-screen map rendering, but not `Render.NullRenderer`: a null renderer cannot produce a visual frame. ## Failure diagnosis | Symptom | Check | |---|---| | Map is absent from Map browser | Prototype input roots, `Baking.ProtoFileExtensions`, declared `[ProtoMap]` name, and bake/config selection. | | Prototype is absent from Workspace | Resource pack inclusion, prototype bake, collection name, and project tab scripts. | | Map renders black | Whether the map is intentionally empty, current zoom/hex, missing tiles/images/effects, and renderer log. | | Edit appears but cannot save | Source path resolution, read-only files, multi-map container ownership, and mapper messages. | | Inspector rejects a value | Generated property type, enum spelling, array/struct syntax, nullability, and prototype constraints. | | Particle source is listed but preview is not | Enabled backend, baked `.spk`/`.efk`, referenced effect/texture, bounds, seed/prewarm, and log exceptions. | | Windows are missing or off-screen | Open Windows menu, then use Settings -> Reset layout. | | UI screenshot contains only the map | `SaveMapperScreenshot` is map-only; capture the visible application window with a platform screenshot tool. | Treat `ScriptException`, `VerificationException`, assertion/fatal lines, missing resource logs, failed saves, and invalid map declarations as failures. Do not publish a screenshot or map because the process merely returned zero. ## Project-owned completion gate An embedding project should add: - a documented launch task and representative map; - project tab/filter conventions; - property and placement rules; - content and map validators; - a clean bake; - runtime scene acceptance on supported renderers; - screenshot provenance when project docs show concrete assets; - an update rule that reviews this manual and [Mapper Tools](mapper.md) whenever its Engine pin changes. The interactive Mapper is an authoring surface. The serialized source, project tests, and runtime behavior remain the authority. ===== END DOCUMENT mapper-interactive-manual ===== ===== BEGIN DOCUMENT font-format-guide ===== Source: Docs/en/how-to/content/font-format.md Canonical URL: https://fonline.ru/Docs/en/how-to/content/font-format.html Content SHA-256: 40b691afb97e7264162953a58b48a42f1e52b25f53282ed366d8200f0adb2340 --- layout: default title: Font Formats And Text Layout locale: en document_id: font-format-guide permalink: /Docs/en/how-to/content/font-format.html --- # Font Formats And Text Layout FOnline renders bitmap fonts described by either the Engine text format `.fofnt` or the binary BMFont v3 format `.fnt`. Descriptors are copied into baked resources unchanged, their referenced images follow the normal image baking pipeline, and client scripts bind descriptor paths to `FontType` slots. Use this guide for authoring and integration decisions. Use the generated [font-format reference](../../reference/font-format/index.md), its focused [format](../../reference/font-format/formats.md), [FOFNT](../../reference/font-format/fofnt.md), [BMFont](../../reference/font-format/bmfont.md), [binding](../../reference/font-format/binding.md), [layout](../../reference/font-format/layout.md), [rendering](../../reference/font-format/rendering.md), and [validation](../../reference/font-format/validation.md) pages, plus the [canonical JSON model](../../../generated/font-format.json), for the exact current-revision contract. ## Scope and authority The owning sources are: - `Source/Client/FontManager.cpp` and `.h` for descriptor parsing, glyph metrics, bind-time scaling, atlas preparation, text layout, drawing, and the short-lived format cache; - `Source/Scripting/ClientGlobalScriptMethods.cpp` for `Game.BindFont`, `Game.GetTextInfo`, `Game.GetTextLines`, and `Game.DrawText`; - `Source/Common/Settings.inc` and `Source/Tools/RawCopyBaker.cpp` for raw-copy selection and delivery; - `Source/Client/Updater.cpp` for the built-in default-font dependency; - `Resources/Core/Fonts/` for shipped examples of both runtime descriptor formats and BMFont authoring sidecars. `BuildTools/FontFormatInterface.json` is the source-backed structured contract. `BuildTools/docs_font_format.py` derives the live extension dispatch, raw-copy defaults, FOFNT keys and maximum version, BMFont binary constants and signed fields, font slots and flags, scale range, atlas, cache lifetime, updater path, and bundled descriptor inventory. It rejects source or manifest drift and renders the generated reference. This page is reusable Engine documentation. An embedding project owns its font files, `FontType` extensions, GUI assignments, typography, language coverage, licensing, backend screenshots, and acceptance thresholds. Project docs may link here but must not redefine the parser contract. ## Supported resources The runtime accepts exactly two case-sensitive suffixes through `Game.BindFont`: | Suffix | Runtime role | Notes | | --- | --- | --- | | `.fofnt` | Engine text descriptor | Explicit image, line, and glyph records. | | `.fnt` | Binary BMFont v3 descriptor | Binary only, one texture page, one-pixel padding. | | `.bmfc` | None | BMFont authoring configuration; raw-copied by default but never parsed by `Game.BindFont`. | The client does not load BMFont text/XML descriptors, TTF, OTF, or other vector fonts at runtime. Convert or rasterize those sources before shipping them. Descriptor and image delivery are separate: 1. Keep `fofnt` and `fnt` in `Baking.RawCopyFileExtensions`. `RawCopyBaker` preserves each descriptor's bytes and resource path. 2. Put the referenced bitmap in a resource pack. The image is baked and loaded through the [image and sprite format](image-format.md) pipeline. 3. Preserve the relative relationship between descriptor and image. Both loaders combine the image filename with the descriptor directory. 4. Bind the slot on the client before any code measures or draws with it. A successful raw-copy bake proves only that the descriptor was delivered. It does not prove that its image, glyph rectangles, language coverage, borders, or GUI composition are correct. ## Minimal FOFNT FOFNT is a whitespace-token parser. The first parsed key must be `Version`. Current resources should author version 2; the client rejects values greater than 2. A small descriptor looks like this: ```text Version 2 Image Example.png* LineHeight 14 YAdvance 2 Letter ' ' PositionX 1 PositionY 1 Width 1 Height 1 OffsetX 0 OffsetY 0 XAdvance 5 Letter 'A' PositionX 4 PositionY 1 Width 9 Height 12 OffsetX 0 OffsetY 0 XAdvance 10 End ``` The trailing `*` on `Image` requests grayscale normalization. Omit it when the bitmap's authored RGB must remain. `Image` is mandatory and relative to the descriptor. The current loader reaches `image_name.back()` without an explicit empty-value guard, so validate the field before runtime instead of relying on a binding diagnostic. `Letter` decodes one UTF-8 codepoint beginning after the first apostrophe. Its following metric keys modify that current glyph until another `Letter` appears. A duplicate codepoint replaces the earlier record. Unknown keys are ignored; this makes typos especially dangerous because the descriptor may bind with zero/default metrics. Treat warnings, missing glyphs, and visual displacement as asset failures. `#` and `;` are stripped only when found in the current whitespace-delimited key token. Do not rely on them as a full line-comment grammar. `End` stops parsing and should terminate every authored descriptor. ## FOFNT metrics Each glyph has a visible rectangle and cursor metrics: - `PositionX`, `PositionY`: top-left pixel of the visible rectangle; - `Width`, `Height`: visible dimensions, excluding the one-pixel sampling border; - `OffsetX`, `OffsetY`: signed Engine bearings. Drawing starts at cursor minus the offset, so positive values move the bitmap left/up and negative values move it right/down; - `XAdvance`: signed horizontal cursor advance after the codepoint; - `LineHeight`: visible line height. Zero or omission derives the maximum glyph height after optional scaling; - `YAdvance`: additional gap between lines. Author an explicit space glyph. Its `XAdvance` becomes `SpaceWidth`; without one, spaces and tabs can have zero width. A tab advances by four `SpaceWidth` units. Kerning pairs are not represented or applied. Leave at least one transparent pixel around every visible glyph and around the image edge. The renderer expands texture coordinates and geometry by one pixel on all sides. That border carries antialiased edge pixels and gives the optional outline generator room to dilate without bleeding into neighboring glyphs. ## Binary BMFont Use a BMFont exporter with these settings: - binary format, version 3; - exactly one texture page; - padding top/right/bottom/left = `1/1/1/1`; - Info, Common, Pages, and Chars blocks in standard order; - page image filename relative to the `.fnt` file. The client expects 20-byte character records. BMFont defines `xoffset`, `yoffset`, and `xadvance` as signed little-endian 16-bit fields, and negative bearings occur in the bundled fonts. The current loader nevertheless reads all three with `GetLEUInt16`; negative values are therefore reinterpreted near 65535 and can move glyphs far outside their intended position. Treat this as a known runtime limitation and avoid negative metrics until the code fix lands in a separate change. The loader removes the exporter's padding from each record: it shifts X/Y by one, subtracts two from width/height, negates bearings into the Engine offset convention, and adds one to X advance. BMFont Common `lineHeight` participates in vertical-bearing conversion but is not copied directly. Engine `LineHeight` uses the visible `W` glyph height when `W` exists, otherwise Common `base`, and `YAdvance` becomes half of that result. Every binary BMFont binding is grayscale-normalized and gets a bordered atlas copy. The runtime ignores kerning and supports neither multiple texture pages nor alternate block ordering. Review the generated [BMFont contract](../../reference/font-format/bmfont.md) before changing exporter settings. ## Binding font slots The Engine declares one slot: ```angelscript enum FontType { Default = 0 } ``` An embedding project may extend `FontType` through its codegen enum annotation and bind each slot during client initialization: ```angelscript Game.BindFont(FontType::Default, "Fonts/Default.fofnt"); Game.BindFont(FontType::Big, "Fonts/Big.fofnt", 0.8f); Game.BindFont(FontType::Numbers, "Fonts/Numbers.fofnt"); ``` The exact enum-extension syntax belongs to the embedding project's generated API setup. Slots are integer indices, not path aliases. Measuring or drawing an unloaded, negative, or out-of-range slot throws. Both descriptor paths bind to `AtlasType::IfaceSprites`. Rebinding a slot replaces its font, rebuilds texture data, and clears cached layouts. The built-in updater separately attempts to bind `FontType::Default` from `Fonts/Default.fofnt` with skip-if-already-loaded behavior, so that resource is part of the stock host contract. ## Bind-time scale `Game.BindFont` accepts `defaultScale`, defaulting to `1.0`. It must be finite and in `(0, 1]`. The client deliberately does not upscale a bitmap font; author a larger source atlas and downscale it for smaller slots. Scaling happens once while the font is bound: 1. Every glyph is area-average resampled within its own rectangle using alpha-weighted color. 2. The original rectangle is cleared and the smaller bitmap is written at the same top-left position, so neighboring glyphs cannot bleed into it. 3. glyph size, bearings, advance, line height, space width, and line gap are rounded to integer target metrics; 4. grayscale normalization and border dilation run on the scaled result. There is no independent per-widget font scale in `TextFormat`. Bind separate slots when a project needs several sizes. Validate every scale with `Game.GetTextInfo` and visible text because integer rounding can change wrapping and baseline fit. ## TextFormat and layout `TextFormat` contains `Font`, a `FontFlag` bitmask, and nonnegative `SkipLines`. The generated [layout reference](../../reference/font-format/layout.md) lists exact flag values. The important interactions are: - default finite-width layout wraps at the latest space or tab; an overlong token gets a line break inserted at the overflow point; - `NoWrap` truncates drawing at the first width overflow. It is draw-mode-only: `Game.GetTextInfo` still follows ordinary wrapping, so do not use measurement to infer the final truncated substring; - `TruncateLine` removes overflowing glyphs through the next authored newline; - `CenterX` and `AlignRight` position each line independently; - `CenterY` and `AlignBottom` position the visible text block vertically; - `SkipLines` removes leading lines normally and trailing lines when `AlignBottom` is set; - `KeepTail` removes leading overflow so the newest fitting lines remain; - `Justify` distributes remaining width over spaces on wrapped lines. Tabs stay fixed at four space widths; - `Bordered` selects the generated outlined texture. A zero layout width or height is treated as unbounded in the corresponding dimension by the formatter. Public line-count helpers reject nonpositive sizes, so use explicit positive GUI rectangles for portable measurement behavior. The formatter decodes UTF-8 codepoints. Invalid sequences and codepoints absent from the selected font have zero advance and produce no fallback glyph. A font that lacks a required language character can therefore collapse words without a hard runtime error. Glyph coverage must be an explicit project gate. ## Measurement and drawing Use the same font slot, flags, width, and height when measuring and drawing: ```angelscript TextFormat format; format.Font = FontType::Default; format.Flags = FontFlag::CenterX | FontFlag::CenterY; isize resultSize; int resultLines; Game.GetTextInfo(text, boxSize, format, resultSize, resultLines); Game.DrawText(text, boxPos, boxSize, color, format); ``` `Game.DrawText` is available only during the interface-render event. Negative draw width or height mirrors the rectangle origin adjustment into a positive size before layout. A clear color selects the Engine default text white. Except for draw-only `NoWrap`, measurement and drawing share the same formatter, line metrics, skips, scale, and glyph advances. `GetTextInfo` returns the maximum line width, visible block height, and visible line count. Cache entries are keyed by text, slot, flags, skips, rectangle dimensions, color, and formatting mode; they expire after three unused frames and are invalidated by font replacement. Never depend on cache identity or lifetime. ## Color and effects The font starts with the Engine shared font effect. A project may replace the shared effect or select a per-slot `EffectType::Font` subtype. Passing a null per-slot override returns that slot to the current shared font effect. Effect syntax and backend validation belong to [Effect Format](effect-format.md). Inline renderer tags use the Engine's packed `BBGGRR` / `AABBGGRR` order, with an optional `0x` prefix; `@color@` restores the previous color. Valid tags are removed before wrapping. With `NoColorize`, valid tags are still stripped but their colors are not applied. Malformed tags remain ordinary text. See [Text And Localization](text-and-localization.md#engine-and-project-formatting-boundary) for the exact forms and shared string-authoring boundary; do not duplicate these tags in a project-specific localization grammar. ## Recommended project practice Keep typography data-driven and small: 1. Define semantic slots such as body, heading, compact numbers, and debug text instead of binding a separate slot for every widget. 2. Author the largest bitmap needed for a family and bind reviewed downscaled variants. Do not expect layout-time scaling. 3. Include all source-language and fallback-language codepoints, punctuation, digits, and symbols used by gameplay, chat, console, and updater paths. 4. Put descriptor, bitmap, license/provenance note, slot binding, and visual test scene in the same review scope. 5. Measure dynamic labels with the actual localized string and font slot; never size a panel from an English placeholder or character count. 6. Keep at least one screenshot matrix covering normal/bordered rendering, every bound scale, narrow wrapping, all alignments in use, and longest localized labels on every supported backend. ## Validation workflow For an Engine parser, metric, layout, or script-binding change: ```powershell python BuildTools\docs_font_format.py --write python -m unittest BuildTools.tests.test_docs_font_format python BuildTools\docs_contract_diff.py --check cmake --build --config RelWithDebInfo --target RunUnitTests ``` For an embedding-project font change: 1. Regenerate project code when `FontType` changes. 2. Bake descriptor and image resources together. 3. Run focused text-measurement and GUI-layout tests for every changed slot or scale. 4. Launch a visible client and inspect regular, bordered, colored, wrapped, aligned, and localized strings. 5. Check logs for descriptor, image, atlas, effect, and script exceptions. 6. Update the project-owned font catalog and GUI/localization docs in the same change. The focused generated checks prove the documented source contract. Only the embedding project can prove its glyph coverage, typography, UI fit, rendering backend behavior, and asset rights. ===== END DOCUMENT font-format-guide ===== ===== BEGIN DOCUMENT audio-guide ===== Source: Docs/en/how-to/content/audio.md Canonical URL: https://fonline.ru/Docs/en/how-to/content/audio.html Content SHA-256: 5280a938e5587e7260465d27c6b23ae18e78b513e2aef56dc10b5b2d6c90fbef --- layout: default title: Audio Resources and Playback document_id: audio-guide locale: en permalink: /Docs/en/how-to/content/audio.html --- # Audio Resources and Playback > Engine-owned documentation. This guide describes reusable audio baking, > runtime decoding, playback, placement, mixing, and validation in > `cvet/fonline`. A game owns its sound catalog, concept-to-path mapping, music > state machine, spatial policy, mastering, licenses, and audible acceptance. Use the generated [audio reference](../../reference/audio/index.md) when exact stable IDs, source anchors, or machine-readable values matter. ## Source map - `Source/Tools/AudioBaker.*` accepts authored WAV and Ogg, verifies the input, and emits an Ogg Vorbis payload while preserving the authored resource path. - `Source/Client/AudioManager.*` indexes resource paths and owns decoding, streaming, handles, placement, repeat, stop, and live volume state. - `Source/Scripting/ClientGlobalScriptMethods.cpp` exports `Game.PlaySound`, `Game.UpdateSound`, and `Game.PlayMusic`. - `Source/Frontend/Application.*` owns the SDL audio device, conversion, mixing, and callback synchronization. `ApplicationHeadless.cpp` defines the no-audio headless boundary. - `Source/Common/Settings.inc` owns immutable audio startup settings. - `Source/Tests/Test_AudioBaker.cpp` and `Test_AudioManager.cpp` execute the focused native contract. - `BuildTools/AudioInterface.json` is the checked documentation contract. ## Supported resources The authoring boundary accepts two source forms: | Authored suffix | Accepted input | Baker result | Runtime decoder | |---|---|---|---| | `.wav` | RIFF/WAVE PCM at 8, 16, 24, or 32 bits; IEEE float at 32 bits | Normalized to interleaved signed 16-bit and encoded as Ogg Vorbis | libvorbisfile | | `.ogg` | Ogg bitstream containing Vorbis audio | Verified and copied without another lossy pass | libvorbisfile | There is no ACM runtime path. MP3, FLAC, Opus, AAC, arbitrary SDL formats, and Ogg containers carrying a codec other than Vorbis are unsupported. The authored suffix identifies the source path, not the bytes after baking. A resource named `Sfx/Door.wav` still has that path after baking, but its payload is Vorbis. Runtime therefore uses one decoder for both preserved `.wav` paths and native `.ogg` paths; it does not dispatch a codec from the suffix. ## Delivering audio Include the `Audio` baker in every resource pack that owns client audio: ```ini [ResourcePack] Name = Sound InputDirs = Resources/Sound IncludePatterns = ** ClientOnly = True Bakers = Audio ``` Audio is no longer a `RawCopy` resource family. WAV is converted according to `Baking.AudioVorbisQuality`; authored Ogg is validated before passthrough. Both routes verify that the output opens as Vorbis. The baker reports encoded and passthrough counts plus input/output byte totals. `AudioManager.IndexFiles()` records paths with suffixes listed by `Audio.SoundFileExtensions` (normally `wav ogg`). The catalog is an inspection surface; playback itself receives the exact selected resource path. ## Playing effects `Game.PlaySound(path)` returns a non-reused `uint32` lifetime handle: ```csharp uint sound = Game.PlaySound("Sfx/DoorOpen.wav"); if (sound == 0) { // No live sound was started. } ``` Pass the exact baked resource path, including the authored extension and case. There is no extension fallback, lowercase stem normalization, or automatic concept lookup. ### Numbered variants The engine does not discover or randomly select numbered variants. If a game authors `Footstep_1.wav` through `Footstep_4.wav`, project code must choose one and pass its exact path. This keeps catalog and randomization policy outside the reusable mixer. ### Placed playback Use the overload with attenuation and pan when a sound has a current placement: ```csharp uint sound = Game.PlaySound("Sfx/Generator.wav", attenuation, pan); ``` - attenuation at or below zero returns handle zero before file I/O; - pan is clamped to `-1..1`; negative values attenuate the right channel and positive values attenuate the left; - the near channel remains at unity, so panning does not boost into clipping. When the source or listener moves, update the existing instance: ```csharp bool alive = Game.UpdateSound(sound, attenuation, pan); ``` `false` means the handle is zero, audio is inactive, or playback already finished. Handles are never reused, so a stale handle cannot target a later sound. Distance curves, listener selection, occlusion, and recipient filtering remain project policy. ## Playing music `Game.PlayMusic(path, repeatTime)` takes an exact path. An empty path stops the current music and returns success. A new track stops existing music before it loads the replacement; a failed replacement does not restore the old track. Only one music group is active. Music does not use concept lookup or suffix fallback. ## Repeat timing A zero `repeatTime` means play once. A nonzero value retains the playback object and restarts it after completion: - values greater than one millisecond insert that delay; - values at or below one millisecond repeat immediately; - retained Vorbis streams seek back to byte position zero before replay. The interval begins after playback reaches the end; it is a gap, not a period that includes the track duration. ## Format details ### WAV authoring `AudioBaker` walks RIFF chunks in any order, skips unknown chunks, validates bounds, and respects odd-byte padding. It requires one usable `fmt ` chunk and a non-empty `data` chunk. Supported formats are PCM (`1`) at 8/16/24/32 bits and IEEE float (`3`) at 32 bits. Channel count and sample rate must be positive; block alignment must match the declared frame shape; truncated frames fail the bake. Every accepted sample is normalized to signed 16-bit PCM before Vorbis encoding. Test the exact source export: metadata layout and malformed format fields are authoring errors even if an editor happens to play the file. ### Ogg Vorbis runtime Authored Ogg and generated output are opened with libvorbisfile during baking. At runtime `AudioManager` opens every audio resource as Vorbis and decodes an initial portion of 64 KiB on native targets or 128 KiB on Web. Short files become resident; longer files retain `OggStream` and continue decoding from the audio callback. ## Device conversion and mixing `AppAudio::ConvertAudio` converts decoded channels, sample rate, and format to the active SDL output device. The callback starts with silence, asks `AudioManager` for active data, applies per-instance attenuation and pan, then mixes using the live sound or music volume. `Audio.SoundVolume` and `Audio.MusicVolume` are immutable startup defaults. Use `Game.SetSoundVolume` and `Game.SetMusicVolume` for live changes; the frontend clamps every mix operation to `0..100`. Play, stop, and placement update operations synchronize through the audio device lock. Native extensions must not mutate mixer storage from callbacks. ## Disabled and headless behavior `Audio.DisableAudio = true`, an unavailable SDL device, and headless/stub frontends leave audio inactive. In that state `PlayMusic` is a successful no-op and `PlaySound` returns handle zero. These results are not a resource-existence check. Baking proves that a path and payload are valid; a visible client with an active device proves conversion, callback scheduling, mixing, and audible output. ## Recommended project practice 1. Put authored WAV/Ogg in a client pack that selects the `Audio` baker. 2. Keep exact paths in project-owned catalogs; resolve concepts and variants before calling the engine. 3. Use WAV as an editable/master input and native Ogg when avoiding another lossy encode matters. 4. Centralize distance curves, listener choice, ambient scheduling, cooldowns, and music transitions in project code. 5. Keep server-authoritative gameplay decisions separate from local playback. 6. Store masters, licenses, attribution, and redistribution provenance outside baked output. 7. Test representative assets and placement motion on every supported platform. ## Diagnostics Treat all `AudioBaker` exceptions as content failures. Typical causes are a missing or truncated RIFF chunk, unsupported WAV encoding/width, inconsistent block alignment, an empty Ogg, or an Ogg stream without Vorbis. At runtime, Ogg open/decode and device-conversion failures are logged and enter the debugger. Handle zero only says that no live effect started; it does not replace the baker gate. ## Validation workflow Run documentation and focused native checks: ```powershell python BuildTools\docs_audio.py --check python -m pytest BuildTools\tests\test_docs_audio.py cmake --build --config RelWithDebInfo --target RunUnitTests ``` `Test_AudioBaker` covers PCM/float WAV conversion, native Ogg passthrough, and invalid inputs. `Test_AudioManager` covers decoder/mixer output, pan, placed starts, live updates, handle expiry, and synchronization-relevant behavior. Then bake the embedding project's real pack. Confirm preserved paths, inspect baker counters, and open every output as Vorbis. In a visible client, play a WAV-path and native-Ogg-path resource, move a placed sound, replace music, exercise immediate/delayed repeats, and test volume endpoints on each claimed platform. ## Project boundary An embedding project owns catalogs, concepts, variants, server/client routing, distance and occlusion policy, ambient and music state, concurrency budgets, loudness/accessibility choices, masters, licenses, attribution, and audible acceptance. The engine supplies baking, one decoder path, a client mixer, handles, and placement parameters. ## Maintenance Changes to accepted inputs, baker conversion, preserved paths, Vorbis streaming, handles, placement, repeat timing, script signatures, live volume, device conversion, package delivery, or headless behavior are documentation-bearing. Update `BuildTools/AudioInterface.json` and this guide, regenerate the reference, run focused tests and the aggregate contract diff, and include migration guidance for public behavior changes. ===== END DOCUMENT audio-guide ===== ===== BEGIN DOCUMENT video-guide ===== Source: Docs/en/how-to/content/video.md Canonical URL: https://fonline.ru/Docs/en/how-to/content/video.html Content SHA-256: ff9a3082fb14e50d2f5b9683a4142dc78d416e9725ffe315cb1f6cb310fd3c83 --- layout: default title: Video Resources and Playback document_id: video-guide locale: en permalink: /Docs/en/how-to/content/video.html --- # Video Resources and Playback > Engine-owned documentation. This guide describes the revision-pinned > Ogg/Theora decoder and presentation primitives in `cvet/fonline`. A game owns > its cinematic catalog, triggers, recipients, skip policy, subtitles, > localization, save-state consequences, mastering, provenance, and visible > acceptance tests. Use the generated [video reference](../../reference/video/index.md) for stable contract IDs, checked source anchors, and machine-readable values. ## Contract status The stock video path is **experimental**. It is useful for controlled project integration, but it is not yet a versioned production media subsystem: - only Ogg carrying Theora video is recognized by the implementation; - the complete compressed resource is loaded into memory before decoding; - decoding and YCbCr-to-RGBA conversion run on the client CPU; - the Ogg container's audio is not decoded; - there is no focused native video decoder, rendering, queue, or loop fixture; - there is no built-in subtitle, accessibility, aspect-fit, streaming, or cinematic-state layer. Pin the exact Engine revision and re-run visible acceptance tests whenever the decoder, rendering, resource, input, or audio path changes. ## Source map - `Source/Client/VideoClip.*` owns Ogg packet ingestion, Theora setup, frame timing, pixel conversion, stop/pause state, and the loop flag. - `Source/Client/Client.*` owns fullscreen playback, queueing, input interruption, separate music pairing, draw order, and status. - `Source/Scripting/ClientGlobalScriptMethods.cpp` exports fullscreen and script-owned playback methods. - `Source/Client/SpriteManager.cpp` defines the target-rectangle behavior used by video drawing. - `Source/Common/Settings.inc` declares `.ogv` as a raw-copy extension. - `Source/Tools/RawCopyBaker.*` delivers authored bytes without transcoding. - `BuildTools/cmake/stages/ThirdParty.cmake` links Ogg and Theora into client targets. - `BuildTools/VideoInterface.json` is the checked documentation contract. ## Delivering video There is no video baker. The default `Baking.RawCopyFileExtensions` contains `ogv`; an embedding project must also put `RawCopy` in the client-visible resource pack that owns the files. Prefer a dedicated, client-only video pack so server and mapper payload decisions stay explicit. Both playback surfaces use exact resource paths: ```angelscript Game.PlayVideo("Video/Intro.ogv", true, false); VideoPlayback video = Game.CreateVideoPlayback("Video/Terminal.ogv", false); ``` Include the `.ogv` suffix. There is no default extension, normalized video-stem index, language fallback, or filesystem search. Path spelling and case must match the delivered resource on case-sensitive targets. The presence of `ogv` in `RawCopyFileExtensions` only permits copying. It does not select a resource pack, prove that the client received the file, or validate its container and codec. ## Authoring requirements Author an Ogg resource with a valid Theora stream. The decoder requires valid picture dimensions, frame-rate numerator and denominator, setup headers, and one of these Theora pixel formats: - `TH_PF_420`; - `TH_PF_422`; - `TH_PF_444`. The Engine does not invoke an authoring tool. A conventional starting point for a silent asset is: ```powershell ffmpeg -i input.mov -an -c:v libtheora output.ogv ``` Treat that command as an authoring example, not a compatibility proof. Inspect the produced stream, bake its exact bytes, and run it in every supported client. Choose dimensions and frame rate from measured platform budgets. Avoid assuming that another codec in an Ogg container will work. The video path does not decode audio from the container. Export sound separately in a format accepted by [Audio Resources and Playback](audio.md). The built-in fullscreen path can start one separate music resource, but it does not provide sample-accurate synchronization, alternate tracks, dialogue ducking, or language selection. ## Memory and performance `Resources.ReadFile(...).GetData()` moves the complete compressed file into `VideoClip`. The implementation does not stream from disk, package storage, or the network. Each active playback also owns: - a CPU buffer of one opaque RGBA frame; - an Ogg/Theora decoder state; - a GPU texture matching the encoded picture dimensions. Budget at least `compressed file size + width * height * 4` CPU bytes plus the GPU texture and decoder overhead for every simultaneous playback. Embedded instances retain their own copy and texture. Do not preload many long clips by creating dormant `VideoPlayback` objects. Ogg input is fed to the parser in 1024-byte portions from the resident buffer. That implementation detail is not resource streaming. Frame selection uses a monotonic clock and the encoded frame-rate ratio; when presentation falls behind, one draw call may decode multiple packets before uploading a frame. Profile CPU conversion and texture upload on the weakest supported target. ## Fullscreen playback Call: ```angelscript Game.PlayVideo(videoName, canInterrupt, enqueue); bool pending = Game.IsVideoPlaying(); ``` The method returns no success value. A missing file leaves no active playback, and the current implementation emits no dedicated missing-video diagnostic on that path. Validate resources before entering a transition that depends on completion. `Game.IsVideoPlaying()` returns true when a clip is active **or** the fullscreen queue is non-empty. It does not prove that a frame is currently visible. ### Queue and replacement When `enqueue` is false, `Game.PlayVideo` destroys the current clip, clears the entire queue, and then tries to load the requested file. Passing an empty string therefore acts as a fullscreen stop-and-clear operation. When `enqueue` is true and a clip is active, the request is appended. When no clip is active, the same request starts immediately; it does not create a waiting-only state. Queued entries start sequentially after the previous clip stops. Test a missing queued entry because loading failure can make the queue advance without showing a frame. ### Interruption With `canInterrupt = true`, the current clip stops on: - key-down; - mouse-down; - touch down, move, up, tap, double-tap, scroll, or zoom. This is a broad transport primitive, not a complete skip policy. A game that requires hold-to-skip, a protected first interval, confirmation, input debouncing, or mandatory story state must implement that policy around its own cinematic controller. ### Separate music The fullscreen string accepts one optional music path after `|`: ```angelscript Game.PlayVideo("Video/Intro.ogv|Sound/Intro.ogg", true, false); ``` The Engine loads the first component as video and uses only the second component as one-shot music. Additional separators do not form a playlist. Starting a paired request first stops current music. Completion or interruption of every fullscreen clip also calls `StopMusic()` unconditionally, even when that clip did not start paired music. Do not place gameplay-critical music restoration behind an assumption that the video path preserves the previous music group. A project controller should record and restore the intended state explicitly. ### Drawing and aspect Fullscreen frames are uploaded and drawn after `Game.OnRenderIface`. Drawing without source and target regions fills the complete current render target with alpha blending disabled. The encoded image is stretched when its aspect ratio differs from the target. The built-in path has no letterboxing, pillarboxing, safe-area, crop, caption, overlay, or transition policy. Use a project-owned embedded presentation when those requirements matter. ## Embedded playback Create a script-owned instance with: ```angelscript VideoPlayback video = Game.CreateVideoPlayback("Video/Terminal.ogv", false); ``` Unlike fullscreen playback, a missing resource throws `Video file not found`. The returned ref-counted object owns independent decoder state and a texture. Draw it only during `Game.OnRenderIface`: ```angelscript void RenderTerminalVideo() { Game.DrawVideoPlayback(video, ipos(120, 80), isize(640, 360)); } ``` The exact event subscription syntax belongs to the embedding script module. The target width and height must be positive. A non-positive size skips frame decode, upload, and drawing, so a hidden instance does not advance through this API. The Engine draws exactly the requested rectangle and does not preserve aspect ratio. Compute fit, crop, bars, safe area, and responsive layout in project UI code. Passing null or an instance whose resources were released is a no-op. `VideoPlayback.Stopped` becomes true only when a subsequent `Game.DrawVideoPlayback` call observes the stopped clip, clears its resources, and updates the field. Continue drawing or polling through a controller until that cleanup transition has occurred. ## Looping `Game.CreateVideoPlayback(path, true)` exposes the `VideoClip` loop flag. At end-of-stream, the current source calls `Stop()` followed by `Resume()`, but there is no focused fixture proving decoder rewind, packet-state reset, frame counter reset, or uninterrupted multi-cycle output. Treat looping as experimental even within this experimental subsystem. Do not promise a production ambient loop until a visible test has completed several cycles for the exact asset and every claimed platform. Prefer an explicit project fallback when continuity matters. ## Diagnostics Construction and frame decode can report failures such as: - packet seek or Theora header decode failure; - missing setup data or decoder allocation failure; - malformed encoded frame data; - color-buffer output failure; - unsupported Theora pixel format. Inspect the client log around the first attempted frame. For fullscreen missing files, independently inspect baked resources because that lookup currently returns silently. A successful constructor still does not prove sustained rendering, correct aspect, synchronized audio, cleanup, or loop continuity. ## Validation workflow Run the source-backed documentation checks: ```powershell python BuildTools\docs_video.py --check python -m unittest BuildTools.tests.test_docs_video python BuildTools\docs_validate.py ``` Then validate each representative asset in a visible client: 1. Bake and confirm the exact client resource path and byte identity. 2. Show the first frame and sustained motion at the intended resolution. 3. Reach natural completion and verify resource cleanup. 4. Exercise every allowed interruption input and the project's skip policy. 5. Exercise replacement, multiple queued entries, and a deliberately missing path. 6. Verify target resize, aspect policy, safe area, overlays, and captions. 7. Verify separate music start, stop, restoration, and acceptable drift. 8. Profile memory, CPU decode/conversion, frame pacing, and texture upload. 9. For looped playback, observe several complete cycles. 10. Repeat on every native, web, Android, and mapper target the project claims. There is currently no focused native video fixture. Source-backed checks keep the documentation honest; they do not replace player-visible evidence. ## Project boundary The Engine owns: - exact-path client resource loading; - Ogg/Theora packet and frame decoding; - CPU RGBA conversion and texture upload; - fullscreen replacement, queue, interruption, and separate music pairing; - script-owned playback creation and rectangular interface drawing. An embedding game owns: - a video resource pack and asset catalog; - cinematic triggers, recipients, authority, and replay rules; - skip/queue policy and save-state consequences; - subtitles, localization, accessibility, aspect, safe area, and overlays; - music/voice strategy and restoration; - source assets, licenses, attribution, and provenance; - file, memory, CPU, GPU, package, and download budgets; - visible acceptance tests and platform support claims. Project documentation may link here for Engine mechanics. It must document its own integration rather than treating another game's scripts or assets as an Engine contract. ## Maintenance When video behavior changes: 1. update `BuildTools/VideoInterface.json` and its source anchors; 2. run `python BuildTools/docs_video.py --write`; 3. update this guide when authoring or operational meaning changed; 4. add or update focused native tests when the decoder becomes testable; 5. run the documentation contract diff and disposition workflow; 6. require embedding projects to repeat visible acceptance for affected assets and targets. The generated reference is machine-checkable evidence for the current revision, not a substitute for a compatibility policy or production media tests. ===== END DOCUMENT video-guide ===== ===== BEGIN DOCUMENT gui-runtime-guide ===== Source: Docs/en/how-to/runtime/gui.md Canonical URL: https://fonline.ru/Docs/en/how-to/runtime/gui.html Content SHA-256: 3653ca03d660b95b397878634d01c7e483835c84113363b06266eb7554794996 --- layout: default title: GUI Integration Boundary locale: en document_id: gui-runtime-guide permalink: /Docs/en/how-to/runtime/gui.html --- # GUI Integration Boundary > Engine-owned documentation. The reusable Engine owns rendering, input, window, resource, and script-export primitives. A high-level GUI object model, screen stack, layout library, generated screens, and authored GUI formats belong to the embedding project. ## Current status The former Engine-owned AngelScript `CoreScripts/Gui.fos` and `Input.fos` library was removed when high-level script libraries moved to the embedding project during the Managed C# integration. The old generated GUI runtime reference was retired with it because its source contract no longer exists in Engine. This is an ownership route, not a compatibility specification for that removed library. Do not restore the deleted `.fos` files merely to keep an Engine documentation generator alive, and do not infer a Managed C# GUI API from the old AngelScript types. ## What Engine provides - application/window services and native input events; - renderer, sprites, atlases, render targets, fonts, text, effects, video, and drawing exports; - backend-neutral metadata and generated script bindings for native APIs; - client lifecycle hooks on which a project GUI can subscribe; - AngelScript and Managed C# backends through which a project implements its GUI library. Use [Frontend and Rendering](../../explanation/rendering/) for the native presentation contract, [Scripting](../../explanation/scripting-runtime/) for backend-neutral bindings, and [Managed C# Scripting](../scripting/managed-csharp.md) for C# authoring and runtime behavior. ## What the project must document The embedding project owns and documents its GUI types, screen registration, stack/modal rules, layout and scaling, drawing order, mouse/keyboard/touch behavior, focus, drag/drop, item views, declarative formats or generators, localization behavior, and screen-specific acceptance tests. These contracts may differ between projects and scripting languages. ## Validation boundary For an Engine-native render/input export change, regenerate the script API and run the focused native tests. Then compile/bake every affected scripting backend in a representative embedding project and perform visible interaction checks on every claimed platform. Headless success is not visual acceptance. For a project GUI-library change, keep its source-derived reference, tests, generated screens, and visual evidence in that project. Engine-only validation cannot prove project screen behavior. ## Maintenance triggers Update this route when Engine adds or removes native presentation/input primitives, changes which scripting backends expose them, or introduces a new reusable Engine-owned GUI layer. A future Engine GUI layer needs an actual source owner, tests, generated/reference contract where useful, EN/RU documentation, project integration evidence, and visible acceptance before this page may call it implemented. ## Source paths inspected - `Source/Frontend/` - `Source/Client/` - `Source/Scripting/*ScriptMethods.cpp` - `Source/Scripting/AngelScript/` - `Source/Scripting/Managed/` - `Docs/en/explanation/rendering/` - `Docs/en/explanation/scripting-runtime/index.md` ===== END DOCUMENT gui-runtime-guide ===== ===== BEGIN DOCUMENT ai-maintainer-entry ===== Source: AGENTS.md Canonical URL: https://fonline.ru/AGENTS.html Content SHA-256: a0ddae586a823fc78fa92f18bc58631bf7f32b7835aefac051b599ace508d1e3 # FOnline Engine — AI Maintainer Guide This is the AI entry point for the reusable FOnline engine repository. For the human entry point, start with [README.md](README.md). For the English documentation map, start with [Docs/en/index.md](Docs/en/index.md); the Russian mirror starts at [Docs/ru/index.md](Docs/ru/index.md). ## Scope - This repository is the reusable engine submodule used by games such as Last Frontier. - Engine-owned code lives under `Source/`, `BuildTools/`, `Resources/`, and engine tests under `Source/Tests/`. - Game-specific content, scripts, native extensions, CI glue, and launch presets live in the embedding project and should not be moved into the engine unless the behavior is genuinely reusable. ## Before Changing Anything 1. Check whether the change belongs to the engine or to the embedding game project. 2. Read the nearest existing code and follow its style; do not introduce parallel conventions. 3. Verify before editing: docs may drift, and when a doc and the live source disagree, the source wins — fix the doc in the same change. 4. If behavior changes, update the owning engine doc in `Docs/` in the same worktree change. 5. Do not commit or push unless explicitly asked by the repository owner. 6. When pulling, rebasing, or changing the engine revision, follow [documentation revision reconciliation](Docs/en/contributing/documentation/index.md#revision-update-reconciliation): record old/new SHAs, audit every incoming source/test change, regenerate affected models, and document the reconciliation before dropping any safety stash. 7. Treat published branch history as immutable. Once the branch has a remote tip, do not rebase, reset, amend, or force-push it; merge upstream and the published tip so every publication is a fast-forward. Verify with `git merge-base --is-ancestor HEAD` before pushing, and stop instead of rewriting history when that check fails. ## Documentation Map The full maintained index is [Docs/en/index.md](Docs/en/index.md); use it when a topic is not listed here. Convention-critical docs for maintainers: - [Engine Architecture](Docs/en/explanation/architecture/) - engine layer map and where a behavior belongs. - [Source Tree Guide](Docs/en/contributing/source-tree/) - source-tree navigation. - [Essentials](Docs/en/reference/native/essentials.md) - low-level platform, logging, memory, filesystem, serialization, sockets, utilities, and `vector` / `small_vector` selection rules. - [Configuration and Data Sources](Docs/en/reference/settings/configuration-and-data-sources.md) - config parsing, settings, data sources, file lookup, and caches. - [Networking and authority](Docs/en/explanation/authority-and-networking/) - transports, Noise NK secure channel, key pins and rotation, remote calls, and server-authoritative boundaries. - [Docs/en/how-to/build/project-configuration.md](Docs/en/how-to/build/project-configuration.md) - project `.fomain`, resource packs, sub-configs, precedence, and validation. - [Docs/en/explanation/content-pipeline/baking.md](Docs/en/explanation/content-pipeline/baking.md) - resource-pack execution, built-in baker ordering, incremental/full rebuilds, reports, payload ownership, and project composition practices. - [Docs/en/how-to/release/security-and-secrets.md](Docs/en/how-to/release/security-and-secrets.md) - config substitution timing, command-line masking limits, package signing handoff, CI trust boundaries, rotation, revocation, and incident routing. - [Docs/en/how-to/release/operations.md](Docs/en/how-to/release/operations.md) - server process selection, readiness/health evidence, staged rollout, graceful shutdown, and rollback boundaries. - [Docs/en/how-to/release/backup-and-recovery.md](Docs/en/how-to/release/backup-and-recovery.md) - backend-aware backup sets, oplog limits, isolated restore, disaster-recovery drills, and project-owned recovery policy. - [Docs/en/how-to/build/generated-content.md](Docs/en/how-to/build/generated-content.md) - generated source/resource/metadata/documentation dependency order and recovery. - [Docs/en/how-to/migration/engine-upgrade.md](Docs/en/how-to/migration/engine-upgrade.md) - complete Engine-update, migration, compatibility, and documentation reconciliation route. - [Docs/en/reference/platforms/support-matrix.md](Docs/en/reference/platforms/support-matrix.md) - truthful build/smoke/source-capable claims and project release qualification. - [Docs/en/contributing/testing/index.md](Docs/en/contributing/testing/index.md) - test-suite inventory, generated test targets, coverage, and validation routing. - [Docs/en/how-to/testing/gameplay-and-integration.md](Docs/en/how-to/testing/gameplay-and-integration.md) - boundary-based gameplay/integration tests, deterministic fixtures, process manifests, semantic markers, deadlines, cleanup, and reports. - [Docs/en/how-to/ai-control-protocol.md](Docs/en/how-to/ai-control-protocol.md) - project-neutral AI-control transport, command lifecycle, loopback threat boundary, reference client/sample, and project-owned observation/MCP boundary. - [Docs/en/how-to/quality/profiling.md](Docs/en/how-to/quality/profiling.md) - Tracy build modes, client/server capture boundaries, reproducible workloads, and result interpretation. - [Documentation maintenance](Docs/en/contributing/documentation/) - source-grounded docs maintenance workflow. - [Documentation site publication](Docs/en/contributing/documentation/site-publication.md) - GitHub Pages/Jekyll navigation/search, preview, rendered-route and pinned Chromium/WCAG gates, CI artifacts, custom-domain contract, and production verification. - [Docs/en/contributing/documentation/ai-evaluation.md](Docs/en/contributing/documentation/ai-evaluation.md) - versioned standalone documentation tasks, deterministic retrieval gate, model-family run protocol, and scoring. - [Docs/en/contributing/documentation/snippets.md](Docs/en/contributing/documentation/snippets.md) - fenced-example policy, parser harnesses, generated coverage, external shell checks, and semantic-owner boundary. - [Docs/en/contributing/documentation/translation.md](Docs/en/contributing/documentation/translation.md) - English/Russian paths, glossary, normalized source hashes, parity, and link/code preservation. - [Docs/description-translations.ru.json](Docs/description-translations.ru.json) and [Docs/generated/description-translation-status.json](Docs/generated/description-translation-status.json) - stable-ID Russian overlays and explicit semantic-translation coverage for prose supplied by generated contract models; regenerate/check with `BuildTools/docs_description_translations.py`. - [Public Example Repositories](Docs/en/how-to/build/public-example-repositories.md) - external example portfolio, ownership, exact Engine pins, compatibility lanes, governance overlay, releases, support, and asset provenance. - [llms.txt](llms.txt), [llms-full.txt](llms-full.txt), and [docs-manifest.json](docs-manifest.json) - generated external-agent routes derived from the documentation manifest; regenerate with `BuildTools/docs_ai_delivery.py`, never edit them manually. - [Docs/ai-evaluation.json](Docs/ai-evaluation.json) and [Docs/generated/ai-evaluation-report.json](Docs/generated/ai-evaluation-report.json) - reviewed AI task source and deterministic current report; regenerate the report with `BuildTools/docs_ai_eval.py`, never edit it manually. - [BuildTools/docs_ai_model_eval.py](BuildTools/docs_ai_model_eval.py), [BuildTools/docs_ai_model_review.py](BuildTools/docs_ai_model_review.py), and [Docs/_meta/ai-evaluation/](Docs/_meta/ai-evaluation/) - optional isolated model-family runner, compact independent-review workflow, and dated internal evidence; keep raw runs in ignored `Workspace/ai-evaluation/` and never claim the production target from automatic term matching alone. - [BuildTools/SnippetPolicy.json](BuildTools/SnippetPolicy.json) and [Docs/generated/snippets.json](Docs/generated/snippets.json) - reviewed fence/harness policy and generated complete coverage report; regenerate with `BuildTools/docs_snippets.py`, never edit the report manually. - [BuildTools/DocumentationDiagrams.json](BuildTools/DocumentationDiagrams.json), [Docs/generated/diagrams.json](Docs/generated/diagrams.json), and [Docs/assets/diagrams/](Docs/assets/diagrams/) - source-owned accessible teaching diagrams, provenance, and exact SVG hashes; regenerate with `BuildTools/docs_diagrams.py`, never edit the SVG or catalog manually. - [BuildTools/DocumentationScreenshots.json](BuildTools/DocumentationScreenshots.json), [Docs/generated/screenshots.json](Docs/generated/screenshots.json), and [Docs/assets/screenshots/](Docs/assets/screenshots/) - versioned tool screenshots, exact capture inputs, recapture triggers, and source/image hashes; regenerate the catalog with `BuildTools/docs_screenshots.py`, never replace an image without updating its provenance record. - [_data/docs-site.json](_data/docs-site.json), [assets/docs-search.json](assets/docs-search.json), [assets/docs-search.ru.json](assets/docs-search.ru.json), and [Docs/generated/document-routes.json](Docs/generated/document-routes.json) - generated human navigation, per-locale search, and version/locale/legacy-route data from the same manifest; regenerate with `BuildTools/docs_site.py`, never edit them manually. - [Docs/en/contributing/decisions/0006-documentation-version-locale-routing.md](Docs/en/contributing/decisions/0006-documentation-version-locale-routing.md) - rolling/current documentation identity, deferred release snapshots, planned English/Russian paths, and durable Markdown redirect policy. - [ThirdParty Maintenance](Docs/en/contributing/third-party/) - vendored dependency update, pruning, version pin, and `(FOnline Patch)` workflow. - [Docs/en/explanation/runtime/client-updater.md](Docs/en/explanation/runtime/client-updater.md) - client host/runtime split, ABI, updater protocol, and `UpdaterBackend`. - [Frontend and Rendering](Docs/en/explanation/rendering/) - application services, renderer selection and backend contracts, atlases, render targets, logical resolution, shared depth, direct-scene paths, and validation. - [Docs/en/troubleshooting/debugging.md](Docs/en/troubleshooting/debugging.md) - native, AngelScript, and Managed C# debugging, mixed stacks, crash diagnostics, and validation boundaries. - [Docs/en/contributing/coding-contracts/nullability.md](Docs/en/contributing/coding-contracts/nullability.md) - `T?` script / `ptr`·`nptr` native boundary contract. - [Docs/en/how-to/scripting/lifecycle-and-concurrency.md](Docs/en/how-to/scripting/lifecycle-and-concurrency.md) - script module init, callback ownership, `[[Async]]`/`Yield`, server entity covers, mutable-state ownership, and teardown. - [Docs/en/how-to/scripting/managed-csharp.md](Docs/en/how-to/scripting/managed-csharp.md) - Managed C# configuration, generated projects and assemblies, attributes, async scheduling, synchronization-cover analyzers including FOSYNC015, runtime isolation, packaging, platforms, diagnostics, migration, and validation. - [Docs/en/reference/scripting/remote-calls.md](Docs/en/reference/scripting/remote-calls.md) - remote-call declaration, direction, handler, authority, baked catalog, and validation contract. - [Native Extensions](Docs/en/how-to/native-extensions.md) - project-native C++ roles, hooks, script exports, lifecycle, compatibility boundary, and validation. - [Project-Local Dependencies](Docs/en/how-to/native-extensions/project-dependencies.md) - project-local library/SDK ownership, role-scoped linking, platform/package delivery, updates, and validation. - [Prototype Format](Docs/en/how-to/content/prototype-format.md) - prototype syntax, inheritance, property applicability, references, migrations, and project-owned semantic validation. - [Docs/en/how-to/content/map-format.md](Docs/en/how-to/content/map-format.md) - `.fomap` sections, placement ids, ownership, mapper round-trip, side-specific baking, and runtime materialization. - [Docs/en/how-to/content/model-format.md](Docs/en/how-to/content/model-format.md) - `.fo3d` syntax, FBX/OBJ inputs, layers, attachments, transforms, materials, cuts, runtime composition, and validation. - [Text and Localization](Docs/en/how-to/content/text-and-localization.md) - `.fotxt` syntax, language normalization, prototype `$Text`, runtime lookup, renderer color tags, and the project-formatting boundary. - [Image And Sprite Formats](Docs/en/how-to/content/image-format.md) - image source formats, FOFRM composition, legacy selectors, baked sprite records, runtime factories, atlases, caches, and validation. - [Effect Format](Docs/en/how-to/content/effect-format.md) - `.fofx` sections, passes, render state, shader resources, baking outputs, runtime cache, script values, and validation. - [Docs/en/how-to/content/particle-format.md](Docs/en/how-to/content/particle-format.md) - optional SPARK/Effekseer selection, `.spark`/`.efkproj` authoring, `.spk`/`.efk` baking, Mapper tools, runtime routes, integrations, and validation. - [Docs/en/how-to/tools/particle-authoring.md](Docs/en/how-to/tools/particle-authoring.md) - operational Particle Preview, SPARK editor, pinned Effekseer editor, focused-viewer, and visible-validation workflow with versioned screenshots. - [Docs/en/how-to/content/font-format.md](Docs/en/how-to/content/font-format.md) - FOFNT and BMFont descriptor syntax, resource delivery, slots, binding scale, measurement, wrapping, rendering flags, inline colors, and validation. - [Audio](Docs/en/how-to/content/audio.md) - WAV/ACM/Ogg delivery and decoding, effect-name/variant lookup, music/repeat behavior, mixing, headless boundaries, and project validation. - [Video](Docs/en/how-to/content/video.md) - experimental Ogg/Theora delivery and decoding, fullscreen queue/input/music behavior, embedded playback, memory, and project validation. - [Frontend and Rendering](Docs/en/explanation/rendering/) owns Engine-native GUI input/render primitives; high-level GUI libraries and declarative layouts are project-owned. - [Docs/en/how-to/scripting/style-and-refactoring.md](Docs/en/how-to/scripting/style-and-refactoring.md) - reusable `.fos` module construction, source layout, formatter behavior, generated-file discipline, attributed calls, refactoring batches, and validation gates. - [Model Animation](Docs/en/how-to/content/model-animation.md) - `.fo3d` animation tuples, authored speed, one-step aliases, common duration metadata, script lookup, and project boundary. - [Sprite Root Motion](Docs/en/how-to/content/sprite-root-motion.md) - 2D per-frame `NextX`/`NextY` offsets, walk/run cycle anchoring, movement-driven frame selection, and project validation. - [Docs/en/contributing/contract-change-management.md](Docs/en/contributing/contract-change-management.md) - multi-domain generated contract diff, shared disposition ledger, and CI enforcement workflow. - [Docs/en/contributing/coding-contracts/smart-pointers.md](Docs/en/contributing/coding-contracts/smart-pointers.md) - native smart-pointer vocabulary (`ptr`/`nptr` borrows, `unique_*`/`refcount_*` owners, engine-own `shared_ptr`/`weak_ptr`), raw-pointer allowlist, and audit expectations. - [Docs/en/contributing/coding-contracts/exception-safety.md](Docs/en/contributing/coding-contracts/exception-safety.md) - engine-invariant stability under exceptions: terminate-on-OOM allocation model, entity-lifecycle throw-as-signal contract, and the `FO_STRONG_ASSERT` disposition rules. - [Docs/en/contributing/coding-contracts/thread-safety-analysis.md](Docs/en/contributing/coding-contracts/thread-safety-analysis.md) - `FO_TSA_*` Clang Thread Safety Analysis annotations, locking primitives, and `-Werror=thread-safety` enforcement. - [Docs/en/how-to/tools/mapper.md](Docs/en/how-to/tools/mapper.md) - mapper automation and native mapper helper integration points. - [Docs/en/how-to/tools/mapper-interactive.md](Docs/en/how-to/tools/mapper-interactive.md) - stock interactive Mapper menus, windows, editing, history, save discipline, failure diagnosis, and full-window capture workflow. - [Animation and Particle Viewers](Docs/en/how-to/tools/animation-particle-viewers.md) - standalone/Mapper AnimationViewer and ParticleViewer build, controls, review evidence, and validation boundaries. - [Web Build, Packaging, and Browser Debugging](Docs/en/how-to/platforms/web-debugging.md) - web target build/package/browser workflow. - [Android Build, Packaging, and Device Debugging](Docs/en/how-to/platforms/android-debugging.md) - Android target build, package, device, and release-qualification workflow. - [Docs/en/reference/script-api/index.md](Docs/en/reference/script-api/index.md) - generated human reference for the native-codegen API surface. - [Docs/generated/api.json](Docs/generated/api.json) - canonical machine-readable native-codegen API model. - [Docs/en/reference/cmake/index.md](Docs/en/reference/cmake/index.md) - generated human reference for project options, stages, hooks, and selected CMake helpers. - [Docs/generated/cmake.json](Docs/generated/cmake.json) - canonical machine-readable CMake project-interface model. - [Docs/en/reference/buildtools/index.md](Docs/en/reference/buildtools/index.md) - generated human reference for the main BuildTools command line. - [Docs/generated/cli.json](Docs/generated/cli.json) - canonical machine-readable BuildTools CLI model. - [Docs/en/reference/helper-cli/index.md](Docs/en/reference/helper-cli/index.md) - generated human reference for engine-owned helper-script command lines. - [Docs/generated/helper-cli.json](Docs/generated/helper-cli.json) - canonical machine-readable helper CLI model and ownership inventory. - [Native extension reference](Docs/en/reference/native-extension/index.md) - generated role, hook, and native binding reference. - [Docs/generated/native-extension.json](Docs/generated/native-extension.json) - canonical machine-readable native-extension model. - [generated prototype-format reference](Docs/en/reference/prototype-format/index.md) - generated prototype grammar, built-in properties, and validation reference. - [Docs/generated/prototype-format.json](Docs/generated/prototype-format.json) - canonical machine-readable prototype-format model. - [Docs/en/reference/map-format/index.md](Docs/en/reference/map-format/index.md) - generated map grammar, ownership, properties, baking, and validation reference. - [Docs/generated/map-format.json](Docs/generated/map-format.json) - canonical machine-readable map-format model. - [Docs/en/reference/model-format/index.md](Docs/en/reference/model-format/index.md) - generated model-description grammar, assets, composition, animation, and validation reference. - [Docs/generated/model-format.json](Docs/generated/model-format.json) - canonical machine-readable model-format contract. - [Generated text-format reference](Docs/en/reference/text-format/index.md) - generated text-pack, language, prototype-text, runtime, and validation reference. - [Docs/generated/text-format.json](Docs/generated/text-format.json) - canonical machine-readable text-format contract. - [Generated effect-format reference](Docs/en/reference/effect-format/index.md) - generated effect syntax, render-state, resource, baking, runtime, and validation reference. - [Docs/generated/effect-format.json](Docs/generated/effect-format.json) - canonical machine-readable effect-format contract. - [Docs/en/reference/particle-format/index.md](Docs/en/reference/particle-format/index.md) - generated particle backend, source/runtime form, baking, rendering, tooling, integration, and validation reference. - [Docs/generated/particle-format.json](Docs/generated/particle-format.json) - canonical machine-readable particle-format contract. - [Docs/en/reference/font-format/index.md](Docs/en/reference/font-format/index.md) - generated font descriptor, binding, layout, rendering, and validation reference. - [Docs/generated/font-format.json](Docs/generated/font-format.json) - canonical machine-readable font-format contract. - [Docs/en/reference/packages/index.md](Docs/en/reference/packages/index.md) - generated human reference for package declarations, support matrix, payloads, and artifacts. - [Docs/generated/package.json](Docs/generated/package.json) - canonical machine-readable package interface model. - [Docs/en/reference/public-contract/index.md](Docs/en/reference/public-contract/index.md) - generated public contract index and stability notes. - [First FOnline Headless Project](Docs/en/tutorials/first-project.md) - tested first headless project tutorial. - [Examples/MinimalProject/README.md](Examples/MinimalProject/README.md) - canonical engine-owned starter and smoke contract. - [Examples/ContentShowcase/README.md](Examples/ContentShowcase/README.md) - canonical engine-owned content gallery, provenance, performance-budget, and capture contract. - [Docs/en/reference/public-examples/index.md](Docs/en/reference/public-examples/index.md) and [Docs/generated/public-examples.json](Docs/generated/public-examples.json) - checked public-example repository registry from `Examples/PublicRepositories.json`. - [Docs/en/reference/platforms/generated-matrix.md](Docs/en/reference/platforms/generated-matrix.md), [Docs/generated/support-matrix.json](Docs/generated/support-matrix.json), and [Docs/generated/translation-status.json](Docs/generated/translation-status.json) - generated support evidence and locale parity; regenerate with their owning BuildTools scripts. - [Source/README.md](Source/README.md) - source-tree overview. - [Source/Tests/README.md](Source/Tests/README.md) - engine unit-test suites. - [BuildTools/README.md](BuildTools/README.md) - build-tooling notes. ## Validation Routing - **Zero tolerance for warnings.** Engine C++ builds and codegen must finish clean; there is no acceptable warning backlog (Clang thread-safety analysis already runs as `-Werror=thread-safety`). Never introduce a new warning; if a change surfaces one, resolve it at the root in the same change rather than suppressing it. - Engine C++ changes: build and run the embedding project's generated engine unit-test target (`_UnitTests` / `RunUnitTests`). - Build-system changes: update `BuildTools/cmake/ProjectInterface.json` when the project-facing CMake surface changes, update [Project-Local Dependencies](Docs/en/how-to/native-extensions/project-dependencies.md) when role-scoped project linking or dependency integration changes, regenerate the CLI reference when `BuildTools/buildtools.py::create_parser()` changes, run the affected structural/generated-reference checks and the aggregate contract diff, and validate the affected preset or BuildTools command in the embedding project that exercises it. - Platform-packaging changes: update `BuildTools/PackageInterface.json` when targets, platforms, architectures, packs, payloads, or artifacts change; regenerate its model/reference, run the structural package test and aggregate contract diff, validate the relevant package path (`Raw`, `Raw+WebServer`, Android package, etc.), and update the platform doc. - Native-extension composition or hook changes: update `BuildTools/NativeExtensionInterface.json`, regenerate its model/reference, run `validate_native_extension_interface.cmake`, the aggregate contract diff, and the minimal starter or affected embedding-project path. - Prototype parser, property text loading, `HasProtos` metadata, or `Baking.ProtoFileExtensions` changes: update `BuildTools/PrototypeFormatInterface.json`, [Prototype Format](Docs/en/how-to/content/prototype-format.md), regenerate/check the prototype-format model/reference, run its focused test and aggregate contract diff, then rebake an affected embedding project. - Map parser, mapper serialization, map baker, `ItemOwnership`, static-map loading, or map materialization changes: update `BuildTools/MapFormatInterface.json`, [Docs/en/how-to/content/map-format.md](Docs/en/how-to/content/map-format.md), regenerate/check the map-format model/reference, run its focused test and aggregate contract diff, then run engine map tests and rebake an affected embedding project. - `.fo3d` parser state/tokens, FBX/OBJ import, model layers, attachments, particles, transforms, materials, cuts, rendering flags, or compile-time model limits: update `BuildTools/ModelFormatInterface.json` and [Docs/en/how-to/content/model-format.md](Docs/en/how-to/content/model-format.md), regenerate/check the model-format reference, run `BuildTools/tests/test_docs_model_format.py`, the aggregate contract diff, focused model-baker tests, and a visible embedding-project scene. - `.fotxt` parsing, language selection/normalization, `TextBaker`, prototype `$Text`, text script methods, or inline color tags: update `BuildTools/TextFormatInterface.json` and [Text and Localization](Docs/en/how-to/content/text-and-localization.md), regenerate/check the text-format reference, run `BuildTools/tests/test_docs_text_format.py`, the aggregate contract diff, focused text-baker tests, and an embedding-project bake plus visible language check. - `.fofx` sections/state, `EffectBaker`, built-in shader resources, renderer bindings, `EffectManager`, effect script methods, or `FO_EFFECT_*` limits: update `BuildTools/EffectFormatInterface.json` and [Effect Format](Docs/en/how-to/content/effect-format.md), regenerate/check the effect-format reference, run `BuildTools/tests/test_docs_effect_format.py`, the aggregate contract diff, focused effect-baker tests, and visible checks on every affected backend/profile. - `ImageBaker`, FOFRM fields/flattening, any built-in image loader, the baked sprite container, `DefaultSpriteFactory`, `SpriteManager` image dispatch/cache, or `TextureAtlas` upload behavior: update `BuildTools/ImageFormatInterface.json` and [Image And Sprite Formats](Docs/en/how-to/content/image-format.md), regenerate/check the image-format reference, run `BuildTools/tests/test_docs_image_format.py`, the aggregate contract diff, focused image/atlas tests, and an affected embedding-project bake plus visible client checks. - `FO_*_PARTICLES`, `.spark`/`.efkproj` source parsing, `.spk`/`.efk` baking, backend composition, `SparkQuadRenderer`, Effekseer callbacks, Mapper particle tools, `ParticleManager`, `ParticleSpriteFactory`, script methods, or model-particle integration: update `BuildTools/ParticleFormatInterface.json` and [Docs/en/how-to/content/particle-format.md](Docs/en/how-to/content/particle-format.md), regenerate/check the particle-format reference, run `BuildTools/tests/test_docs_particle_format.py`, the aggregate contract diff, focused baker/runtime/model tests, and an affected embedding-project bake plus visible backend/integration checks. - Mapper/SPARK UI, full-window capture, a versioned screenshot, its owning prose, capture fixture, or source provenance: update [Docs/en/how-to/tools/mapper-interactive.md](Docs/en/how-to/tools/mapper-interactive.md) and/or [Docs/en/how-to/tools/particle-authoring.md](Docs/en/how-to/tools/particle-authoring.md), update `BuildTools/DocumentationScreenshots.json`, recapture from the recorded example/profile, regenerate `Docs/generated/screenshots.json`, and run `BuildTools/tests/test_docs_screenshots.py` plus the rendered-site image gates. - `.fofnt`/`.fnt` parsing, `Baking.RawCopyFileExtensions`, `FontManager`, `FontType`, `FontFlag`, `TextFormat`, `Game.BindFont`, text measurement, wrapping, or inline color behavior: update `BuildTools/FontFormatInterface.json` and [Docs/en/how-to/content/font-format.md](Docs/en/how-to/content/font-format.md), regenerate/check the font-format reference, run `BuildTools/tests/test_docs_font_format.py`, the aggregate contract diff, native unit tests, and an affected embedding-project bake plus focused measurement and visible rendering checks. - `SoundManager`, `ResourceManager` audio indexing, WAV/ACM/Ogg decoding, `Game.PlaySound`, `Game.PlayMusic`, `Audio.*`, `AppAudio`, or raw-copy audio delivery: update `BuildTools/AudioInterface.json` and [Audio](Docs/en/how-to/content/audio.md), regenerate/check the audio reference, run `BuildTools/tests/test_docs_audio.py`, the aggregate contract diff, native unit tests, and an affected embedding-project bake plus audible visible-client checks on every claimed platform. - `VideoClip`, Ogg/Theora decoding, `Game.PlayVideo`, fullscreen queue/input/music/drawing, `VideoPlayback`, `Game.DrawVideoPlayback`, or raw-copy OGV delivery: update `BuildTools/VideoInterface.json` and [Video](Docs/en/how-to/content/video.md), regenerate/check the video reference, run `BuildTools/tests/test_docs_video.py`, the aggregate contract diff, native unit tests, and an affected embedding-project bake plus visible video acceptance on every claimed platform. - Native GUI input/render primitives or their script exports: update [Frontend and Rendering](Docs/en/explanation/rendering/) and the affected generated API, then run focused native tests plus an embedding-project compile/bake and visible interaction checks. High-level GUI libraries and declarative layouts are project-owned; do not describe them as Engine-owned. - AiControl framing, methods, errors, common command fields, authorization, queue/event bounds, lifecycle, security, reference client, or sample behavior: update `BuildTools/AiControlProtocol.json` and [Docs/en/how-to/ai-control-protocol.md](Docs/en/how-to/ai-control-protocol.md), regenerate/check the AiControl protocol and helper-CLI references, run both focused AiControl test files plus the aggregate contract diff, and validate every affected embedding-project native/client/MCP path. Keep project observations, game actions, administrator tools, and MCP namespaces project-owned. - Model animation tokens, mesh clip durations, alias selection, `ModelInfoBaker`, `ModelAnimationInfo.foinfo`, metadata registration, or duration script methods: update [Model Animation](Docs/en/how-to/content/model-animation.md), run `BuildTools/tests/test_docs_model_animation.py`, regenerate/check the native API/reference when exports changed, run the focused native tests, and rebake an affected embedding project. - Image-frame `NextX`/`NextY`, baked sprite offsets, `SpriteSheet`, `MovingContext` interpolation, `GeometryHelper::NormalizeHexOffset`, or `CritterHexView` walk/run phase behavior: update [Sprite Root Motion](Docs/en/how-to/content/sprite-root-motion.md), run `BuildTools/tests/test_docs_sprite_root_motion.py` plus focused image-baker/geometry tests, then validate the affected locomotion in a visible client scene. - Public example portfolio, ownership, common policy/workflow files, starter source, compatibility boundaries, or published example pins: update `Examples/PublicRepositories.json` and [Public Example Repositories](Docs/en/how-to/build/public-example-repositories.md), regenerate/check the public-example model, validate affected repositories in both pinned/current modes, and update only verified public links. - AngelScript module construction, source conventions, CoreScripts formatting, namespace/file ownership, attributed calls, mutable globals, or `.fos` refactoring guidance: update the [AngelScript style and refactoring guide](Docs/en/how-to/scripting/style-and-refactoring.md) and its Russian mirror, run `BuildTools/tests/test_docs_angelscript_style.py`, use the owning Engine or project formatter, and compile/test every affected script role. - Managed C# configuration, generated project/assembly shape, attributes, async callbacks, synchronization covers, analyzers, marshalling, runtime/load-context lifetime, packaging, platform wiring, or migration: update the [Managed C# scripting guide](Docs/en/how-to/scripting/managed-csharp.md) and its Russian mirror, run `BuildTools/tests/test_docs_managed_csharp.py` plus affected `test_managed_*.py` and `Test_ManagedScriptBaker` routes, then compile/bake every affected project role. - Fenced documentation changes: declare a supported language, regenerate/check `Docs/generated/snippets.json`, run `BuildTools/tests/test_docs_snippets.py`, and run `python BuildTools/docs_snippets.py --check --external`; then run the semantic compile/bake/smoke owner for every claimed outcome. - Script/native API boundary changes: update nullability/API docs, review the source-owned `///@ ApiContract` disposition, run the aggregate contract diff described in `Docs/en/contributing/contract-change-management.md` (using the specialized API report when deeper symbol context is useful), and run the smallest test target that covers the changed binding. For project remote calls, bake both sides and check the project catalog described in `Docs/en/reference/scripting/remote-calls.md`. - When a serialized contract changes (entity properties, network messages, save data), update the relevant `///@ MigrationRule` metadata. - Engine changes that affect network interaction or are otherwise substantial enough to matter for client/server runtime compatibility must force a compatibility-version change by bumping the central marker in `Source/Common/Common.h`. Locate and increment the current marker instead of copying a fixed value from documentation: ```bash rg -n "MigrationRule Version" Source/Common/Common.h ``` ## Behavioral Rules - **Script callers own the complete native entity cover.** Before an ordinary server `FO_SCRIPT_API` call, AngelScript must acquire every existing entity the native call graph can read or mutate, including transitive map/location, holder, controlled-critter, and group dependencies. A native path reachable from that call may validate the prepared package with `ValidateEntityAccess()` and may call `EnsureEntitySynced()` freely and repeatedly to retain the own lock of an entity already covered by the current context. Both operations are idempotent no-ops once their condition is satisfied, so loops, repeated same-target calls, `catch` handlers, and recursion carry no call-count restriction. Each actual `EnsureEntitySynced()` acquisition never releases/reacquires the caller cover and never parks on a lock: it only ADDS the target's own lock plus the intermediate ancestor marks, as one all-or-nothing state-mutex transaction. Because the covering ancestor is held exclusively, any block it meets is a foreign thread mid-flight through its own non-blocking pass (releasing marks by lock address rather than hierarchy, or rolling back an ascending-address try-prefix), which clears in microseconds — so the acquisition is MANDATORY and retries the whole batch with bounded back-off until it lands, retaining nothing across the back-off. It throws only when the target is not actually covered or the covered-cover invariant is corrupt, never for ordinary transient contention, and a failure leaves the caller cover intact. It cannot repair an omitted unrelated existing dependency. Fresh parentless entities are captured only by the private trusted registration boundary before publication; that boundary never authorizes discovery or acquisition of pre-existing dependencies. Native code must not call `SyncEntity()` / `SyncEntities()` or otherwise obtain missing existing cover on the script's behalf. The explicit synchronization primitives `Game.Sync`, `Game.SyncRelease`, `Game.Lock`, and `Game.Unlock` are the exceptions because changing synchronization state is their script-visible purpose. The whole contract is opt-out: with the `Server.SingleThreadedLogic` setting the engine runs its logic on one worker, the cover model is bypassed, and scripts need no synchronization at all (see [Docs/ServerRuntime.md](Docs/ServerRuntime.md)). - **Fix the source, never mask.** When a lower layer returns a wrong result, swallows an error, or silently coerces bad input into "looks fine", fix it at the source (throw / validate / propagate) instead of papering over it from the caller with pre-checks, try/catch that fabricates a "good" value, default substitutes, or other workarounds. If you find yourself adding a guard that compensates for a bug elsewhere, stop and fix the bug instead. - **No permanent legacy compatibility in the reusable engine.** When an engine API, configuration key, data format, or internal contract is replaced, remove the old surface completely. Do not retain deprecated aliases, fallback parsing, or migration shims. A narrow refusal of an older build or transitional bridge needs a documented reason and a dated removal marker below; migration of database-persisted game data belongs to the embedding project. - **Code that knows an old format carries its removal date.** Mark every necessary old-build refusal or transition point, tests included, with `FO_TEMPORARY_COMPAT(Id, "YYYY-MM-DD");` (native) or `[TemporaryCompat("Id", "YYYY-MM-DD")]` (managed). One id and date cover all places of one compatibility. `BuildTools/temporary_compat.py` fails after the date and is run by the Engine validation workflow; embedding projects can pass their own source directories too. See [temporary compatibility](Docs/en/reference/native/essentials.md#temporary-compatibility). - **Keep engine invariants stable under exceptions.** An exception thrown partway through a multi-step engine mutation (entity create/register/destroy/invalidate, cross-entity links) must never leave a half-mutated, restart-only state. Engine allocation terminates on OOM (`safe_alloc`/`safe_allocator`), so `bad_alloc` is not a recoverable error and never a reason to build a rollback. A post-mutation invariant that "can only be false if the world is already corrupt" is a `FO_STRONG_ASSERT` (always-on, deterministic exit), not a swallowable `FO_VERIFY_AND_THROW`. Entity create/destroy is throw-as-signal, not transactional rollback (events may legitimately destroy or relocate the entity; the function throws but the entity is left in a valid state) — pinned by `Source/Tests/Test_EntityLifecycle.cpp` and `Test_ServerMapOperations.cpp`, so do not add a blanket create-time rollback. Full rules: [Docs/en/contributing/coding-contracts/exception-safety.md](Docs/en/contributing/coding-contracts/exception-safety.md). - **No singletons. No hidden global / static state for engine-semantic data.** State with *per-engine semantics* (lock sets, entity registries, ticket allocators tied to one engine's wait queues, mutable runtime caches that could be mixed across engines) lives on an owning instance reachable through the engine object graph — typically `ServerEngine` (managers), `ServerEntity` (per-entity locks, properties, parent), or `BaseEngine` (`ScriptSystem`, settings). Do not introduce file-scope mutable statics, `static` data members on engine classes, function-local statics that cache cross-call state, or unnamed-namespace mutable variables. Multiple engine instances may coexist in one process (embedding projects run parallel test suites this way), so hidden globals silently share state across engines, hide cross-test pollution, and turn lifetime-ordering bugs into timing windows. - **`static thread_local` is OK *only* when threads are partitioned by engine ownership** and the slot can never observe values from a foreign engine on the same thread. The current example is `SyncContext* CurrentContext` in `Source/Server/EntitySync.cpp`: every thread that touches it belongs to exactly one engine, so the thread-local slot is implicitly per-engine. The same logic permits a `static std::atomic` whose value has no per-engine semantics (e.g. a monotonic ticket counter only ever compared inside one lock's wait queue). When in doubt, prefer instance ownership — but do not pay a mutex-plus-map lookup just to satisfy the rule when a `thread_local T*` is already correctly isolated. - **`static` is otherwise acceptable only for** compile-time constants (`static constexpr`), file-local pure helper functions (`static void Helper(...)`), and `static_assert`. An existing static that holds per-engine state is a bug to migrate to instance ownership, not a precedent to copy. ## Style Notes - Prefer existing engine idioms over new local abstractions. - Enum-entry descriptions use trailing declaration comments; value-layout field descriptions use `// field: ...` lines immediately before `ExportValueType`. These field lines are metadata, not multi-line prose; type-description prose retains the two-line limit. See [generated metadata](Docs/en/reference/metadata/index.md#source-owned-symbol-descriptions). - Use `struct` only for passive data aggregates: no user-defined constructors, methods, or hidden invariants. When behavior or construction logic belongs on the type, make it a `class` and apply full encapsulation with private state and a deliberate public interface. Do not mark such structs `final` — a plain data aggregate needs no inheritance guard, the keyword only complicates it. - **The code is the documentation; a comment carries only what the code cannot.** Names, types, and structure already state *what* happens, so a comment that re-tells them is deleted, not shortened — and the default state of a line of engine code is *no* comment. Write one only for what the reader cannot see: why the code sits here rather than elsewhere, why this approach instead of the obvious alternative, the gist of a genuinely dense block, or a contract that is not derivable from the code (sentinel meanings and units, threshold calibration, concurrency/ownership/lifetime requirements, edge-case exemptions, why a plausible alternative is unsafe). **One line is the default and two is the ceiling** — no multi-line essays. When the explanation genuinely needs more room, it belongs in the owning `Docs/` page with a short link from the code; never link a temporary plan. Do not narrate the next statement, restate a signature or parameter list, enumerate steps the code already spells out, or record history, fixed-bug stories, or benchmark numbers (those belong in commit messages and docs). **A comment points at code, never at bookkeeping**: no task, ticket, issue, or PR numbers, no dates or "as of" wording, no version or release markers, no author names — that metadata lives in the commit and the tracker, and in code it only rots. Refer to a symbol, a function, a file, or the test that pins the behavior; the sole non-code target is the owning `Docs/` page named above. Short structural headings are allowed when they materially improve navigation in long flat code. Only the concluding sentence of a comment drops its period (the single line of a one-line comment, the final line of a two-line block, or a trailing comment); every preceding sentence keeps its period. Capitalize prose, not symbols (a leading lowercase code identifier stays as written). Fixed-text banners such as the license header, and commented-out code, are exempt. - **A type whose whole content is static members is a namespace, not a class.** A deleted constructor plus nothing but statics is a namespace written in class syntax: it can never be instantiated, it has no state and no invariant, and `X::f()` reads the same either way — so write `namespace X { ... }` and drop the `static`, which at namespace scope would mean internal linkage instead. Two things change with the spelling and are worth knowing: a namespace resolves names in **declaration order** (a class body does not, so a helper called from an inline function above it must be declared first), and a namespace **cannot be a friend**. That second point is the exception: `safe_alloc` keeps its class shape precisely because a dozen types declare `friend class safe_alloc` so its factories can reach their private constructors. There, the class-ness is load-bearing rather than accidental. - **Do not write `extern` on a function.** At namespace scope a function already has external linkage, so the keyword states the default and says nothing. Keep it only where it still decides something: a variable declaration, where `extern` is what makes the line a declaration rather than a definition, and an `extern "C"` language-linkage boundary. - **A free function that has to carry its module's name gets that module's namespace instead.** `write_base_log`, `fs::exists`, `mem_copy`, `get_stack_trace` and `report_exception_and_continue` were all spelling their module into the identifier because nothing else could — so the module becomes a namespace and the word comes out of the name: `logging::write_base`, `fs::exists`, `memory::copy`, `stack_trace::get`, `exceptions::report_and_continue`, alongside the `winapi::` / `posix::` / `platform::` / `utf8::` modules that already read that way. Once a module takes a namespace its whole free surface moves in, types included, so it is never split across two scopes. The test is the name, not the module: a function that needed no prefix is already the layer's vocabulary and stays bare — `numeric_cast`, `safe_call`, `copy_hold_ref`, `coarse_sleep`, `run_async`, `exit_app`, the `vec_*` helpers. One name is not ours to pick: `log` collides with `::log` from ``, which vendored headers call unqualified, so the logging module is `logging::`. - **Naming follows the layer: `Source/Essentials/` is snake_case, everything above it is PascalCase.** The foundation layer is the engine's standard-library-shaped vocabulary, so its types, free functions, methods, public fields and private members are spelled the way `std::` spells its own — `data_writer`, `safe_alloc::make_unique`, `logging::write`, `_read_pos`. Every layer above it (`Source/Common/`, `Client/`, `Server/`, `Tools/`, and an embedding project's own native sources) stays PascalCase, so the spelling of a name tells a reader which side of the boundary it lives on. Four things keep PascalCase *inside* Essentials because they are not the layer's to name: **template parameters** (`CharT`, `Traits`, `Alloc`, `InlineCapacity` — the standard library's own convention), **exception type names** (`XException` is an engine-wide convention that Essentials only seeds), **the ref-count protocol as AngelScript spells it** — `AddRef` / `TryAddRef` / `Release` on `asIScriptFunction` and `asITypeInfo` (`refcountable` accepts either spelling and `refcount_ptr` dispatches on whichever the pointee declares, so `refcounted` uses `addref` / `release` / `get_refcount` and only the library types keep their own), and **foreign API names** — the OS calls re-spelled inside `WinApi.cpp` / `Posix.cpp`, and the global crash hooks in `ExceptionHandling.cpp` that the vendored `backward.hpp` declares by name. Module file names stay PascalCase as well (`MemorySystem.h`), because the `Essentials.h` include-order block and `BuildTools/tests/test_essentials_layering.py` are keyed on them. See [Docs/en/reference/native/essentials.md](Docs/en/reference/native/essentials.md). - Reuse the existing MIT source-file header and `#pragma once`; match the surrounding module-level layout. - Module layout: put class definitions and static-function forward declarations near the top of the translation unit, then implementations ordered from high-level entry points down to low-level helpers, so a reader meets the public/orchestrating code first. - **Inside a class, what reports on the object comes before what changes it.** A class body opens with construction and destruction, then the informational block — the `[[nodiscard]]` accessors (`Get*`, `Has*`, `Is*`, `Check*`) a reader consults to learn what the object holds — and only below them the methods that mutate it, ordered by lifecycle: the initializing ones (`Set*`, `Add*`, `Attach*`, `Load*`) before the finalizing ones (`Remove*`, `Detach*`, `Clear*`, the teardown helpers). So `Critter::ClearAllAssociations` sits at the bottom of the mutating run rather than beside the destructor it serves, and `MapManager::ClearStaticMaps` below `ViewMap` rather than inside the accessor block. Blank lines separate the groups, and a family that already owns a block of its own — the `Send_*` / `Broadcast_*` runs, the `///@ ExportEvent` declarations — keeps it where the file puts it. - Every standalone native module must be created as a complete `.h` + `.cpp` pair and both files must be registered in the owning build source list. A named subsystem/module must never be introduced as a lone header; keep the translation unit as the module anchor even when all current declarations are passive data and no out-of-line implementation is needed yet. Pure template/constexpr/umbrella headers are not standalone modules. - Order code top-down by importance and abstraction level: high-level entry points and orchestration first, secondary helpers and low-level details below, unless a nearby file has a stronger established ordering. - **A `.cpp` function earns a profiling zone by what it explains: `FO_TRACE_ZONE()` as its first statement**, followed by one blank line when the body continues. It goes on what frames the capture (main loop and tick steps), on units of work whose cost is non-trivial or varies, on what can block, and on rare expensive operations. It stays off accessors and trivial forwarders, small pure helpers, anything called per element in a hot loop, member-only constructors and destructors, the profiler's own path (allocator, logging, stack traces, crash handling), functions that do not return during a capture, headers, lambdas and tests. The category is the subsystem the work belongs to, not necessarily the file's; the categories are the `FO_TRACE_COLOR_` lines of `Source/Essentials/BasicCore.h`, and a build compiles in only those `FO_TRACE_CATEGORIES` selects (every zone category by default; allocation tracking and log messages are the opt-in `Memory` and `Log`, declared by `FO_TRACE_OPT_IN_`). **`FO_SCRIPT_API` script-export functions (`///@ ExportMethod`, `///@ EngineHook`, and the other script-bound exports) take *no* zone — the generated binding that calls them opens a named `Script` zone — and neither do the file-local helpers only exports call.** The full rules: [profiling guide](Docs/en/how-to/quality/profiling.md#placing-zones). - Separate semantically distinct code blocks with a blank line. In particular, surround every control-flow statement (`if`/`else`, `for`, `while`/`do`, `switch`, `try`/`catch`, and similar constructs) with a blank line before and after the complete statement. Blank lines may be omitted only inside a consecutive run of the same construct (`if` + `if`, `for` + `for`, and so on). - Treat preprocessor directives as transparent to the blank-line rule: do not add or remove blank lines merely because code is enclosed by `#if`/`#else`/`#endif`; apply spacing to the surrounding C++ blocks as if the directives were not there. - Keep a comment attached to the code block it describes below: put a blank line before the comment, but no blank line between the comment and that block. For control-flow spacing, the comment is part of the following block. - Prefer `FO_VERIFY_AND_THROW` over silently masking unexpected states. - **The condition of a verification macro is a predicate, not an operation.** Write the condition of `FO_VERIFY_AND_THROW`, `FO_VERIFY_AND_CONTINUE`, `FO_VERIFY_AND_RETURN`, `FO_VERIFY_AND_RETURN_VALUE`, `FO_STRONG_ASSERT` and `FO_BASIC_STRONG_ASSERT` as if the macro were stripped like an `assert`: any work the rest of the function depends on — a write, a flush, a read that fills a buffer or advances a reader, an insertion checked through its `.second`, a file-system change, a job `Run()`, a call that fills an out parameter — happens first into a named local, and the macro only reads the answer: `bool written = _file.write(data) && _file.flush(); FO_VERIFY_AND_THROW(written, "Can't write resource patch", path);`, never `FO_VERIFY_AND_THROW(_file.write(data) && _file.flush(), ...)`. The macros are never compiled out, so this is not about losing the call: an operation inside a check reads as a check and is skimmed past as one, and a check that acts cannot be moved, reworded or swapped for another variant without changing what the function does. See [Docs/ExceptionSafety.md](Docs/ExceptionSafety.md); embedding projects may gate the generic shapes with an audit tool (Last Frontier: `Tools/CiChecks/check_verify_conditions.py`). - **Throw engine exceptions with a fixed message plus context arguments, never a pre-formatted string.** `FO_DECLARE_EXCEPTION` exception types and `FO_VERIFY_AND_THROW` take a constant `string_view` message followed by variadic context values; the base exception preserves the bare message and appends each value as its own `- value` line. Write `throw SomeException("Fixed human-readable message", id, path, value);` and `FO_VERIFY_AND_THROW(cond, "Fixed message", ctx...);`, not `throw SomeException(strex("... {} ...", value));`. A constant message keeps every occurrence of the same failure identical, so failures group and sort by message uniqueness while the variable parts travel in the arguments — the same rule the script-side `verify` macro follows. Do not build the message with `strex` / `std::format` / `+` from runtime values. - **Never throw anything that is not derived from `std::exception`, and treat `catch (...)` as a defect handler, not an error handler.** Every exception the engine raises comes from the `FO_DECLARE_EXCEPTION` hierarchy or the standard library, so a non-`std::exception` type walks past every `catch (const std::exception&)` recovery path and arrives with nothing to report. Wherever a `catch (...)` sits beside a `catch (const std::exception& ex)`, its entire body is `FO_UNKNOWN_EXCEPTION();` — which raises `StrongAssertationException`, reports it, and terminates the process. Do not log it and continue, fabricate an `"Unknown exception"` message, count it as an ordinary failure, or rethrow it as a domain exception. The only places that may swallow instead are no-throw teardown/unwind paths (`noexcept` bodies, `scope_exit`/`scope_fail` bodies, `safe_call` targets, destructors, C-ABI callbacks) and the reporting/logging machinery itself, which must not re-enter while reporting. Full rule: [Docs/en/contributing/coding-contracts/exception-safety.md](Docs/en/contributing/coding-contracts/exception-safety.md) §2.1. - **Write for exception safety.** A throw must never escape a function leaving a broken invariant. As you write or change engine code, derive the exception-safety level the body provides (`NoThrow` / `Strong` / `Basic` / `None`) per [Docs/en/contributing/coding-contracts/exception-safety.md](Docs/en/contributing/coding-contracts/exception-safety.md) (§5 disposition ladder, §8 levels): do fallible/validating work (throwing `numeric_cast`, duplicate/collision guards) before the first observable mutation, insert into the authoritative store before any derived cache, and undo an early flag/counter/lock/handle mutation on unwind with `scope_fail` (noexcept body) or RAII on raw OS/third-party resources. Never aim for `None` — it is a defect tier to fix, not to record. Allocation *terminates* on OOM (§1) so do not write allocation-only rollbacks, and entity create/destroy is *throw-as-signal* (`Basic` by design). If an embedding project maintains a per-function audit baseline, update that project-owned baseline in the same integration change; it is not normative engine evidence. - Use `numeric_cast` for numeric conversions; do not use `static_cast` for numeric narrowing/widening unless the surrounding code has a specific established reason. - Use fixed-width types (`int8_t`, `uint8_t`, `int16_t`, `uint16_t`, `int32_t`, `uint32_t`, `int64_t`, `uint64_t`, `float32_t`, `float64_t`, `size_t`) instead of bare `int` / `float` in new engine code. - Do not use `auto` for primitive values or simple obvious types such as `string`, `hstring`, `size_t`, and the fixed-width aliases. - **Do not put top-level `const` on an automatic local when removing it leaves the type contract unchanged** — write `int32_t count = GetCount();`, not `const int32_t count = GetCount();`. There is no engine-wide immutability-by-default rule, and parameters are out of scope. Constness that belongs to the value itself stays: a `const` pointee (`const Item* item`), a `const` referent (`const Item& item`), `const` array elements, and `constexpr`. Top-level `const` on a pointer (`int32_t* const pointer`) is removable and is diagnosed. A deliberate qualifier is kept with `// FO_REDUNDANT_CONST_SUPPRESS: ` on the declaration or the line above it, and the reason is mandatory. See [local variables](Docs/en/contributing/coding-contracts/local-variables.md); embedding projects gate this together with explicit simple local types and use-after-move (Last Frontier: `Tools/LocalVariableValidator/`, `Tools/ExplicitLocalTypes/`). - Use the engine smart-pointer vocabulary from `Source/Essentials/SmartPointers.h` — `ptr`/`nptr` for borrows, `unique_*`/`refcount_*` for owners, engine-own `shared_ptr`/`weak_ptr` via `safe_alloc::make_shared()` — instead of `std::` smart pointers or bare raw `T*`. Raw pointers remain only at the documented ABI/low-level allowlist boundaries; inside a function, bind them to wrappers before ordinary engine work and unwrap with `.get()` only at the final handoff. A checked `nptr` and every owner convert to `ptr` implicitly, so **do not write `.as_ptr()` where that implicit conversion applies** — a `ptr` parameter, member, typed local or return takes the value directly; spell `.as_ptr()` only where no conversion can happen (a deduced `auto` local, overload or template deduction, a lambda capture, a value needed after its owner moves). See [smart pointers](Docs/en/contributing/coding-contracts/smart-pointers.md); embedding projects may gate this with an audit tool (Last Frontier: `Tools/SmartPointerAudit/`). - **Use the engine container aliases from `Source/Essentials/Containers.h`, never their `std::` originals** — `string`, `wstring`, `vector`, `map`, `unordered_map`, `set`, `list`, `deque`, `stringstream`, `small_vector`. Each follows the terminate-on-OOM contract instead of throwing `std::bad_alloc`: `string` and `wstring` are the engine `basic_string` from `Source/Essentials/StringObject.h`, whose small-string buffer is the `FO_STRING_INLINE_CAPACITY` build option, `deque` is the engine `basic_deque` from `Source/Essentials/DequeObject.h`, whose block size is a template parameter, and the rest are the standard containers instantiated on `safe_allocator`. For allocation that cannot be expressed as a C++ container, use `safe_alloc`: the `make_*` family for typed objects, and the raw tier (`malloc_raw`/`calloc_raw`/`realloc_raw`/`free_raw`, `malloc_aligned_raw`/`free_aligned_raw`) for third-party C-ABI hooks. Do not reach for bare `malloc`/`free`, and do not use `std::format`/`std::vformat`/`std::to_string`, which materialise a `std::allocator` string — format into an existing buffer with `std::format_to`/`std::vformat_to` instead. Exceptions are limited to foreign ABI boundaries, the Essentials modules above `MemorySystem` in the include order, and standard types with no allocator parameter; each one is recorded with a written reason. See [Essentials](Docs/en/reference/native/essentials.md); embedding projects may gate this with an audit tool (Last Frontier: `Tools/AllocatorAudit/`). - **Use the engine callable wrappers from `Source/Essentials/FunctionObjects.h`, never `std::function`** — `function` (an alias of `move_only_function`) is the default, and `copyable_function` is for a stored callable that is genuinely copied, such as one snapshotted during dispatch or handed to several owners. Both keep a small nothrow-movable target inside the wrapper, so an ordinary closure allocates nothing. When a `function` member no longer compiles because something copies it, first check whether the copy should be a `std::move`; reach for `copyable_function` only when the copy is the actual contract. The sole remaining `std::function` is the `StackTrace.h` script-provider hook, which sits above the callable module in the Essentials order. See [Essentials](Docs/en/reference/native/essentials.md). - **The engine `string` behaves as `std::basic_string`, so write it as you would the standard string.** The one difference is the tunable inline buffer, which is a build option rather than a source-level choice. Three interop rules do not follow from the standard: text handed to a standard string stream is copied through `make_stream_string`, a `std::filesystem::path` is built from an engine string with `fs::make_path`, and `getline` must be called unqualified so ADL finds the engine overload. See [Essentials](Docs/en/reference/native/essentials.md). - **Use `random_generator` from `Source/Essentials/RandomGenerator.h`, never `std::mt19937` or `std::uniform_int_distribution`.** The standard engine costs 5000 bytes of state and 3.6 us to construct, but the deciding reason is that the standard distributions are implementation-defined: the same seed maps to different values on a Windows client and a Linux server. `next()` draws raw bits, `next_below` / `next_between` / `next_normalized` are the engine's own bounded draws and produce one sequence everywhere. See [Essentials](Docs/en/reference/native/essentials.md). - **Sleep with `coarse_sleep` or `precise_sleep` from `Source/Essentials/Threading.h`, never `std::this_thread::sleep_for`.** The standard call rounds up to the OS timer tick, so a 50 us request parks for 15 ms on a default Windows configuration. `coarse_sleep` parks and lands within half a millisecond at no CPU cost; `precise_sleep` hits the deadline to within microseconds by spinning the last millisecond, and is for waits whose duration was chosen on purpose. See [Essentials](Docs/en/reference/native/essentials.md). - **Call the operating system through `winapi::` or `posix::`, never directly.** `Source/Essentials/WinApi.*` and `Posix.*` are the only places `` and the POSIX headers are included, and their boundary carries engine types rather than OS ones. `Platform` dispatches between them; add a wrapper there instead of reaching for the OS from a consumer. Three implementations below the modules in the Essentials order keep their own calls because the layering leaves no alternative (`BasicCore.cpp`, `BaseLogging.cpp`, `StringUtils.cpp`), and two more are OS wrappers in their own right rather than consumers (`NetSockets.*`, `ServerServiceApp.cpp`). See [Essentials](Docs/en/reference/native/essentials.md). - **A setting is read, never written.** `Source/Common/Settings.inc` declares every entry with one macro, `SETTING(, , , )`, which makes the member `const`; `SetRuntimeSetting()` rejects a write to any of them, and `ManagedScriptBaker` emits the script-side `Settings.Group.Name` as a get-only property. A value that changes while the game runs therefore cannot live here — it belongs to the system that owns it, reached through that system's API (`MapView::SetVisibleLayers`, `AudioManager::SetMusicVolume`, `Application::ScreenState`, …). `BakerTests::OverrideSetting` is the one deliberate exception and is confined to test code. See [configuration and data sources](Docs/en/reference/settings/configuration-and-data-sources.md). - **A setting is addressed through its group.** `Source/Common/Settings.inc` declares a group as `SETTING_GROUP(, )` and holds its settings in a nested aggregate named after the group, so engine code reads `settings.Network.ServerPort` and a config key, a command-line override and `GetRuntimeSetting()` / `SetRuntimeSetting()` all spell it `Network.ServerPort`. A bare `Name` names no engine setting anywhere — it lands in custom settings, which the config baker reports as `Unknown setting` — and that is what leaves two groups free to declare the same short name. A `///@ Setting` declaration carries the whole `Group.Name` too. See [configuration and data sources](Docs/en/reference/settings/configuration-and-data-sources.md). - **Task-language definition:** in requirements, reviews, and conversation about engine code, informal wording such as “pointer”, “raw pointer”, “borrowed pointer”, “указатель”, or “сырой указатель” means a non-owning engine borrow, **not** permission to write a bare C++ `T*`. Use `ptr` when presence is guaranteed and `nptr` when absence is valid. A bare `T*` is authorized only when the request explicitly requires the literal C++ `T*` type **and** identifies a documented ABI/low-level boundary; if either condition is missing, the wrapper rule wins. - For `Entity` and derived types use **pointers**, not references. - Prefer `static` free functions for file-local helpers instead of unnamed namespaces. - Do not add `static` variables for hidden state or caches; see the Behavioral Rules above for the narrow allowed uses of `static`. - Add `const` and `noexcept` where they express the semantic contract; do not add them mechanically everywhere possible. For `noexcept` specifically: a `NoThrow` exception-safety classification alone **never** justifies the keyword — the ES baseline records the current fact, while `noexcept` is a contract that blocks future legitimate validating throws. Reserve it for move operations/swap, teardown/unwind-path callables (`scope_exit`/`scope_fail` bodies, `safe_call` targets), C-ABI callbacks, and documented no-throw primitives; see [Docs/en/contributing/coding-contracts/exception-safety.md](Docs/en/contributing/coding-contracts/exception-safety.md) §8. - Use `ignore_unused(...)` only for variables/objects; for an intentionally ignored function-call result, write `(void)FunctionCall(...)`. - For C++ string/text construction and parsing, prefer existing engine helpers such as `strex` and `strvex` when they make formatting or token handling clearer. If the helper surface is missing a repeated string-formatting operation, add a reusable helper in the appropriate engine utility layer instead of open-coding ad hoc parsing/formatting at call sites. - Gate conditional compilation on our own (`FO_*` / project) macros with `#if FOO` / `#if !FOO`, never `#ifdef` / `#ifndef`: every such macro is mandatorily `#define`d to `0` or `1`, so its definedness must never carry meaning — only its value does. - **Keep engine-owned source inventories unconditional.** Add every engine-owned `.h` / `.cpp` pair to its normal CMake source list even when the feature is optional; the files themselves own the `#if FO_*` guard. Do not mirror a feature toggle around `AppendList(...)`, and do not create a second feature-only `AppendList` for the guarded module. Likewise, do not wrap `#include` directives for self-guarded engine headers in a standalone feature conditional: include them normally and keep the feature guard around declarations/definitions that need it. Conditional third-party targets, link dependencies, and third-party headers whose include paths only exist with the feature may remain gated. - Include layering: non-Essentials engine code must consume Essentials modules through `Common.h`, not by including individual `Source/Essentials/*.h` headers directly. STL headers are centralized through `BasicCore.h`; add or adjust standard-library includes there instead of including `<...>` headers from higher layers. - **Essentials layering is strict.** The `Source/Essentials/Essentials.h` aggregate lists Essentials headers in include order — each Essentials header (and its `.cpp`) may only depend on headers listed *above* it in that block. This is a compile- and link-time rule: declaring a function in an early header but defining it in a later module is still a forbidden reverse dependency, even when no downward `#include` appears. Public declarations are implemented in their owning module (or an earlier one), and `BuildTools/tests/test_essentials_layering.py` enforces both direct-include direction and definition ownership for the namespace-level APIs a header declares. If a low-layer module needs information that physically lives in a higher layer, expose only what the lower layer can produce on its own or take the higher-layer value as a parameter — do not back-channel the include and do not reorder the block to work around it. - **Never `#include` an STL header outside the engine's `Source/Essentials/BasicCore.h`.** The standard library is not included per-file; the whole STL surface in use is included centrally through `BasicCore.h`. If a standard-library facility is missing, add its include to `BasicCore.h`, never to the consuming file. - **Iterating server-side entities while events may fire:** any script-visible event (e.g. `OnItemOnMapAppeared.Fire(...)`, `OnCritterDisappeared.Fire(...)`) can re-enter scripts and mutate world state — including destroying the very entity being iterated. The convention is: (1) snapshot the iteration set with `copy_hold_ref(...)` so each element is held by ref-count for the duration of the loop; (2) re-validate inside the loop with `if (cr->IsDestroyed()) continue;` (and after each event for any other entity you keep working with); (3) fire scripted events at the **end** of the unit of work, after non-revocable state changes are committed. - Keep edited source files ending with exactly one trailing blank line. - **A method the native backend resolves through Mono (`[CallableByEngine]`) is `internal`.** Native lookup (`mono_class_get_method_from_name`) finds non-public members; `private` looks unused to `IDE0051` because nothing in C# calls it. Helpers that only their own type uses stay `private`. In CoreScripts an embedding project's analyzer may gate this (Last Frontier: `LF0015`); `ManagedHost` is compiled into its own assembly, which those analyzers never see, so there the rule is held by review alone. - **Managed C# member names are PascalCase, whatever their access** — private fields included (`Continuations`, not `_continuations`); underscores may separate segments that each start with a capital letter or a digit, never lead or trail a name. A field that must stay a field beside a same-named property gets a name of its own (`RecordedGlobally` behind `GlobalCount`). Two surfaces keep a different spelling on purpose: the script-visible value types mirror the native API in lower case (`ipos.x`, `hstr()`, `timespan.milliseconds`), and code `ManagedScriptBaker` generates keeps `_`-prefixed internals (`_entityPtr`, `__event_OnStart`) so no property an embedding project declares can collide with them. - Keep docs reusable: describe engine behavior first; mention Last Frontier only as an embedding-project example, never as an engine-doc dependency or validation owner. - Keep `README.md` human-oriented and `AGENTS.md` AI-oriented. `CLAUDE.md` is intentionally only a pointer to `@AGENTS.md`. ===== END DOCUMENT ai-maintainer-entry ===== ===== BEGIN DOCUMENT exception-safety ===== Source: Docs/en/contributing/coding-contracts/exception-safety.md Canonical URL: https://fonline.ru/Docs/en/contributing/coding-contracts/exception-safety.html Content SHA-256: afb4d473355a7b4ac5947d87c2acb2a745c098bded2b3cec918e219e44f8e258 --- layout: default title: "Exception Safety & Engine-Invariant Stability" locale: en document_id: exception-safety permalink: /Docs/en/contributing/coding-contracts/exception-safety.html --- # Exception Safety & Engine-Invariant Stability How the engine stays in a consistent state when an exception is thrown, and the rules to keep it that way. The concern this doc answers: an exception thrown *partway through* a multi-step state mutation (entity create/register/destroy/invalidate, cross-entity links, persistence) must never leave the running process in a half-mutated state whose only recovery is a restart. ## 1. Out-of-memory is not a recoverable error Engine memory comes from paths that **terminate the process on exhaustion instead of throwing**: - `safe_alloc::make_refcounted` / `MakeRaw` / `MakeUnique` / `MakeRawArr` (`Source/Essentials/MemorySystem.h`): nothrow `new`, then drain a fixed backup pool and retry, then `ReportAndExit`. Every entity (`Item`/`Critter`/`Map`/`Location`/`CustomEntity`/…) is allocated this way. - `SafeAllocator` (`MemorySystem.h`) backs **every engine container alias** - `vector`, `small_vector`, `unordered_map`/`unordered_set`, `map`/`set`, `list`, `deque`, `string`/`stringstream` (`Source/Essentials/Containers.h`). Its `allocate()` is nothrow + backup-pool retry + `ReportAndExit`. So `emplace`, `emplace_back`, `insert`, `resize`, `reserve`, rehash, complex-property storage growth, and string growth on engine containers **cannot throw `std::bad_alloc` - they terminate at the allocation site.** - The private model-animation codec installs an engine-owned allocator from `ModelAnimationData.cpp` before any runtime or offline Ozz object is created. Its aligned byte blocks are backed by `SafeAllocator`, so Ozz uses the same rpmalloc, backup-pool retry, OOM reporting, and deterministic termination policy as engine containers. The vendored Ozz sources remain identical to the pinned upstream release; each linked host/runtime module installs its own adapter for its own statically linked Ozz state. - `safe_alloc::malloc_raw` / `CallocRaw` / `ReallocRaw` / `FreeRaw` and `MallocAlignedRaw` / `FreeAlignedRaw` (`MemorySystem.h`) apply the identical report → backup-pool retry → `ReportAndExit` sequence to untyped C-shaped allocation. This tier exists so third-party allocator hooks — which demand `realloc` or a raw byte block and therefore cannot be expressed as a C++ allocator — stay inside the contract instead of silently opting out of it. SDL, Effekseer, spine-cpp, libpng and curl are wired through it; the unpoliced rpmalloc primitives underneath are file-local to `MemorySystem.cpp` precisely so they cannot become a second entry point that returns null. - `move_only_function` / `copyable_function` (`Source/Essentials/FunctionObjects.h`) keep a small nothrow-movable target inline and send larger targets through nothrow allocation followed by `ReportFatalAndExit` on exhaustion. The module precedes `MemorySystem`, so it cannot use `SafeAlloc`, but storing an engine callable still terminates instead of surfacing `std::bad_alloc`. - `ModelMeshBaker` installs `SafeAllocator` callbacks into its private meshoptimizer dependency once before parallel mesh jobs start. Vertex-cache/fetch passes therefore use the same rpmalloc and fail-fast backup-pool policy for all temporary optimization blocks; the dependency remains baker-only and is not linked into runtime model readers. **Consequence — do not write rollbacks for allocation.** A code path whose only failure mode is an allocation cannot leave a half-mutated world: it either completes or the process dies deterministically at the failing allocation (which is the fail-fast outcome we want). Treat `std::bad_alloc` as "can't happen"; never add `scope_exit`/`scope_fail` undo logic justified solely by a possible container/string/refcount allocation between two mutations. (The throwing global `operator new` still exists for a bare `new T` / a `std::allocator`; engine code should not rely on it for OOM behavior - prefer `SafeAlloc`/engine containers so OOM terminates rather than propagates.) The allocator rule does not make every container operation no-throw. Element construction, conversion, comparison, and move/swap can still throw. This matters especially when replacing `vector` with `small_vector`: inline moves relocate elements, and move/swap are conditionally `noexcept` based on the element type. Re-derive the enclosing function's guarantee and audit surviving pointers/iterators; [Essentials.md](../../reference/native/essentials.md#vector-containers-and-inline-storage) owns the adoption rules and exact-type boundaries. **Where `std::bad_alloc` genuinely remains reachable.** "Can't happen" is a statement about the engine allocation vocabulary, not about every allocation in the process. Some standard types have no allocator parameter at all and therefore always allocate through the throwing global `operator new`: `std::future`/`std::promise`/`std::packaged_task` shared state, `std::thread`, `std::filesystem::path`, and the file streams. The sole retained `std::function` is the `StackTrace.h` script-provider hook above the engine callable module. Foreign ABIs may also own their containers — `nlohmann::json`, and the vendored libraries with no hook (LibreSSL, ogg/vorbis/theora). Separately, `BasicCore`, `StackTrace` and `BaseLogging` use `std::` containers by design, because they sit above `MemorySystem` in the `Essentials.h` include order and `ReportBadAlloc` calls into them — the OOM reporter must not depend on the allocator that just failed, which is why `SafeWriteStackTrace` wraps its resolution in `try`/`catch` with a raw-address fallback. None of this changes §1's rule for engine code; it means a `catch (const std::bad_alloc&)` is not automatically dead code at an interop boundary. An embedding project may enforce the full inventory with its own allocator audit, but project commands and allowlists are not normative Engine evidence. **The terminate-on-failure guarantee is memory allocation only — it does *not* extend to other resource acquisition.** Only heap allocation through `SafeAlloc` / the engine containers is "can't fail" (§1). OS-resource acquisition that can fail by exhaustion is a **genuinely recoverable throw** and must be handled, not assumed away. The canonical case is **thread creation**: constructing a `std::thread` can throw `std::system_error` when the OS is out of thread resources — that is *not* the allocation case, so `Threading.cpp`'s `spawn_pool_worker` is **not** `noexcept`, and `submit_impl` rolls back the just-queued task on that throw (otherwise the orphaned task would keep a dangling capture of an unwinding caller). So: only suppress a guard / avoid `ES: None` when the sole throw is a `SafeAlloc` memory allocation; a possible thread-creation, file-open, socket, or other OS-resource failure still needs correct handling. ## 2. What can actually be thrown, and where it is caught After §1, the exceptions that can propagate through a running server are: - `VerificationException` from `FO_VERIFY_AND_THROW(cond, …)` / the script `verify(…)` macro. - Engine exceptions: `EntitySyncException`, `DataBaseException`, `GenericException`, manager exceptions, etc. - Exceptions raised by native lifecycle code around callback dispatch, including post-event re-validation `FO_VERIFY_AND_THROW` checks. - `ScriptException` from `ScriptHelpers::CallInitScript` when an entity's `InitScript` cannot be resolved to a function with the expected signature. This is an engine-raised refusal, not a script-raised throw: it fails loudly rather than leaving the entity silently uninitialized. It reaches `CallInit` (and therefore entity creation and world load), while a *throwing* init script does not — `ScriptFunc::Call` is `noexcept`, reports the script's exception via `ReportExceptionAndContinue`, and returns `false`. The server runs all gameplay work as jobs on a `WorkerPool`. `WorkerPool::WorkerEntry` (`Source/Server/WorkerPool.cpp`) wraps each job body in `try { job() } catch (const std::exception&) { ReportExceptionAndContinue (log only) } catch (...) { FO_UNKNOWN_EXCEPTION -> terminate }`. So a `std::exception` from a job **does not crash the server** — it is logged, the job's `SyncContext` is released (locks dropped), and the worker proceeds. Whatever half-state existed at the throw point stays live in the world. This is exactly why the rules below matter. Scripted-event fan-out (`Game.OnX.Fire(...)`, `entity->OnX.Fire(...)`) is **noexcept**: `Entity::FireEvent` swallows per-callback exceptions and converts a throwing/`StopChain` callback into a chain stop. So `OnX.Fire` itself never propagates — but a handler's *side effects* (it may destroy or relocate entities) do persist, and the engine re-validates after firing. The client has the same recovery shape at frame granularity, plus a rendering obligation. `MainEntry` catches a `std::exception` from `ClientEngine::MainLoop`, reports it, and continues with the next frame only after `Application::EndFrame`, which requires no render target to remain bound. `ClientEngine::MainLoop` therefore guards the draw block with a `scope_fail` that calls `SpriteManager::AbortScene`: partial draws are dropped and the scissor/render-target stacks are unwound. Any new frame-scoped render-target binding owes the same cleanup. `AbortScene` is `noexcept` because it runs during unwind. Infallible manager state is reset directly, while backend release uses `safe_call` so a lost render context is reported without replacing the original exception. The backend operation itself remains throwing on the normal path. `EndScene` likewise stays an ordinary call rather than a `scope_success`: its invariant checks must throw while the `scope_fail` guard is still armed, not from an implicitly `noexcept` destructor. ### Nothing outside `std::exception` may be thrown Every exception raised by Engine or embedding-project native code must derive from `std::exception`. Throwing an integer, bare struct, or third-party non-standard exception bypasses the normal reporting frontiers and is forbidden. Accordingly, a general `catch (...)` beside `catch (const std::exception& ex)` is not a recoverable error path. It marks a broken invariant and must be: ```cpp catch (...) { FO_UNKNOWN_EXCEPTION(); } ``` Do not log and continue, fabricate an `"Unknown exception"` domain error, or rethrow it as an ordinary failure. The only narrow disposition exemptions are no-throw teardown/unwind boundaries and the logging/reporting machinery itself, where nothing may escape or recursively re-enter the reporter. They do not permit code to throw a non-`std::exception` value in the first place. ## 3. The entity-lifecycle contract (create / destroy) Entity lifecycle is deliberately **not** "transactional rollback". The contract, encoded by the tests in `Source/Tests/Test_EntityLifecycle.cpp` and `Source/Tests/Test_ServerMapOperations.cpp`, is: **Create (`CritterManager::CreateCritterOnMap`, `ItemManager::CreateItem`, `MapManager::CreateLocation`/`CreateMap`, `ServerEngine::CreateCritter`/`LoadCritter`):** - The entity is allocated and registered first, then placed, then its init script and entry events run. - A lifecycle event (or init script) may legitimately **destroy** the new entity, **relocate** it (e.g. an `OnMapCritterIn` handler attaches and transfers the critter to another map), or leave it where created. - The create function **throws as a signal** that the requested operation did not complete nominally — it does **not** roll the entity back. The entity is left in whatever valid state the events produced: - event destroyed it → the entity is gone and the registry is back to baseline (`ItemInitEventMayDestroyItem`, `CritterInitEventMayDestroyCritter`, `LocationInitEventMayDestroyLocation`); - event relocated it → the entity **survives** at its new location (`MapAddCritterEventMayMoveCritterAwayThrows`, `MapAddCritterInitEventMayMoveCritterAwayThrows` assert the critter is alive on the destination map after the throw); - load event destroyed a loaded critter → `LoadCritter` throws and the critter is gone (`CritterLoadEventMayDestroyLoadedCritterThrows`). Because "relocated and surviving" is indistinguishable from "leaked" at the throw site, **a blanket create-time rollback is incorrect** — it would destroy entities the design intends to keep. Do not add one. **Destroy (`DestroyCritter`/`DestroyItem`/`DestroyLocation`/`DestroyMap`/`DestroyCustomEntity`):** - `MarkAsDestroying()` latches first; a redundant call early-returns. - `IsDestroying()` / `IsDestroyed()` are cross-worker lifecycle latches backed by acquire/release atomics. This lets a deferred script context observe teardown without racing the player/entity job that publishes it; the entity lock still protects the mutable entity contents. - The finish event fires, then the environment tear-off runs inside a `for (size_t prev_deps = std::numeric_limits::max(); …) { try { … } catch (ReportExceptionAndContinue); /* progress guard */ }` loop so a misbehaving teardown step is retried until the entity is fully detached (or the inline progress guard, §3.1, terminates on non-convergence), and a finish-event handler **cannot take over or block** the destruction (`ItemFinishEventCannotTakeOverItemDestruction`, and the critter/location variants). - Snapshot iteration sets with `copy_hold_ref(...)` and re-check `IsDestroyed()` after each event before continuing to use a retained reference. ## 3.1 Destruction-loop convergence (why a teardown can't spin forever) Each `DestroyX` runs a teardown loop `for (size_t prev_deps = std::numeric_limits::max(); ;) { try { detach steps } catch { ReportExceptionAndContinue } /* inline progress guard, §3.1 */ }`, converging by emptying the holder's child collections / links. Eight such loops exist: `ItemManager::DestroyItem`, `CritterManager::DestroyCritter` + `DestroyInventory`, `MapManager::DestroyMapContent` + `DestroyMapInternal` + the `DestroyLocation` inner-entity loop, and `EntityManager::DestroyInnerEntities` + the `DestroyCustomEntity` inner-entity loop. A loop can fail to converge for three reasons: 1. **Re-entrant re-add (the realistic cause).** The teardown fires Finish events and recursively destroys children (more events) and transfers critters (more events). A script handler reacting to a child's destruction can *add a new child to the holder being destroyed* — e.g. an `OnItemFinish` handler that puts a fresh item back on the same critter/map/container, or an inner-entity-destroy handler that creates a new inner entity. If it re-adds on every child-destroy, the collection refills as fast as it drains. 2. **Persistent-throw detach step.** A detach step throws on every iteration (caught, logged, retried), so the collection never shrinks — a genuine bug in that step. 3. **No-progress logic bug.** A detach step succeeds but does not reduce the loop condition. ### Exit mechanism **Primary — block re-add during destruction (eliminates cause 1), echeloned across tiers (§5).** A re-entrant re-add originates from a **script** handler (Finish/destroy events run scripts), so the same "you may not add a child to a destroying entity" rule is enforced at two depths — caught as early as possible, and re-checked deeper as a backstop: - **Top — `throw` (expected script misuse):** every `FO_SCRIPT_API` add-method (`Server_Map_AddItem` / `Server_Map_AddCritter`, `Server_Critter_AddItem` / `Server_Critter_AttachToCritter`, `Server_Item_AddItem`, `Server_Location_AddMap`) rejects an add to an `IsDestroying` entity with a `ScriptException`. - **Below — `FO_VERIFY_AND_THROW` (invariant re-check):** the internal mutation methods (`Entity::AddInnerEntity`, `CritterManager::AddItemToCritter`, `Map::AddCritter` / `Map::SetItem`, `Item::SetItemToContainer`, `Critter::AttachToCritter`, `Location::AddMap`) re-check the same `IsDestroying` / `IsDestroyed` invariant in case a path slipped past the top guard. With these, a destroying holder cannot gain new children, so the teardown loop is monotone. Note: only *adding children* is forbidden — **modifying** a destroying entity (writing its properties) stays allowed, because a Finish/destroy handler legitimately mutates the entity it is cleaning up (and the engine passes the destroying entity into its own handlers). **Reads on a destroying map are allowed under the lock (owner directive — "option B"; supersedes the earlier read-side strictness for queries).** An earlier version of this doc claimed the map's hex-field grids (`_hexField` / `_staticMap->HexField`) are torn down *before* the destructor, making a spatial read on an `IsDestroying` map a use-after-free — **that was wrong.** `_hexField` is a `unique_ptr` member destroyed only by `~Map` (there is no pre-destructor reset anywhere in teardown), and the shared static `HexField` outlives the instance; so throughout `IsDestroying` the grid *structure* is alive and only its *contents* drain — a hex-keyed read mid-drain returns empty cells, not freed memory. The real protection against the concurrency hazard (the sibling-parallel grid race) is the **lock**: a grid-*mutating* op holds the map exclusively and serializes against readers — not an `IsDestroying` gate, which never addressed concurrency and only ever returned a degraded value or threw at a legitimate under-lock reader (e.g. a deferred map-analysis job hitting `Cannot query a map that is being destroyed`). So map read methods take **`LOCKED, NOT_DESTROYED`** and do **not** gate on `IsDestroying`: script `Server_Map_Get*` / `Server_Item_GetItems`, and engine `Map::Get*` / `Is*` / `Has*` / `Check*` (the 14 read macros dropped `NOT_DESTROYING`; the 8 `noexcept` hand-written `FO_VERIFY_AND_RETURN_VALUE(!IsDestroying(), …)` read-gates were removed). A consumer that does not want to act on a dying map checks `IsDestroying` / a failed `Sync::Lock` *itself* — that is the consumer's call, not a hard engine gate. The single unified rule: **grow a dying entity → forbidden (`NOT_DESTROYING`, expansion only); read → allowed under the lock (`LOCKED, NOT_DESTROYED`); fully dead → never (`NOT_DESTROYED`); drain → *is* the destruction (allowed).** The `IsDestroyed` (`FO_STRONG_ASSERT`, corrupt) vs `IsDestroying` (`FO_VERIFY_AND_THROW`, recoverable) tiers stay distinct for the methods that still gate (the expansion `Add*` / `SetItem` add-guards). **Declaring method preconditions: `FO_VALIDATE_ENTITY()`.** *(Temporary scaffolding — owner directive 2026-07-11: the macro exists to debug the lock system and is slated for removal once it is validated; per-method ES tags classify bodies as if it were absent, see §8.)* Every server entity method opens by declaring what it requires, via the macro in `Common.h`: `LOCKED` (the caller's sync context covers `this`; the noexcept report-and-exit form), `NOT_DESTROYED` (`FO_STRONG_ASSERT(!IsDestroyed())` — the corrupt-state tier above), `NOT_DESTROYING` (`FO_VERIFY_AND_THROW(!IsDestroying())`; it *throws*, so a `noexcept` method instead takes `LOCKED[, NOT_DESTROYED]` and hand-writes a `FO_VERIFY_AND_RETURN_VALUE(!IsDestroying(), …)`), or `NONE`. This makes "may this be called now?" explicit and greppable per method, and replaces the older `FO_VALIDATE_ENTITY_ACCESS()` / `FO_NO_VALIDATE_ENTITY_ACCESS()` markers. **The `noexcept`⇒no-`NOT_DESTROYING` rule is compiler-caught at `/W4`:** `FO_VERIFY_AND_THROW` expands to a **direct inline `throw`** (not a wrapped function call — see `Source/Essentials/ExceptionHandling.h`: `if (!(expr)) [[unlikely]] { throw VerificationException(...); }`), so MSVC **does** emit `C4297` ("function assumed not to throw an exception but does") for a `NOT_DESTROYING` — i.e. any reachable `FO_VERIFY_AND_THROW` / `verify(…)` / bare `throw` — in a `noexcept` body (verified in RelWithDebInfo). The engine builds at `/W4` **without** `/WX`, so C4297 is a *warning*, not a hard error — but under the zero-tolerance-warnings policy it is still a defect to fix, and it is a real net that catches a `noexcept` method carrying `NOT_DESTROYING`. Review stays the primary guard (a `noexcept` signature must not carry `NOT_DESTROYING`), because a throw fully enclosed in the function's own `try`/`catch` does not escape and so would not trip C4297. **Strictness policy (owner directive 2026-06-29, amended by the option-B read rule above): make every method's `FO_VALIDATE_ENTITY` as strict as it can bear — when in doubt, forbid and relax later.** The default for a non-`noexcept` **mutation** is the maximal `LOCKED, NOT_DESTROYED, NOT_DESTROYING`. A **read/query** instead defaults to `LOCKED, NOT_DESTROYED` only: a read does not grow the entity and the grid/collections it touches are valid throughout the drain, so `NOT_DESTROYING` on a read buys no safety and only rejects a legitimate under-lock reader (option B). `NOT_DESTROYED` stays broadly on both (it tests `IsDestroyed`, which is *false* throughout the drain, so it never breaks teardown while still catching a stale/freed pointer). The hard part is the **exclusions** — *mutation* methods the strict flags would break because a legitimate teardown/destroy path reaches them. Static analysis under-finds these; the **gameplay-test sweep + engine unit tests are the authoritative net** — every exclusion below built clean and was caught only by the behavioral gate. The exclusion classes: 1. **`noexcept` ⇒ never `NOT_DESTROYING`** (it throws; `C4297` *does* fire as a `/W4` warning — see above). A `noexcept` method that needs the guard hand-writes `FO_VERIFY_AND_RETURN_VALUE(!IsDestroying(), …)`. 2. **Drain-reached accessors ⇒ no `NOT_DESTROYING`.** Reading an owned collection (inventory, inner items/entities, visibility, `Location` maps) during `IsDestroying` is safe (valid until the destructor) and the destruction drain loops *call exactly those accessors* (`HasItems`, `HasInnerItems`, `GetInvItems`, `GetRawInnerItems`, … — the loop conditions in the convergence backstop below) to empty the entity; guarding `Item::HasInnerItems` with `IsDestroying` makes it report "nothing left" mid-drain and the loop abandons an un-drained entity. Also here: a property push fired *during* the drain (`Critter::Broadcast_Property`). Same at the **script frontier** — the `Server_Location_Get*` map readers must not reject `IsDestroying`, because `OnLocationFinish` reads the location's still-alive maps to detach guard data. 3. **Destroy/transfer-cascade-reached ⇒ no `NOT_DESTROYING`.** `DestroyCritter` → `MapManager::Transfer`/`TransferToGlobal` unhooks a **destroying** critter onto the global map, and the path's guards check `IsDestroyed` *only* — so an `IsDestroying` critter flows through and the cascade calls `StopMoving`, `GetGlobalMapGroup`, `MoveAttachedCritters`, `ChangeDir` on it. These must tolerate `IsDestroying`: the cascade *is* the destroy, so an `IsDestroying` guard there would abort teardown, not protect it. 4. **Event-destroy re-validation ⇒ no `NOT_DESTROYED` either.** A few methods run on an *already-`IsDestroyed`* (but still ref-held) self after an event destroyed it: a `scope_exit{ cr->UnlockMapTransfers() }` that must rebalance the counter the destructor asserts is zero; and the post-event re-validations whose first body statement is `if (IsDestroyed()) return X` (`Map::IsMapItemContextChanged`, `Critter::CanSeeItemOnMap`). For these even `NOT_DESTROYED` is wrong (it asserts before the graceful return) — they take `LOCKED` only, and the `if (IsDestroyed())` early-out *is* the contract. The **genuine positive ground** for `NOT_DESTROYING` is an expansion/add-to-holder method (`AddItem`, `AddCritter`, `AddMap`, `SetItemToContainer`, …), enforcing the "can't add a child to a dying holder" invariant (§3). Reads and queries do not take `NOT_DESTROYING`: the map grid and owned collections stay alive throughout the drain, and their concurrency safety comes from the lock. Everything else is strict-by-default minus the four exclusion classes. **Backstop — terminate on genuine non-convergence (causes 2 and 3).** Each teardown loop carries a local `size_t prev_deps` (seeded to `std::numeric_limits::max()`) and, at the end of every pass, recomputes the entity's *current* remaining-dependency count (e.g. `cr->GetInvItems().size() + cr->GetInnerEntitiesCount() + …`) and asserts a **strict monotonic decrease**: ```cpp for (size_t prev_deps = std::numeric_limits::max(); cr->HasItems() || cr->HasInnerEntities() || …;) { try { /* tear off one layer */ } catch (const std::exception& ex) { ReportExceptionAndContinue(ex); } const size_t remaining_deps = cr->GetInvItems().size() + cr->GetInnerEntitiesCount() + …; FO_STRONG_ASSERT(remaining_deps < prev_deps, "Critter destruction made no progress", cr->GetId(), remaining_deps, prev_deps); prev_deps = remaining_deps; } ``` If a pass does not reduce the count (the loop stalled, or a re-add grew it), `FO_STRONG_ASSERT` terminates. The loop's boolean condition is exactly equivalent to `remaining_deps != 0`, so a real drain ends the loop *before* the next check — a slow-but-progressing teardown is never falsely killed; only a stall is. This is written inline at each destruction loop (no shared helper class) so the dependency count is defined right next to the loop it guards. The earlier `throw`-on-overflow escaped to the WorkerPool catch and left a half-destroyed, un-finalized "undead" entity in the registry until restart; terminating surfaces the underlying bug deterministically. (Every such progress-guarded loop is a destruction loop, where non-progress is always corruption.) The reaction is `FO_STRONG_ASSERT` (terminate), not stop-and-throw (`SetDestroying(false)` + throw): by the time the loop spins, the entity's `Finish` event has already fired, so a "recovered" entity would be alive-but-finalized — a worse corruption than a deterministic crash on the real bug. Net guarantee: a destruction either completes, or the process terminates deterministically on a real bug — it never silently leaves an undead entity. ## 4. Post-mutation invariants → `FO_STRONG_ASSERT` This is the **unexpected-and-unhandled** tier (§5). A verify placed **after** an irreversible mutation that checks an invariant which "can only be false if the world is already corrupt" is `FO_STRONG_ASSERT` (deterministic `ReportExceptionAndExit`), **not** a throwable verify: there is nothing left to handle, and a throwable verify would just be caught by the WorkerPool and keep running on corrupt state. (The same invariant may still be re-checked *earlier*, where it is recoverable, as a `throw` or `FO_VERIFY_AND_THROW` — that is the echeloned model in §5, not a contradiction.) Such invariants must abort in **every** build. `FO_STRONG_ASSERT` (`Source/Essentials/ExceptionHandling.h`) is **unconditional** — it always evaluates its condition and calls `ReportExceptionAndExit` on failure, regardless of build profile — so it is the correct tool for load-bearing post-mutation invariants. The condition of every `FO_VERIFY_*` and `FO_STRONG_ASSERT` variant is a predicate, never the operation being checked. Perform a write, flush, read, insertion, filesystem mutation, or out-parameter call first, bind its result to a named local, and check that local. For example, write `bool written = file.write(data) && file.flush(); FO_VERIFY_AND_THROW(written, "Resource footer was not committed");`, not the write and flush inside `FO_VERIFY_AND_THROW(...)`. The macros do evaluate their conditions in all builds; this rule keeps an action from being hidden inside what reads as an assertion. Examples converted to `FO_STRONG_ASSERT` (all post-mutation, can't-happen-with-correct-code): - `EntityManager` typed-registry duplicate checks and `Unregister*`/global lookup checks (forward registration implies the typed/global maps agree). - `Critter` visibility-graph symmetry (`AddVisibleCritter` reverse insert, `RemoveVisibleCritter`/`ClearVisibleEnitites` reverse lookups: a forward link implies its reverse). - `EntitySync` post-grant lock invariants (`EntityLock::Acquire`: a non-aborted waiter must wake `GRANTED` and own the lock). - `AngelScriptContextManager::ResumeSpecificContext` pre-resume invariants (a suspended context must have its extended data / scheduler). The legitimate **non**-invariant throws are left as `FO_VERIFY_AND_THROW`: e.g. `EntityManager::RegisterEntity`'s global-registry insert (a duplicate id from a corrupt persisted record is handled gracefully on the load path via `is_error`), and `EntityLock::Acquire`'s `EntityLockWaitAbortedException` (the legitimate shutdown-abort path). ## 5. Error tiers & choosing the disposition Three tiers, classified by whether an error is *expected* and whether it is *handled*. **All three stay active in every build, including release** — we accept that our own bugs can surface only under real, loaded production conditions, beyond what development and testing catch, so we never compile out the lower tiers. | Tier | When | Mechanism | |------|------|-----------| | **Expected error** | A condition we anticipate and design around — bad script arguments, malformed/untrusted input, a target legitimately full/absent/forbidden. *Not* an invariant violation. | **`throw`** a domain exception (`ScriptException`, `DataBaseException`, …). Caught upstream and turned into a normal failure result; no side effect when it fires before any mutation. | | **Unexpected but handled** | An **invariant violation** — "can't happen if the code is correct" — that we nonetheless re-check and catch rather than trust blindly. **Assert semantics, not expectation.** | The **`FO_VERIFY_*` family** (`FO_VERIFY_AND_THROW`, `FO_VERIFY_AND_CONTINUE`, `FO_VERIFY_AND_RETURN`, `FO_VERIFY_AND_RETURN_VALUE`). All **report** the violation (log + stack trace) and are **never compiled out**; the variant only chooses the post-report control flow — see "Picking the variant" below. | | **Unexpected and unhandled** | An invariant violation reached where it is **too late to recover** — continuing would run on already-corrupt state and no caller can sensibly handle it. | **`FO_STRONG_ASSERT`** — deterministic `ReportExceptionAndExit` (terminate). Also always-on. | **Picking the `FO_VERIFY_*` variant.** The tier is the same for all of them — an invariant violation, always reported — only the *continuation after the report* differs, and it is dictated by the **control-flow context**, not preference. In every case you get the report; what matters is how execution proceeds: - **`FO_VERIFY_AND_THROW`** — throws `VerificationException`. Use **only where an exception may legally propagate** and be caught by some upper layer (a normal exception-permitting flow: a WorkerPool job, a script-call boundary, a destroy loop's `try`). The current unit of work unwinds. - **`FO_VERIFY_AND_CONTINUE`** — reports and **keeps executing** at the same point. Use in a **`noexcept` context** (a destructor, a `scope_exit`/`scope_fail` body, a `noexcept` callback, any function on a `noexcept` path — throwing there would `std::terminate`), or in a loop where you want to skip a bad element and keep iterating. - **`FO_VERIFY_AND_RETURN`** — reports and **returns from a `void` function**. Use in a `noexcept`/no-throw `void` function that must bail out cleanly. - **`FO_VERIFY_AND_RETURN_VALUE(expr, fallback, …)`** — reports and **returns a safe fallback**. Use in a `noexcept`/no-throw value-returning function (e.g. a getter that must yield *something* defined). Rule of thumb: if you are (or might be) inside a `noexcept` region you **cannot** use `FO_VERIFY_AND_THROW` — pick `CONTINUE` / `RETURN` / `RETURN_VALUE` so the report is emitted *and* the caller is left in a defined state. (`FO_STRONG_ASSERT` is also valid from a `noexcept` region, when the right response is to terminate rather than continue.) **Echeloned (defense-in-depth) detection.** The *same* underlying error may be caught by more than one tier, at different depths — and that is **intentional**: the path from a high-level entry point to the low-level mutation varies, so we catch it as early as possible *without* removing the deeper backstops. Catching an error earlier is always better than later, and there can never be "too much" error protection — so the tiers are **additive, not either/or**. Canonical example, "a script adds a child to an entity that is being destroyed": 1. **`throw` at the top** — the `FO_SCRIPT_API` add-method rejects with `ScriptException` (expected script misuse, caught early). 2. **`FO_VERIFY_AND_THROW` below** — the internal mutation method (`CritterManager::AddItemToCritter`, `Map::AddCritter`, `Entity::AddInnerEntity`, …) re-checks the same `IsDestroying` invariant, in case a call slipped past the top guard. 3. **`FO_STRONG_ASSERT` at the bottom** — if a re-add nonetheless stalled a teardown loop (a pass that removes no dependency), the loop's inline strict-monotonic-decrease check (§3.1) terminates (too late to undo). Per-situation guidance: - **Expected runtime failure** (bad args/input, target full, forbidden call) → **`throw`** the domain exception (e.g. `DbStorage.Insert`'s empty-document guard, the `NetOutBuffer` outgoing-message verifies, the `FO_SCRIPT_API` argument and `IsDestroying` checks). Never `FO_STRONG_ASSERT` — it has no side effect yet and crashing all players for a recoverable mistake (or one connection) is wrong. - **Validate top-level (script / RPC / client-writable-property) arguments at the boundary, before the value can reach a deep `numeric_cast` or a low-level `FO_VERIFY_*`.** A `FO_SCRIPT_API` method's args and a `///@ RemoteCall` payload are *script/client-controlled* (a tampered client can send anything an `int`/`ipos`/`mpos` can hold). Range-check them in the export method and `throw ScriptException` (range, bounds, non-negative) so the failure surfaces at the right altitude with a clear message — rather than a deep `numeric_cast` throwing an opaque `OverflowException`/`UnderflowException`, or a `std::string::resize`/container op throwing `length_error`, far from the call site. The deep cast/verify stays as the echeloned backstop. (Audited 2026-06-16: no script-reachable `FO_STRONG_ASSERT` exists — the engine never *terminates* on bad script input — but several args reached deep wrong-tier throws; those boundary guards were added. The property get/set boundary was confirmed fully guarded.) - **Invariant re-check you can still reject** → **`FO_VERIFY_AND_THROW`** (the internal add-to-destroying guards; any "this should already hold" check *before* a mutation that a caller can abort). - **Invariant reached too late to recover** → **`FO_STRONG_ASSERT`** (post-mutation registry/graph/lock corruption; non-convergent teardown). - **Throw-as-signal (no rollback)** — the operation may legitimately not complete because an event/script changed the world; throw to inform the caller and leave the entity in the valid state the events produced (the lifecycle contract, §3). - **Commit-or-rollback (`scope_fail`/`scope_exit`)** — only when a step that genuinely can fail at runtime would otherwise leave **two representations out of sync** with no design intent to keep the partial state, and there is no allocation-only excuse (§1). The rollback body must be `noexcept` (`safe_call`). Confirm with a failing test first. - **Reorder (validate-first, mutate-last)** — do all fallible/validating work before the first irreversible mutation; insert into the authoritative store (with its duplicate guard) before populating any derived cache. (Example: `ProtoManager::AddProto`.) Persistence note: DB writes (`DbStorage.Insert/Update/Delete`) are **enqueue-only** and never perform synchronous backend I/O; a backend failure is handled asynchronously (recovery op-log + reconnect + panic-shutdown with replay on restart). So a DB write cannot throw a backend error mid-mutation — no write-through rollback is needed. See `Source/Server/DataBase.cpp`. ## 6. Primitives - `scope_exit` / `scope_fail` / `scope_success` (`Source/Essentials/BasicCore.h`): RAII guards. `scope_fail` runs only on stack unwinding (an in-flight exception) and static-asserts its callback is `noexcept`. - `safe_call(callable, …)` (`Source/Essentials/CommonHelpers.h`): invoke a callable, swallowing any exception — use to make a rollback/teardown body `noexcept`. - `copy_hold_ref(container)`: snapshot a server-entity collection by ref-count so elements survive a loop that fires re-entrant events. - Inline teardown-progress guard (no shared class): seed `size_t prev_deps = std::numeric_limits::max()`, and at the end of each destruction-loop pass `FO_STRONG_ASSERT(remaining_deps < prev_deps, …)` then `prev_deps = remaining_deps`. Detects non-convergence by *progress*, not by an iteration cap, so a slow-but-progressing teardown is never falsely killed (§3.1). ## 7. Tests The contracts above are pinned by `Source/Tests/Test_EntityLifecycle.cpp` (init/finish-event destruction, load/unload events, registry collections) and `Source/Tests/Test_ServerMapOperations.cpp` (the full map critter in/out/init × may-destroy / may-move-away / may-destroy-map matrix). When changing any lifecycle or invariant behavior, run the embedding project's generated engine unit-test target and extend these suites rather than weakening an assertion. ## 8. Per-function exception-safety classification (ES levels) For audit purposes, each function/method definition in `Source/**/*.cpp` (excluding `Source/Tests/` and the codegen `*.template.cpp` inputs) can be assigned an exception-safety level stating what its body **currently guarantees** - documentation of fact, not aspiration. | Level | Guarantee | |-------|-----------| | `NoThrow` | No exception propagates to the caller: `noexcept` signature, a catch-all that swallows, or a body that provably cannot throw. Remember the engine model: `SafeAlloc`/engine-container allocation **terminates** on OOM (§1), `FO_STRONG_ASSERT`/`ReportAndExit` **terminate**, event `Fire()` is noexcept, and `FO_VERIFY_AND_CONTINUE`/`RETURN`/`RETURN_VALUE` report without throwing — none of those demote a method from NoThrow. | | `Strong` | May throw, but on throw the observable engine state is exactly as before the call — validate-first bodies (all throws precede the first observable mutation), read-only queries, or genuinely rolled-back mutations (`scope_fail`). | | `Basic` | May throw after an observable mutation, but every invariant holds and every touched entity/object stays valid and usable. The §3 entity-lifecycle throw-as-signal contract is *by design* this level: the throw reports "did not complete nominally" while the world remains valid. | | `None ()` | A throw can escape with a broken invariant / half-mutated state (two representations out of sync, a partially rebuilt structure, an orphaned link). Always carries a short reason naming the broken invariant. Per the §5 policy this is a **defect candidate to triage**, not an accepted level. | The engine repository defines the classification vocabulary and derivation rules, but does not currently ship a canonical complete per-function baseline or analyzer. An embedding project may maintain its own baseline with level, verification state, and a body hash, but that artifact is project-owned and cannot serve as normative proof for engine documentation. This section is the engine-side contract an auditor - human or agent - uses to **derive** a level. When a change alters what a body guarantees, re-derive its level and update any consuming project's baseline in the same integration change. Derivation, in order: a `noexcept` definition, a catch-all-swallowing body, or a body with no real throw points is `NoThrow`; when throw points exist but all fire before the first observable mutation (or the mutations are rolled back), it is `Strong`; when a throw can escape after a mutation with every invariant intact — including the §3 throw-as-signal lifecycle paths — it is `Basic`; only when a concrete broken invariant can be named is it `None`. Judge each function by its own body given its callees' behavior: a throwing callee is a throw point at the call site. Lambdas, `= default`/`= delete`, declarations, header-inline bodies, and test code are not classified. **`FO_VALIDATE_ENTITY(...)` is ignored for ES classification (owner directive 2026-07-11).** The precondition macro (§3.1) is *temporary* lock-system-debugging scaffolding, slated for removal once the lock system is validated. ES levels document the method's *permanent* body semantics, so classify **as if the macro were absent**: its `LOCKED` throw (`ValidateEntityAccess` → `ScriptException`) and its `NOT_DESTROYING` `FO_VERIFY_AND_THROW` are **not** counted as throw points, and its `NOT_DESTROYED` `FO_STRONG_ASSERT` terminates (never a throw point anyway). A read-only method whose only potential throw comes from `FO_VALIDATE_ENTITY` is therefore `NoThrow`. Hand-written guards in the body (`FO_VERIFY_AND_THROW(!IsDestroying())`, `vec_add_unique_value`'s duplicate verify, explicit `throw`) are permanent code and always count. **`noexcept` is a semantic contract, never a record of the current classification (owner decision 2026-07-12).** A `NoThrow` ES level states a *fact about the current body* and can be re-derived when the body changes; a project baseline may enforce that with a body-hash gate. The `noexcept` keyword states an *interface promise* with a terminate sanction, and it ossifies: a later, perfectly legitimate validating throw (say, an argument `FO_VERIFY_AND_THROW` added to a mutation) would have to fight the specifier, and a reader cannot tell a deliberate promise from a snapshot of "didn't throw on the day of analysis". So: **never add `noexcept` merely because a function is classified `NoThrow`**. Write `noexcept` only where it expresses a contract someone relies on: - **move operations and swap** (container/`std::move_if_noexcept` semantics), - **teardown/unwind-path callables** — `scope_exit`/`scope_fail` bodies (statically enforced), `safe_call` targets, rollback commit-tails a `Strong` function depends on, - **C/OS-ABI callbacks** whose contract is no-unwind across the boundary, - **Essentials primitives documented as no-throw** (e.g. the terminate-on-OOM allocation layer, §1). Removing an incidental `noexcept` from an ordinary method when it gains a legitimate throwing check is normal evolution, not a contract break; reclassify the function in any consuming project baseline in the same integration change. A large mechanical `NoThrow`-to-`noexcept` annotation sweep was deliberately reverted for exactly this reason (2026-07-12). For the cases where `noexcept` *is* warranted, three engine-specific hazards still apply: 1. **Inline throw ⇒ `C4297`.** A body containing a reachable inlined throw — `FO_VALIDATE_ENTITY(… NOT_DESTROYING)`, a hand-written `FO_VERIFY_AND_THROW` / `verify(…)`, a bare `throw` — trips `warning C4297` under `/W4` when declared `noexcept` (§3.1); a `noexcept` function that needs such a guard uses `FO_VERIFY_AND_RETURN*` instead (§5). 2. **AngelScript-bound functions.** Since C++17 `noexcept` is part of the function *type*; `RegisterGlobalFunction` / `RegisterObjectMethod` calling-convention detection rejects a `noexcept` pointer at **registration time** with `asWRONG_CALLING_CONV` — a runtime `ScriptSystemException`, not a compile error. The `Source/Scripting/AngelScript/*.cpp` binding layer takes no `noexcept` at all (codegen `FO_SCRIPT_API` exports already don't). 3. **Dual-implementation classes.** A class declared once with two build-selected implementations (e.g. `Application` in `Application.cpp` vs `ApplicationHeadless.cpp`) needs the specifier on the declaration and on **every** implementation together, or the build fails with `C2382: different exception specifications`. ===== END DOCUMENT exception-safety ===== ===== BEGIN DOCUMENT local-variables ===== Source: Docs/en/contributing/coding-contracts/local-variables.md Canonical URL: https://fonline.ru/Docs/en/contributing/coding-contracts/local-variables.html Content SHA-256: 9f1a122dc5f4c825be8e7fa3e4619403edb0de33977834e9925209add1cdd88b --- layout: default title: Local Variables locale: en document_id: local-variables permalink: /Docs/en/contributing/coding-contracts/local-variables.html --- # Local Variables This document owns the narrow first-party C++ rules for local type spelling, redundant top-level `const`, and use-after-move diagnostics. There is no engine-specific immutability-by-default rule. Local variables and function or method parameters follow normal C++ mutation semantics and require no annotation when they are written. ## Redundant local `const` Do not add top-level `const` to an automatic local when removing it leaves the type contract unchanged: ```cpp int32_t count = GetCount(); item_ptr item = FindItem(); ``` Preserve constness that is part of the value's actual type or semantics: ```cpp const Item* item = FindItem(); // const pointee const Item& item = GetItem(); // const referent const int32_t values[] = {1, 2}; // const elements constexpr int32_t limit = 10; // required by constexpr ``` `int32_t* const pointer` has removable top-level const and is diagnosed. Parameters are not part of this check. The rare intentional top-level local qualifier can be suppressed on the diagnostic line or the preceding line: ```cpp const int32_t value = SelectOverload(); // FO_REDUNDANT_CONST_SUPPRESS: const is required for overload selection ``` The reason is mandatory and must contain at least eight characters. ## Explicit simple local types Replace local `auto` only when all of these are true: - Clang resolves one accessible, unqualified `snake_case` type name. - The type has no visible template arguments. - The type is not nested or namespace-qualified. - Writing the explicit type does not introduce or hide a conversion. - The declaration is not a structured binding, lambda, or dependent deduction. When Clang exposes only a canonical template type, an unqualified alias is eligible if that alias is explicitly spelled in the initializer, is the only simple alias for the exact same desugared type, and is accessible at the declaration. This covers calls such as `glm::translate(mat44 {...}, offset)` without admitting library-internal names such as `iterator` or `value_type`. Examples: ```cpp int32_t count = GetCount(); float32_t factor = GetFactor(); hstring name = GetName(); ``` Keep `auto` for cases such as: ```cpp auto values = vector {}; auto iter = values.begin(); // nested iterator type auto handle = MakeHandle(); // template spelling auto pointer = GetRawPointer(); // explicit target may convert auto [left, right] = Split(value); // structured binding ``` When an eligible deduced numeric primitive is written explicitly, use the engine's sized spelling: | Deduced type | Explicit spelling | |---|---| | `short` | `int16_t` | | `int` | `int32_t` | | `float` | `float32_t` | | `double` | `float64_t` | Existing fixed-width signed and unsigned types keep their spelling. See [Enforcement](#enforcement) for how this rule is checked. ## Use after move Moving from a local does not make it mutable and needs no annotation. The independent `bugprone-use-after-move` gate rejects subsequent use until a valid reinitialization: ```cpp value_type value = BuildValue(); Consume(std::move(value)); ``` Intentional inspection of a moved-from object must state the contract: ```cpp CHECK(value.empty()); // FO_USE_AFTER_MOVE_SUPPRESS: test verifies the moved-from container contract ``` The reason is mandatory and must contain at least eight characters. ## Enforcement These rules are intended for a Clang 20+ `compile_commands.json` analyzer: an explicit-type checker plus `clang-query` / `clang-tidy` checks for redundant local constness and use after move. The engine owns only the rules above and the `FO_REDUNDANT_CONST_SUPPRESS` / `FO_USE_AFTER_MOVE_SUPPRESS` markers. An embedding project owns analyzer implementation, compile-database generation, scope selection, and CI policy. Run all three checks over changed engine sources before publication. A project may extend the same gate to its native extensions, but project paths, task names, and workflow jobs are not part of this reusable contract. Both `clang-query` and `clang-tidy` must be present on the host running the gate, and the compile database must support a real parse. Generated headers reachable from the engine's common include chain must exist before analysis; otherwise translation units fail on missing includes before any rule is evaluated. Prune the compilation database to one command per translation unit before passing it to Clang tooling. CMake emits one entry for every target that compiles a file; an Engine source may therefore appear dozens of times. Clang runs its action once per entry, and `clang-query` retains every built AST, so duplicates multiply both wall time and memory. One Engine AST is roughly half a gigabyte; substantially larger usage usually indicates an unpruned database rather than an inherent analyzer cost. ===== END DOCUMENT local-variables ===== ===== BEGIN DOCUMENT nullability ===== Source: Docs/en/contributing/coding-contracts/nullability.md Canonical URL: https://fonline.ru/Docs/en/contributing/coding-contracts/nullability.html Content SHA-256: ba79255cf2cce8d31b49bf02997ea78d9acc950f4ea846fe2647afb845e6686a --- layout: default title: Nullability locale: en document_id: nullability permalink: /Docs/en/contributing/coding-contracts/nullability.html --- # Nullability > Engine-owned documentation. This page defines the reusable compiler, runtime, and native-boundary contract. Project-side analyzers may enforce stricter authoring policy, but they are not part of the engine contract. Convention and runtime enforcement for nullable values across AngelScript, Managed C#, and the native engine boundary. For the broader scripting runtime, see [Scripting](../../explanation/scripting-runtime/); for exported native method ownership, see [Script Methods Map](../../reference/script-api/method-ownership.md). ## Core principle > Better to not pass `null` at all than to defensively check inside and bail out. > > A parameter or return may be marked nullable **only when the function meaningfully handles both null and non-null cases**. Early-exit-on-null guards are a code smell — the contract should be non-null and the caller fixed instead. This applies symmetrically on both sides of the script-engine boundary. ## Managed C# side Generated Managed sources enable nullable reference types and map the same metadata nullable bit to C# `?` on entity, string, and ref-type references. Value types never carry that bit. A native `ptr` becomes a non-null C# reference contract; `nptr`, a nullable property flag, or a nullable event/remote-call tag becomes `T?`. Generated methods, properties, events, delegates, and remote-call caller methods preserve the spelling so Roslyn can diagnose an unchecked dereference or an incompatible assignment at compile time. C# annotations are not a runtime ownership guarantee. A managed entity wrapper remains borrowed, may refer to an entity destroyed after an `await`, and must be re-resolved/revalidated together with server cover. Narrow an expected absence with an ordinary `is null` / `is not null` branch. For an invariant, use `Game.VerifyNotNull(value, message)` or `Game.Verify(...)` and keep the narrowed non-null value; do not suppress a warning with `!` unless an external API has made the proof invisible to the compiler. Metadata declarations remain authoritative across backends. A nullable `///@ Event` or `///@ RemoteCall` argument generates a nullable C# parameter, and the attributed `[Event]`, `[ServerRemoteCall]`, or `[ClientRemoteCall]` handler must preserve the same semantic contract. Managed build validation consists of the generated project compile with nullable diagnostics enabled, configured Roslyn analyzers, and a runtime callback/serialization test where null is legitimate. See [Managed C# Scripting](../../how-to/scripting/managed-csharp.md) for generation, async lifetime, analyzers, and packaging. ## AngelScript side: `T?` suffix AngelScript modules use a Kotlin/C#-style `?` suffix on the type to mark nullability. Default is **non-nullable**. ```angelscript // Return may be null Location? GetCritterLocation(Critter cr) { if (cr.MapId == ZERO_IDENT) { return null; } Map map = cr.GetMap(); return map != null ? map.GetLocation() : null; } // Parameter may be null — body handles both cases void ResolveTargetHex(Critter cr, Critter? target, mpos fallbackHex) { mpos resolvedTargetHex = target != null ? target.Hex : fallbackHex; // ... } ``` `?` is parsed by the **AngelScript front-end itself** (see `ParseType` in [../ThirdParty/AngelScript/sdk/angelscript/source/as_parser.cpp](../../../../ThirdParty/AngelScript/sdk/angelscript/source/as_parser.cpp), `MakeNullable`/`isNullable` in [../ThirdParty/AngelScript/sdk/angelscript/source/as_datatype.h](../../../../ThirdParty/AngelScript/sdk/angelscript/source/as_datatype.h), and the `CreateDataTypeFromNode` consumer in [../ThirdParty/AngelScript/sdk/angelscript/source/as_builder.cpp](../../../../ThirdParty/AngelScript/sdk/angelscript/source/as_builder.cpp)). The marker is no longer rewritten by the preprocessor — the `StripNullableTypeSuffix` pass was removed once the engine learned the suffix directly. Misplaced markers (e.g. `int?`) produce a compile-time error: *"Nullable marker '?' is only allowed on handle types"*. The AS parser disambiguates the type-suffix `?` from the ternary `?` by only consuming it inside `ParseType`. Inside expressions, `cond ? a : b` continues to parse as the conditional operator. ### `///@ Event` and `///@ RemoteCall` declarations The same `?` suffix is supported in `///@ Event` and `///@ RemoteCall` tag declarations, and the [`MetadataBaker`](../../../../Source/Tools/MetadataBaker.cpp) propagates the per-arg nullable bit into the baked engine metadata (`ArgDesc::Nullable` on `EntityEventDesc::Args` / `RemoteCallDesc::Args`). ```angelscript ///@ Event Server Game OnCritterDamaged(Critter cr, Critter? attacker, int32 damage) ///@ Event Server Game OnCritterDead(Critter critter, Critter? killer) ///@ RemoteCall Server SwitchCharacter(Critter? newCritter) ``` The declaration is the contract. Every `[[Event]]` subscriber and every `[[ServerRemoteCall]]` / `[[ClientRemoteCall]]` implementation that matches the event/call name must use the same `?` marker on each argument. The baker and side-specific handler binding enforce this relationship for remote calls; see [Remote Calls](../../reference/scripting/remote-calls.md). `[[AdminRemoteCall]]` is a separate command entry point rather than a `///@ RemoteCall` target. ```angelscript // Matches the OnCritterDamaged declaration above. [[Event]] void OnCritterDamaged(Critter cr, Critter? attacker, int32 damage) { ... } // Violates declaration parity: declaration has `Critter?`, handler drops `?`. [[Event]] void OnCritterDamaged(Critter cr, Critter attacker, int32 damage) { ... } ``` Because the AngelScript front-end now tracks the per-type nullable bit, the AS engine itself enforces null contracts on handle writes at runtime (see "Runtime enforcement" below). Event and remote-call declaration parity remains a project-side static-analysis responsibility: the AS engine has no way to know that two otherwise unrelated declarations are supposed to share a contract. ## Engine side: `ptr` / `nptr` and raw-pointer nullability Native methods declared with `///@ ExportMethod` in [../Source/Scripting/](../../../../Source/Scripting/), exported events, `FO_ENTITY_EVENT` payloads, `///@ ExportRefType` methods, and `///@ EngineHook` declarations express handle nullability through the smart-pointer vocabulary: a non-null entity/engine/ref pointer is `ptr`, a nullable one is `nptr` (see [SmartPointers.md](smart-pointers.md), "Script binding boundary"). Raw handle pointers (`T*`) are not a nullable export spelling anymore: codegen rejects them in exported/event signatures, and the native marshalling templates static-assert if a raw handle pointer reaches the ABI layer. Raw pointers that remain in generic AngelScript plumbing, generated registration strings, handle slots, process argv, or external C callbacks are ordinary low-level ABI values; bind them to `make_ptr(raw)` / `make_nptr(raw)` before engine work. The owning method files are mapped in [Script Methods Map](../../reference/script-api/method-ownership.md). (The former empty `FO_NULLABLE` marker macro has been removed; pointer nullability is now carried by the `ptr` / `nptr` spelling.) For native C++ code outside exported script signatures, use the pointer vocabulary in [SmartPointers.md](smart-pointers.md): `ptr` for borrowed non-null values, `nptr` for borrowed nullable values, and the matching `unique_*` / `refcount_*` owning forms. `nptr`, `unique_nptr`, and `refcount_nptr` are separate nullable wrapper types; the remaining migration work is tightening nullable operations on the non-null spellings. When an owner is only borrowed to perform a dynamic cast, call `owner.dyn_cast()` directly instead of building an intermediate `owner.as_ptr().dyn_cast()`. Stored native `ScriptFunc` signatures are part of the same contract. Their pointer spellings must match the script callback declaration argument by argument: a script callback parameter declared `Item?` is stored and looked up as `nptr`, while `Item` uses `ptr`. Otherwise a legitimate `null` can cross the exported method boundary successfully and then fail later when the callback wrapper implicitly narrows it. **Prefer the non-null spelling; reach for `nptr` only when absence is a real, handled state.** A nullable wrapper is dead weight when every caller already passes non-null, a function never returns null, or a member is always set before use — convert those to `ptr` / `unique_ptr` / `refcount_ptr`. In particular, do **not** make a raw `pointer + size` buffer parameter nullable just so the degenerate empty case may pass `nullptr` — take a non-null `const_span` / `span` (the engine's standard byte-buffer vocabulary) and let `.empty()` handle the zero-length case. This both removes the spurious nullability and deletes the `nptr x = nullptr; if (!c.empty()) x = c.data(); f(x, c.size())` boilerplate at every call site (`f(c)`). Conversely, leave `nptr` in place where absence is a real, handled state: the legitimate result of a fallible cast/lookup, a data member with a real transient-null window between construction and assignment, or a defensive boundary helper that deliberately accepts a nullable and asserts. In that case **keep the checked value as `nptr` and dereference it directly after the guard** (`if (!x) { ... return; }` / `FO_VERIFY_AND_THROW(x, ...)` then `x->`) rather than copying it into a `nullable_x` intermediate and narrowing. Past the guard the checked `nptr` also flows into any `ptr` parameter, member, or return **implicitly** — the `nptr`→`ptr` conversion asserts non-null at the conversion point. Owning wrappers (`refcount_ptr`/`refcount_nptr`, `unique_ptr`/`unique_nptr`, `unique_del_ptr`/`unique_del_nptr`, `unique_arr_ptr`, and `shared_ptr`) likewise borrow implicitly to `ptr` / `nptr`; ownership acquisition and nullable-owner ownership narrowing remain explicit (`hold_ref`, `adopt_unique_ptr`, `make_unique_del_ptr`, `take_not_null`, `safe_alloc::make_shared`, or a domain factory). Owner dynamic casts should be direct (`owner.dyn_cast()`) instead of going through an intermediate borrow. Freshly assigned non-null owner factory results can be used through the source owner directly when only a few member accesses follow. When a nullable/nullable-owner local is needed and later requires presence, bind the nullable wrapper with `auto`, then check it explicitly with `FO_VERIFY_AND_THROW(local, ...)` (or `FO_STRONG_ASSERT(local, ...)` inside `noexcept`) before deref; do not wrap the local in a double-negation expression for these guards. Explicit `.as_ptr()` / `.as_nptr()` calls are valid when they clarify the borrow or resolve overload/template deduction; implicit conversion remains available when the destination type is unambiguous. When a raw pointer enters native code, use `make_ptr(raw_value)` / `make_nptr(raw_value)`. The audit still enforces guarded nullable dereference through `NullableLocalDereference`; see [SmartPointers.md](smart-pointers.md). ```cpp ///@ ExportMethod FO_SCRIPT_API nptr Server_Critter_GetMap(ptr self) { return self->GetEngine()->EntityMngr.GetMap(self->GetMapId()); } ///@ ExportMethod FO_SCRIPT_API void Server_Player_SwitchCritter(ptr self, nptr cr) { self->GetEngine()->SwitchPlayerCritter(self, cr); } ``` The `self` (first parameter — `this` receiver) and the implicit `engine` parameter for global methods are **never** marked: AS validates `this` before dispatch. If an exported method gives a pointer argument the default value `nullptr`, spell that argument `nptr` so it is nullable; codegen records the default as script `null`, and the nullable spelling keeps the generated native-call validation aligned with the callable signature. A non-null `ptr` argument cannot default to `nullptr`. ### Component accessors are non-nullable and throw; probe with `Has` An entity component getter (`item.Weapon`, `item.MapExit`, `cr.DialogContext`, `item.Locker`, … — every property declared `///@ Property ... Component`) is **non-nullable** and **throws** when the component is absent. `Entity_GetComponent` in [../Source/Scripting/AngelScript/AngelScriptEntity.cpp](../../../../Source/Scripting/AngelScript/AngelScriptEntity.cpp) raises a `ScriptException` unless the presence bool is set, so the getter is registered as `{}@ get_{}() const` (no `?`). Alongside each component getter a `bool get_Has() const` accessor is registered for presence probes. ```angelscript // item.Weapon is ItemWeaponComponent (non-nullable) — access directly int dist = item.Weapon.MaxDist; // OK; throws iff the item is not a weapon // probe presence with Has, never `== null` if (item.HasWeapon) { int d = item.Weapon.MaxDist; // guarded } verify(item.HasWeapon, "Item must be a weapon"); ``` This mirrors the throwing global getters below (`Chosen` / `CurMap` / …): a missing component in code that already assumes it is present is an **invariant violation**, not a recoverable null. Do **not** write `item.Weapon == null` / `!= null` — the getter would throw at the access; write `!item.HasWeapon` / `item.HasWeapon`. The four entity flavors (concrete / `Abstract` / `Proto` / `Static`) and fixed types all share this registration, so `proto.HasWeapon`, `abstractItem.HasWeapon`, etc. are all available. (One name clash to keep in mind: `Ammo` is both an Item component and a nullable *property* of the weapon component — `item.Weapon.Ammo` is the loaded ammo item and remains a normal nullable `== null` check.) ### Throwing global getters: `Chosen` / `CurMap` / `CurLocation` / `CurPlayer` The client global getters `Chosen`, `CurMap`, `CurLocation`, `CurPlayer` (registered in [../Source/Scripting/ClientGlobalScriptMethods.cpp](../../../../Source/Scripting/ClientGlobalScriptMethods.cpp)) are **non-nullable** and **throw** when accessed while absent (e.g. "No chosen critter"). This is deliberate: most client code runs only in contexts where they exist (the in-game UI is disabled without a `Chosen`), so it should read `Chosen.X` directly without a null dance. To check presence where absence is a valid state, use the matching bool predicate: ```angelscript if (!HasChosen) { return; // no chosen critter right now - handle it } Critter cr = Chosen; // OK - non-nullable, guaranteed present here ``` `HasChosen` / `HasCurMap` / `HasCurLocation` / `HasCurPlayer` are the presence checks. Do **not** write `Chosen == null` / `Chosen != null`: comparing a non-nullable handle to null both trips the redundant-comparison warning (#5) and *throws* (evaluating `Chosen` when absent), so it is doubly wrong - use `!HasChosen` / `HasChosen` instead. ### `Game` during shutdown: `IsGameDestroying` `Game` is a **non-nullable** global handle (`GameSingleton@`), so script code reads `Game.X` directly without a null dance. But the game engine is genuinely absent in one window: **script-object destructors that run while the scripting backend is being torn down**. The AngelScript GC runs object destructors from inside `~AngelScriptBackend` *after* the engine pointer has already been reset ([../Source/Scripting/AngelScript/AngelScriptBackend.cpp](../../../../Source/Scripting/AngelScript/AngelScriptBackend.cpp)), and `get_Game` is a **throwing getter** — it raises *"Game engine is not available"* when the backend has no engine. So a `~T()` that touches `Game.*` during shutdown throws from the destructor, and `Game != null` cannot guard it either (same double-wrong as `Chosen != null`: redundant-comparison warning #5, plus `Game` throwing as it is evaluated). The `bool IsGameDestroying` global getter (registered next to `get_Game()` in [../Source/Scripting/AngelScript/AngelScriptGlobals.cpp](../../../../Source/Scripting/AngelScript/AngelScriptGlobals.cpp)) is the probe for exactly this case: it is `true` precisely when `Game` is unavailable (the backend's `HasGameEngine()` is false — the same condition under which `get_Game` throws), and reading it never evaluates the `Game` getter. Use it to skip engine-dependent cleanup in destructors: ```angelscript ~Sprite() { // Game engine may already be gone during shutdown; freeing the sprite then is both impossible and unnecessary if (!IsGameDestroying) { Unload(); // calls Game.FreeSprite(...) } } ``` Reach for `IsGameDestroying` only in destructors (or other teardown paths that can run during backend destruction). Everywhere else `Game` is guaranteed present — read it directly. ### Throwing proto getters: `Game.GetProtoItem/Critter/Map/Location` + `CheckProtoX` `Game.GetProtoItem`, `GetProtoCritter`, `GetProtoMap`, `GetProtoLocation` (in [../Source/Scripting/CommonGlobalScriptMethods.cpp](../../../../Source/Scripting/CommonGlobalScriptMethods.cpp)) are **non-nullable** and **throw** when no proto with that id exists (e.g. "Item proto not found"). Each has a matching `Game.CheckProtoItem/Critter/Map/Location(pid)` bool predicate for the case where the id may legitimately be missing (stale checkpoint/map data, user-supplied ids, …). ```angelscript // id known to exist - read directly: ProtoItem proto = Game.GetProtoItem(Content::Item::Dynamite); // id may be missing - probe first, or keep a nullable local via a guarded ternary: if (!Game.CheckProtoMap(mapPid)) { return; // unknown map proto - handle it } ProtoMap proto = Game.GetProtoMap(mapPid); ProtoLocation? loc = Game.CheckProtoLocation(locPid) ? Game.GetProtoLocation(locPid) : null; if (loc == null) { /* recover */ } ``` Do **not** write `Game.GetProtoX(pid) == null` / `!= null` (it throws on a missing proto before the comparison) - use `!Game.CheckProtoX(pid)` / `Game.CheckProtoX(pid)`. The same applies to the **codegen-generated proto/fixed-type getters** for custom entities (`Game.GetProtoModifier`, `Game.GetProtoFaction`, `Game.GetEncounterProfileData`, `Game.GetWeatherType`, `Game.GetItemBag`, …): they are non-nullable and **throw** when the id is unknown, with a matching `Game.Check(pid)` predicate. Registered in `register_entity_protos` / `register_fixed_type` (`Game_GetProtoCustomEntity` / `Game_CheckProtoCustomEntity`) in [AngelScriptEntity.cpp](../../../../Source/Scripting/AngelScript/AngelScriptEntity.cpp). Read directly when the id is known (`instance.ProtoId`, an authored content id); probe with `Check` only where the id may legitimately be absent. ## Runtime enforcement There are now two complementary runtime gates: Managed indexed interop applies the same contract to its packed ABI. Scalar and plain fixed-value data are non-nullable value slots. Entity, prototype, fixed entity, and native reference arguments use pointer-sized handle slots whose nullable bit comes from the generated `ManagedInteropAbi` manifest. ABI binding rejects a generated/native manifest mismatch before scripts start, while the generated wrapper performs the required null check at the call boundary. Do not encode optional references as zero-valued fixed data or relax a metadata declaration to work around binding failures. ### Script-side: `asBC_RefCpyChk` on handle assignments The AngelScript compiler emits a new `asBC_RefCpyChk` bytecode (defined in [../ThirdParty/AngelScript/sdk/angelscript/include/angelscript.h](../../../../ThirdParty/AngelScript/sdk/angelscript/include/angelscript.h), handler in [../ThirdParty/AngelScript/sdk/angelscript/source/as_context.cpp](../../../../ThirdParty/AngelScript/sdk/angelscript/source/as_context.cpp)) whenever the destination of a handle write is **non-nullable** (`T` without `?`) and the destination is **user-declared** (not a compiler-generated temporary). It is a drop-in REFCPY variant that raises a *Null assignment to non-nullable handle* exception when the source handle is null. Emission sites are `PerformAssignment` and `CompileInitializationWithAssignment` in [../ThirdParty/AngelScript/sdk/angelscript/source/as_compiler.cpp](../../../../ThirdParty/AngelScript/sdk/angelscript/source/as_compiler.cpp); `T?` declarations fall back to the original `asBC_REFCPY` and accept null silently. The non-nullable test is keyed off the **declared** type of the destination, not a smart-cast narrowed view of it: a declared-nullable local or `&` parameter that is currently narrowed (see [Smart-cast](#smart-cast-flow-sensitive-narrowing)) is still a `REFCPY` destination, so `x = null;` inside the narrowing guard is a legal *un-narrowing* write, not a null write into a non-nullable slot. `PerformAssignment` restores the declared nullability on the lvalue before choosing the copy instruction and then invalidates the narrowing, so the next read sees `T?` again. (Before this fix the instruction was chosen from the narrowed type, which compiled the branch into an always-throwing `asBC_RefCpyChk` — the *"Null assignment to non-nullable handle"* ScriptExceptions from `Combat::DeferredAttackHit` and cursor handling were this defect.) The temp guard matters because `PerformAssignment` is reused for argument-setup slots whose type is inherited from a native parameter (no `?` syntax on the AS side). Nullability of those slots is owned by the native-boundary check below — letting the AS-level check skip temporaries keeps `func(null)` working for nullable (`nptr`) natives. This means **script-to-script assignments** of null to a non-nullable handle still throw, even when the value originates from another script function rather than a native call. ### Native-boundary: codegen-emitted arg/return checks Native validation continues to be plumbed through codegen-generated `MethodDesc::Call` lambdas, **not** the AS-to-native bridge. [../BuildTools/codegen.py](../../../../BuildTools/codegen.py) emits per-method calls to `NativeDataProvider::CheckArgNotNull` / `CheckReturnNotNull` (defined in [../Source/Common/ScriptSystem.h](../../../../Source/Common/ScriptSystem.h)) right before/after the native invocation: ```text MethodDesc::Call(call) → NativeDataProvider::CheckArgNotNull(call, i, "Server_Player_SetCritter", "cr", "Critter") // for each non-nullable entity arg → native invocation → NativeDataProvider::CheckReturnNotNull(call, "...", "...") // for non-nullable entity return ``` Array parameters and returns are not blanket-scanned at this boundary. Script arrays can intentionally contain null handle cells (for sparse grids, nullable-element arrays, or caller-owned filtering), and the native export metadata currently has no per-element nullability bit that can distinguish `array` from `array` for generated C++ wrappers. If a particular API requires non-null elements, enforce that invariant in the API-specific producer/consumer and document it there; do not rely on a global codegen check that would reject legitimate nullable arrays. Doing scalar checks at the `MethodDesc::Call` boundary means **every** caller of an `///@ ExportMethod` is covered — the AS-to-native bridge, native test harnesses, future Mono-backend dispatch, anyone. Scalar checks cost a single pointer compare. Violation surface: `ScriptException`, propagated to the calling AngelScript context. Three distinct messages: - *"Null assignment to non-nullable handle"* — raised by `asBC_RefCpyChk` (the new AS-side check on bare-handle writes). Distinct from the generic null-deref so stack traces clearly point at the bad assignment rather than a downstream method call. - *"Null pointer access"* — the original AS message, raised by `asBC_CHKREF` / `asBC_ChkRefS` / `asBC_ChkNullV` / `asBC_ChkNullS` for dereferences, indexing, and method calls on a null handle. - Native-boundary scalar checks emit the method name, parameter name when applicable, and type via the codegen-generated `NativeDataProvider::CheckArgNotNull` / `CheckReturnNotNull`. ### Compile-time guarantees In addition to the runtime `asBC_RefCpyChk`, the AS front-end raises two compile-time errors on null-unsafe assignments before any bytecode is generated: 1. **Bare `null` to a non-nullable handle is always rejected.** Always-on, no engine property required. Emits: *"Cannot assign 'null' to a non-nullable handle of type 'T' (use 'T?' to allow null)"*. This catches `T x = null;`, `someField = null;`, and the implicit `T x;` form that AS lowers into a null initializer. 2. **Nullable handle source to a non-nullable destination is rejected when `asEP_DISALLOW_NULLABLE_TO_NON_NULLABLE` is enabled.** FOnline enables this property in [../Source/Scripting/AngelScript/AngelScriptBackend.cpp](../../../../Source/Scripting/AngelScript/AngelScriptBackend.cpp). Emits: *"Cannot assign nullable 'T?' to non-nullable 'T' without a null-check (add `if (src != null)` or change the destination to 'T?')"*. 3. **Redundant `?` on local initializer is warned about.** When the destination is declared `T?` but the initializer is statically a non-nullable handle (the source type cannot produce `null`), the front-end emits: *"Redundant '?': initializer of type 'T' cannot be null; declare destination as 'T' instead"*. This catches stale `T?` annotations left behind after an API tightened its return type (e.g. `nptr` → `ptr`), and the `if (cr == null)` dead branch that usually follows. Emitted from `CompileInitialization` in [../ThirdParty/AngelScript/sdk/angelscript/source/as_compiler.cpp](../../../../ThirdParty/AngelScript/sdk/angelscript/source/as_compiler.cpp). The diagnostic is a *warning* (compilation succeeds) so existing scripts keep building while authors clean up; the message names both the current source type and the bare destination spelling so the fix is a one-character drop. Like the deref (#4) and redundant-comparison (#5) checks it **trusts the static non-nullability of the source**, including a *non-const handle reference*: reading an `arr[i]` cell of a non-nullable element array, or a non-nullable `T` field/local by reference, aliases storage whose non-null invariant is enforced on every write, so `T? x = arr[i]` is flagged exactly as `arr[i] != null` is by #5. Two source shapes are exempted, because a non-null static type does **not** guarantee a non-null value there: - `cast(...)` and `cond ? a : b` — a failed reference cast yields null, and ternary branch types may differ (use `cast` when the cast can fail). - a **const handle reference `T@const&`** — `dict.get(key, default)` substitutes its (possibly null) default and yields null on a missing key, so the `?` on `T? v = someDict.get(k, null)` is genuinely needed, not redundant. 4. **Dereferencing an un-narrowed nullable handle is warned about.** When a `T?` value is dereferenced (`x.Member`, `x[i]`, `x.Method()`) without first narrowing it to non-null, the front-end emits: *"Dereference of nullable handle 'T?' without a null-check; narrow it first (e.g. `if (x == null) return;`) so it becomes 'T'"*. Emitted from the `ttDot` branch of `CompileExpressionPostOp` in [../ThirdParty/AngelScript/sdk/angelscript/source/as_compiler.cpp](../../../../ThirdParty/AngelScript/sdk/angelscript/source/as_compiler.cpp). It is a *warning*, not an error — the runtime null-deref check (`asBC_CHKREF` etc.) is still the safety net — so authors clean up at their own pace. Narrowing only tracks **named locals/params**: `x.Method() == null || x.Method().Foo` still warns on the second call because each getter call is a fresh expression; bind to a local (`auto v = x.Method(); if (v == null) return; v.Foo`) to narrow. 5. **Redundant null comparison against a non-nullable handle is warned about.** When one operand of `==` / `!=` is `null` and the other is a statically non-nullable handle — **a named local/param *or* a temporary** (e.g. a property/getter call result) — the comparison has a constant result and the guarded branch is dead. The front-end emits: *"Redundant null comparison: 'T' is a non-nullable handle and can never be null; remove the check (or make the source nullable if it actually can be null)"*. Emitted from `CompileOperatorOnHandles` in [../ThirdParty/AngelScript/sdk/angelscript/source/as_compiler.cpp](../../../../ThirdParty/AngelScript/sdk/angelscript/source/as_compiler.cpp). This is the inverse of the deref warning (#4): together they push every nullable value toward exactly one null-check at the point it is introduced. Like #3/#4 it is a *warning*. The "or make the source nullable" hint matters — a hit usually means the **source** is mis-modeled as non-nullable, and the fix is to spell it nullable rather than delete the check. The common cases: - A **throwing getter** (`Chosen`, `CurMap`, a `item.Weapon` component getter, `Game.GetProtoModifier(...)`) returns non-null and *throws* when absent, so `getter() != null` is a footgun — it throws before the comparison. Use the matching `Has*` / `Check*` probe (`HasChosen`, `item.HasWeapon`, `Game.CheckProtoModifier(...)`). - A **fallible reference cast** `cast(x)` is non-null-typed but yields null on a failed downcast — write `cast(x)` so the result is `T?` and the `!= null` is a legitimate check (see [Reference casts](#reference-casts)). - A **proto / fixed-type property handle** that may be unset (`UsableOn.TargetItem`, `Harvested.SmallBag`, `Weapon.Ammo`) — declare the `///@ Property` with the `Nullable` flag so its getter is registered `@?` (see [Nullable property handles](#nullable-property-handles)). - A native return that can be null but is spelled non-null `ptr`, or a genuinely-non-null source where the check is truly dead — spell it `nptr` / remove the check respectively. Both named operands and temporaries are checked, so binding a fallible value to a local does **not** silence the warning — only spelling the source nullable (`cast`, `Nullable` flag, `nptr`) does. That is deliberate: it forces the *type* to tell the truth instead of relying on a hidden runtime-null. **Conditional expressions propagate nullability.** A `cond ? a : b` result is typed `T?` when either branch is statically nullable (or a `null` literal). `CompileCondition` (FOnline patch in [../ThirdParty/AngelScript/sdk/angelscript/source/as_compiler.cpp](../../../../ThirdParty/AngelScript/sdk/angelscript/source/as_compiler.cpp)) captures this before the branch-type unification can drop the flag, then re-applies it to the result handle. This lets the assignment (#2), deref (#4), and redundant-comparison (#5) checks treat `Entity x = cond ? a? : b` as a nullable source and flag it at compile time, instead of leaving it solely to the runtime `asBC_RefCpyChk`. The redundant-`?` exemption for ternaries in #3 still stands: a ternary whose branches are *statically* non-null can still yield null at runtime through a failed reference cast in one branch, so `T? x = cond ? a : b` is not reported as a redundant `?`. (The change is compile-time diagnostics only — it does not alter emitted bytecode, since the runtime `asBC_RefCpyChk` is keyed off the assignment *destination*, so no compatibility-version bump is required.) The runtime `asBC_RefCpyChk` is still the safety net underneath both errors: even when the compile-time pass accepts an assignment (e.g. through a `dict.get(key, null)` that returns a statically-non-nullable reference bound to null at runtime), the bytecode still throws on null write. ### Identity comparison: `==` only, never `is` / `!is` `is` and `!is` are **banned in `.fos`** by project convention. Use `==` and `!=` for both null checks and handle-identity comparison. A FOnline patch on `CompileOperator` ([../ThirdParty/AngelScript/sdk/angelscript/source/as_compiler.cpp](../../../../ThirdParty/AngelScript/sdk/angelscript/source/as_compiler.cpp)) falls back to `asBC_CmpPtr` (handle identity) when no `opEquals` is registered for the ref type, so the two forms produce the same bytecode: | Operands | `==` / `!=` behavior | |----------|-----| | Both sides are entity types (`Critter`, `Item`, `Map`, ...) with codegen-emitted `opEquals` | id-based equality (compares entity id) | | Either side is `null` | null-handle compare | | Both sides are ref types **without** `opEquals` (`Gui::Screen`, `Object`, custom classes, funcdef handles) | handle-identity (`asBC_CmpPtr`) - same as `is` | | One side is a reference and the other is a handle | implicit conversion to handle, then handle-identity | This means the choice between "id-based" and "pointer-based" is determined by the *type*, not the operator, so the operator carries no extra information and `is` is pure cognitive noise. Projects that adopt the `==` / `!=` convention should enforce it with a read-only source check in CI. ### Smart-cast (flow-sensitive narrowing) Kotlin-style smart-cast narrows a `T?` local back to `T` inside a region that is provably non-null, so the body can use it without an explicit cast. Implemented as a per-scope `smartCasts` stack in [../ThirdParty/AngelScript/sdk/angelscript/source/as_compiler.cpp](../../../../ThirdParty/AngelScript/sdk/angelscript/source/as_compiler.cpp); see `DetectNullCheckPattern`, `GetNarrowedTypeForLocal`, and the integrations in `CompileIfStatement` (statement guards), `CompileCondition` (ternary branches), and `CompilePostFixExpression` (`&&` / `||` short-circuit operands). For the short-circuit case the `!=` / `==` check tags its result context in `CompileOperatorOnHandles`; that tag carries a **list** of narrowed locals (`nullCheckNarrowList` on `asCExprContext`), and each chained `&&` (keeps the `!=`-tags) / `||` (keeps the `==`-tags) **accumulates all** matching-polarity narrowings from both operands — so `a != null && b != null && a.X && b.Y` narrows both `a` and `b` in the trailing operands, not just the nearest check. When a tagged operand lands on the postfix evaluation stack, `ShortCircuitNarrowsRightOperand` scans ahead to confirm it is consumed as the *left* operand of a matching short-circuit, and if so every narrowing in the list is pushed for that operator's **entire right operand** (tracked by expr-stack index, popped when the operator is reached) — so `x != null && x.Prop == y` narrows `x.Prop`, not just a term sitting immediately before the `&&`. The tag stores each local's *stack offset*, which is **negative for parameters** — so a dedicated `nullCheckNarrowValid` flag (not the offset sign) marks "tag present", otherwise narrowing would silently skip every parameter. For statement guards, a then/else branch counts as "exits without falling through" (so the complementary narrowing applies afterwards) not only for `return`/`throw` but also for `break` / `continue` (`StatementAbortsFallThrough`) — so `if (x == null) continue;` inside a loop narrows `x` for the rest of the body exactly like an early `return`. This narrowing-only signal never feeds the function-must-return-value analysis (which still ignores `break`/`continue`). Supported patterns: ```angelscript // 1) `if (x != null) { ... }` narrows in the then-branch Item? maybeItem = GetMaybeItem(); if (maybeItem != null) { Item item = maybeItem; // OK — compiler treats maybeItem as Item here item.Use(); } // 2) `if (x == null) { ; return; } ` — early-exit narrows after the if Critter? maybeCr = GetMaybeCr(); if (maybeCr == null) { Logging::Warning("CharacterRoster", "main_critter_missing player=" + player.Name); return null; } Critter cr = maybeCr; // OK — the early return rules out null // 2b) break / continue guards narrow the same way (loop bodies) for (int i = 0; i < ids.length(); i++) { Critter? probe = Game.GetCritter(ids[i]); if (probe == null) { continue; // bails this iteration } probe.Use(); // OK — narrowed for the rest of the loop body } // 3) Compound `&&` / `||` shapes narrow every recognised atom if (a != null && b != null && c != null) { Item ai = a; Item bi = b; Item ci = c; // OK — all three narrowed } if (a == null || b == null) { return; } Item ai = a; Item bi = b; // OK — both narrowed after early return // 4) Assignment invalidates the narrowing on that local. The write itself goes // through the DECLARED type — narrowing is a read-time refinement only — so // `x = null;` inside the guard is a legal un-narrowing write (plain REFCPY), // not a null write into a non-nullable slot. if (x != null) { x = GetMaybeNull(); // x becomes nullable again Item y = x; // compile-time error here } if (x != null && x.IsBroken()) { x = null; // OK — drops the narrowed view, x is `Item?` again } // 5) `&&` / `||` short-circuit narrows every later operand in the chain (any // expression, not just an `if` condition). `&&` consumes a `!=` check (the // rest of the chain runs only when the check was true); `||` consumes an // `==` check. The check may sit anywhere in the chain, and works for locals // and parameters alike. bool ready = maybeItem != null && maybeItem.IsReady(); // narrowed in RHS bool ok = maybeItem == null || maybeItem.IsReady(); // narrowed in RHS bool both = Other() && maybeItem != null && maybeItem.IsReady(); // narrowed after the check bool tail = maybeItem != null && Other() && maybeItem.IsReady(); // still narrowed at the tail // the narrowing covers the WHOLE right operand, not just an adjacent term: bool cmp = maybeItem != null && maybeItem.Id == wanted; // maybeItem.Id narrowed if (maybeItem != null && maybeItem.Id > 0 && Other()) { ... } // narrowed across the compound // every checked local in the chain narrows in the later operands, not just the nearest: if (a != null && b != null && a.Id == b.Id) { ... } // both a and b narrowed if (a == null || b == null || a.Id != b.Id) { return; } // both narrowed past the ||s // 6) Ternary branches narrow when the condition is a null-check int n = maybeItem != null ? maybeItem.Id : 0; // then-branch narrowed int m = maybeItem == null ? 0 : maybeItem.Id; // else-branch narrowed ``` Smart-cast deliberately does **not** narrow: - Class fields or globals (`obj.Field`, `g_Var`). Snapshot to a local first. - Method return values (`GetMaybe()`). Bind to a local. - The result of `cast(...)`. A bare `cast` is non-nullable-typed but can fail at runtime — see [Reference casts](#reference-casts) for the `cast` form and binding to a local. - Conditions with mixed `&&` / `||` at the same precedence level inside an `if`. Split the `if`. - A local **reassigned** inside one of the chain's operands (rare): the chain narrowing follows the immediate-narrowing contract and does not re-track the new value. When smart-cast can't see through the shape, the established fallbacks are: bind the expression to a local, change the destination to `T?`, or wrap the use in `if (x == null) { ; return ...; }`. ### Reference casts: `cast(x)` A reference cast can fail at runtime: `cast(expr)` yields `null` when `expr` is not a `T`. Spell the fallible form **`cast(x)`** so the result type is `T?` and the engine treats it honestly: - `cast(x) != null` / `== null` is a legitimate null-check (no redundant-comparison warning, #5). - `cast(x).Member` requires narrowing first (bind to a local, then `if (v == null) ...`). A bare `cast(x)` keeps the non-nullable "this cast is known to succeed" contract (like a `static_cast`): you may chain `cast(x).Member` directly, but `cast(x) != null` is flagged redundant (#5) — switch to `cast(x)` there. The `?` is honored by `CompileConversion` in [../ThirdParty/AngelScript/sdk/angelscript/source/as_compiler.cpp](../../../../ThirdParty/AngelScript/sdk/angelscript/source/as_compiler.cpp), which applies `to.IsNullable()` to the cast result (including the early-return path where an implicit conversion already produced the target type). `cast(x)` also covers a **same-type** read that is statically non-null but can be null at runtime: a legacy/sparse field declared `array` keeps the element type `T`, yet `resize`/grid growth can leave empty handle cells as **null**. Reading such a cell into a non-nullable `T` (`T v = field[i];`) throws `Null pointer access`, and writing `T? v = field[i];` is flagged a redundant widening (#3) because the element type is non-null. Prefer declaring nullable-element storage as `array` / `T?[]` when the array is allowed to contain holes; when working with an existing `array` sparse container, spell the read `cast(field[i])` so the source type tells the truth. For API parameters, native returns, and sync/lock scopes, null-element policy belongs to that specific API contract. Use nullable-element arrays when null is meaningful; otherwise validate at the producer or at the API entry point that owns the invariant. ### Nullable property handles A `///@ Property` whose type is a proto or fixed-type **handle** (`ProtoItem`, `ItemBag`, `ProtoMap`, …) is non-nullable by default but returns `null` when the field is unset. When that "unset → null" state is legitimate, add the **`Nullable`** flag to the property tag so its AS getter is registered as `@?`: ```angelscript ///@ Property Item Server ProtoItem UsableOn.TargetItem Nullable ///@ Property Item Common ItemBag Harvested.SmallBag Nullable ///@ Property Critter Server ProtoItem StartWeapon Nullable ``` The flag is parsed in [../Source/Common/Properties.cpp](../../../../Source/Common/Properties.cpp) (`_isNullable`) and consumed by the entity getter registration in [../Source/Scripting/AngelScript/AngelScriptEntity.cpp](../../../../Source/Scripting/AngelScript/AngelScriptEntity.cpp) (`@?`); [`MetadataBaker`](../../../../Source/Tools/MetadataBaker.cpp) accepts `Nullable` only on FixedType / Proto entity properties. Without it, `proto.Field != null` is flagged redundant (#5) even though the field can legitimately be unset — reach for the flag rather than deleting the check. (Spelling the type `ItemBag?` in the tag does **not** work — nullability of a stored property is a flag, not a `?` suffix.) For a **`Mutable`** nullable handle property the **setter** parameter is registered nullable too (`@?+`), matching the getter — see the `set_handle_str` branch in [AngelScriptEntity.cpp](../../../../Source/Scripting/AngelScript/AngelScriptEntity.cpp). This is load-bearing, not cosmetic: AngelScript derives a virtual property's static type from the **setter parameter** whenever a setter exists (only getter-only / read-only properties fall back to the getter's return type — see `FindPropertyAccessor` in `as_compiler.cpp`). A non-nullable setter parameter alongside an `@?` getter would make `T? local = obj.MutableNullableProp` read as a *non-nullable* handle and wrongly trip the redundant-`?` warning (#3) — while `T local = obj.MutableNullableProp` (no `?`) still errors via the getter's nullable return — leaving the read with no warning-free spelling. Keeping the setter parameter nullable resolves both spellings consistently. ### Invariant helpers The Engine no longer ships the former AngelScript `Core.fos` library. An embedding project that keeps the traditional `verify(cond, message, ...)` variadic macro owns its definition, visibility, and tests: ```text #define verify(cond, ...) if (!(cond)) throw(__VA_ARGS__) ``` When a project provides it, it states an **invariant**: a condition that holds whenever its own server and client logic behaves correctly. A failure means a bug, so it throws, and the project must keep it enabled in every configuration rather than treating it as a debug-only assertion. Managed C# has the Engine-owned equivalents `Game.Verify`, `Game.VerifyNotNull`, and `Game.Unreachable` in `Source/Scripting/Managed/CoreScripts/Verify.cs`; their nullable-flow annotations and throwing behavior are documented in [Managed C# Scripting](../../how-to/scripting/managed-csharp.md). #### Verify vs. graceful recovery Choose by *what a failure represents*. **Scripts never call `throw` directly** - every failure path goes through `verify`: | Shape | Use when | |-------|----------| | `verify(cond, "...")` | `cond` is an **invariant** - it must hold whenever our system behaves correctly. A violation is a bug (or a tampered client; see below). | | `verify(false, "...")` | an **unconditional** failure with no single guard condition: an unreachable branch (`switch` default, post-loop "not found"), or a guard whose body does more than fail (e.g. log-then-fail). Always throws - the replacement for what used to be a bare `throw(...)`. It is **no-return**, so no `return` / `break` is needed after it (the compiler treats a constant-true `if` whose body terminates as terminating). | | `if (x == null) { ; return ...; }` | the state is an **expected, recoverable** runtime outcome - log and fall back, do not throw. | #### Client input: transport validation and script invariants Treat every client-originated payload as untrusted at the server boundary. The Engine validates remote-call framing, payload size, collection bounds, encoded value shapes, and complete buffer consumption before synchronization and handler dispatch; see [Remote Calls](../../reference/scripting/remote-calls.md). The project handler still owns authorization, object ownership, ranges, state transitions, and other domain rules, and must validate them before its first mutation. Inside script code, `verify` is the always-on rejection mechanism when malformed or forbidden input violates that handler contract. The same check catches both a bug in the project's normal client and a tampered request, but it complements rather than replaces the native transport checks and handler-specific semantic validation. Use an ordinary recovery branch instead when rejection is an expected gameplay outcome rather than an invariant failure. #### Narrowing and arguments `throw(message, ...)` is the exception-raising **global function** (registered in [AngelScriptGlobals.cpp](../../../../Source/Scripting/AngelScript/AngelScriptGlobals.cpp); AngelScript has no `throw` keyword, so the name is free) - but it is only the **primitive the `verify` macro expands to**. Scripts do **not** call `throw` directly; an unconditional failure is written `verify(false, message, ...)`. Because `throw` is marked noreturn, a passing `verify(x != null, "...")` **narrows** `x` to non-null for the rest of the scope, exactly like the `if (x == null) return;` early-exit guard - the macro expands to `if (!(x != null)) throw(...)`, whose then-branch is noreturn. So a verify both documents the invariant and removes the dereference warning that follows. The macro is **variadic**: `verify(cond, message, ctx...)` forwards `message` and any trailing context values straight to `throw`, so a verify carries the same diagnostic context a bare `throw` would (e.g. `verify(trader != null, "Barter target not exists", player, traderId)`). Author the `message` as a readable sentence (`"No current player"`, not `"Expected: CurPlayer != null"`), and pass the entities / ids / values you'd want in the exception as extra arguments. **Scope of enforcement:** every **script handle** crossing the script ↔ native boundary is validated. Concretely codegen emits the check when the meta-type is one of: - a `///@ ExportEntity` name (`Critter`, `Item`, `Map`, `Location`, `Player`, `Game`, `ImGui`) or the generic `Entity`, - an entity relative (`Abstract`, `Proto`, `Static` — currently `AbstractItem`, `ProtoCritter`, `ProtoItem`, `ProtoLocation`, `ProtoMap`, `StaticItem`), - a `///@ ExportRefType` class (`MovingContext`, `MapSpriteHolder`, `SpritePattern`, `VideoPlayback`, `ScriptImGui`). On the C++ engine side the matching pointer spellings (`ptr` / `nptr`, `ptr` / `nptr`, `ptr` / `nptr`, `ptr`, `ptr`, `ptr`, …) are all in scope. The membership test lives in `is_validated_pointer_meta_type(...)` in [../BuildTools/codegen.py](../../../../BuildTools/codegen.py), which is now the sole arbiter: with no `FO_NULLABLE` marker, codegen treats exported `ptr` handles as non-null and `nptr` handles as nullable. Bare raw handle pointers are rejected before registration. Marking `?` on a primitive value type (`int`, `bool`, `mpos`, `hstring`, …) is rejected because those types have no `null` representation. **Out of scope:** script-to-script parameter passing. The `asBC_RefCpyChk` instruction covers handle **assignments** and **initializations**; AS argument passing typically uses copy-on-call patterns that bypass REFCPY. A project-side declaration analyzer plus the native-boundary check can cover the common case where a script value flows through an engine call. ### Migration note The change is **source-incompatible** for any AS code that assigned `null`, or a value that might be null at runtime, to a bare handle. After this change, those scripts must mark the destination `T?`. Inline test scripts embedded in `Source/Tests/Test_*.cpp` were updated in lock-step and are the engine-owned regression coverage. The strict compile-time variant (`asEP_DISALLOW_NULLABLE_TO_NON_NULLABLE`) catches more shapes at build time and should be enabled by projects that have completed the nullable migration. Any remaining runtime *"Null assignment to non-nullable handle"* indicates a missing marker or an invalid non-null contract. Common shapes that the smart-cast or compile-time checker cannot see through, and therefore require an explicit `T?` on the destination or a guarding recovery path, include `dict.get(key, null)`, output-reference parameters (`Map& out`), and class fields assigned across method boundaries. FOnline runs with `asEP_ALLOW_IMPLICIT_HANDLE_TYPES`, so script code uses the implicit-handle form (`Critter`, `Item?`, `array`) rather than the explicit `@` form (`Critter@`, `Item@?`, `array`) wherever the type is itself a ref class. The builder rejects an explicit `@` on `asOBJ_IMPLICIT_HANDLE` types when compiling a user-authored script section with the error *"Explicit handle '@' is not allowed on implicit-handle type 'X'"*. Funcdef parameters keep the `Type@+` form (handle with auto-add-ref) because the trailing `+` modifier is semantically required and the bare `Type+` form is not a valid AS signature. Native API registrations (`RegisterObjectType`, `RegisterFuncdef`, `RegisterObjectMethod`, `GetFunctionByDecl`) and engine-generated declaration strings continue to accept the explicit `@`. The rejection only fires when there is a script module being built (`module != 0`) and the builder is not in silent lookup mode — see `CreateDataTypeFromNode` in [../ThirdParty/AngelScript/sdk/angelscript/source/as_builder.cpp](../../../../ThirdParty/AngelScript/sdk/angelscript/source/as_builder.cpp). ## Project-side tooling The engine owns compiler/runtime enforcement and the native binding contract. An embedding project can add read-only source analyzers for authoring rules that span otherwise unrelated script declarations. Useful checks include: - `?` is used only on handle-capable reference types; - event and remote-call handlers match declaration nullability argument by argument; - a nullable local is narrowed before dereference; - a guarded native `nptr` is dereferenced directly instead of copied into a redundant narrowing alias; - project style does not mix implicit-handle syntax with explicit `@` syntax; - forbidden identity operators or redundant defensive null guards do not return. Keep marker placement explicit. Inferring nullability from body shape is unreliable: a forwarding function may never dereference a non-null parameter, while a nullable return may be produced only through a helper. Authors declare `ptr` / `nptr` on native exports and `T` / `T?` in scripts; analyzers verify those declarations rather than rewriting contracts heuristically. Project-side generators and analyzer task names belong in that project's documentation. If a checker becomes reusable across FOnline games, move it into `BuildTools/` with engine-owned tests before citing it here as a standard command. ## Adding / editing markers When you write a new script function or native export: 1. Choose the nullable contract explicitly in the declaration (`T?` for script handles; `nptr` for nullable native exported pointers, `ptr` for non-null) when the function meaningfully accepts or returns null. 2. Compile the affected script modules so the engine's nullable diagnostics run. 3. Run any project-side declaration-parity and style analyzers required by the embedding project. Project-side rewriters must not own contract inference: they should preserve author-chosen markers and may remove only guard code made redundant by generated runtime checks. If a nullable case is real (for example a `dynamic_cast(param)` path where null is meaningful), keep the marker in the signature and update the analyzer only when its guard-removal pattern is wrong. ## See also - [Scripting](../../explanation/scripting-runtime/) — overall engine scripting runtime, AngelScript and Managed backends, and method-export organization. - [Managed C# Scripting](../../how-to/scripting/managed-csharp.md) — generated nullable surface, async lifetime, analyzers, and build/bake validation. - [Remote Calls](../../reference/scripting/remote-calls.md) - remote-call signatures, handlers, serialization, and project catalog generation. - [Script Methods Map](../../reference/script-api/method-ownership.md) — current `///@ ExportMethod` file map and ownership boundaries. - [GeneratedApiAndMetadata.md](../../reference/metadata/index.md) — generated metadata/API flow that must stay aligned with script-visible contracts. - [SmartPointers.md](smart-pointers.md) — native C++ pointer ownership/nullability vocabulary. - [Testing](../../../Testing.md) — reusable unit, script, integration, package, and smoke-test boundaries. ===== END DOCUMENT nullability ===== ===== BEGIN DOCUMENT smart-pointers ===== Source: Docs/en/contributing/coding-contracts/smart-pointers.md Canonical URL: https://fonline.ru/Docs/en/contributing/coding-contracts/smart-pointers.html Content SHA-256: 00127de2f7ad8dc83ae4397bee91938e88a859e61e5d4d9ce1f581f7dff69a4e --- layout: default title: Smart Pointers locale: en document_id: smart-pointers permalink: /Docs/en/contributing/coding-contracts/smart-pointers.html --- # Smart Pointers > Engine-owned documentation. This page defines the native C++ pointer vocabulary in `Source/Essentials/SmartPointers.h`: ownership, nullability, migration rules, and validation expectations. ## Purpose The engine uses small pointer wrappers to make pointer contracts visible at the type level. The long-term convention matches the script-side nullability model: absence must be explicit. A non-null type means `null` is not a normal state; a nullable type uses the `n*` spelling. The migration from the older `raw_ptr` name is intentionally staged. `raw_ptr` and `nullable_raw_ptr` are removed from the engine-owned API; use `ptr` and `nptr` instead. `nptr`, `unique_nptr`, and `refcount_nptr` are separate nullable wrapper types. The default build enables the strict non-null contract for ordinary `ptr`, `unique_ptr`, and `refcount_ptr` spellings; nullable state belongs in the matching `n*` type. ## Pointer vocabulary | Meaning | Non-null spelling | Nullable spelling | | --- | --- | --- | | Non-owning borrowed pointer | `ptr` | `nptr` | | Unique owning pointer | `unique_ptr` | `unique_nptr` | | Intrusive refcount owning pointer | `refcount_ptr` | `refcount_nptr` | | Unique array pointer | `unique_arr_ptr` | Future `unique_arr_nptr` if needed | | Custom-deleter unique pointer | `unique_del_ptr` | `unique_del_nptr` | `raw_ptr` / `nullable_raw_ptr` are legacy spellings. New engine-owned code must use `ptr` or `nptr`. ## Contracts `ptr` is a borrowed, non-owning pointer. In the final strict contract it is non-null in usable state, has no normal `nullptr` assignment/check path, and must not be used to model optional state. `nptr` is a borrowed nullable pointer. Use it for lookups, current/active/selected state, backend handles that can be absent, and API results where absence is a normal outcome. **Keep a checked nullable as `nptr` and dereference it directly** — do not introduce a `nullable_x` copy and narrow it to `ptr` just to call through it. `nptr` has `operator->` / `operator*`, an unchecked deref exactly like `ptr`, so after a guard the direct deref is free and the clean domain name stays on the `nptr` itself: ```cpp nptr target = engine->GetCritter(id); // clean domain name stays on the nptr if (!target) { return; // guard proves non-null for the rest of the scope } target->Foo(); // deref the nptr directly — no as_ptr(), no copy ``` The guard may be an early `if (!target) { ; return; }`, a positive `if (target) { ... }` / short-circuit `target && target->Foo()`, an `if (nptr target = expr) { target->... }` if-init, a `for (nptr n = ...; n; ...)` loop condition, or an exit-on-fail assertion `FO_VERIFY_AND_THROW(target, "...")` / `FO_STRONG_ASSERT(target, "...")` — each proves non-null, so an `as_ptr()` there would only add a redundant assert. In verify/assert conditions, write pointer truthiness directly (`target`, `size == 0 || data`), not through a double-negation expression; when an invariant really compares presence to another boolean, compute that boolean explicitly (`static_cast(target)` for wrappers, `raw != nullptr` for raw pointers). An embedding project can enforce this rule across the Engine and its native extensions with a guard-aware source analyzer. The Engine repository currently owns the rule and wrapper/runtime tests, but does not ship that analyzer; a project-side `NullableLocalDereference` check must fix every hit at the source (add the guard/`verify`, or narrow the producer to `ptr`) instead of hiding it behind a count baseline. **A checked `nptr` narrows to `ptr` implicitly — do not spell `.as_ptr()` at a `ptr` site.** `nptr` converts to `ptr` through an implicit converting constructor that asserts non-null at the conversion point (the same always-on assert `.as_ptr()` runs), so a guard-checked nptr flows straight into a `ptr` parameter, member, or return with no ceremony: ```cpp nptr map = EntityMngr.GetMap(map_id); // clean domain name on the nptr if (!map) { return; // guard proves non-null } FindPath(map, cr, from_hex, to_hex); // FindPath takes ptr — implicit narrow, no .as_ptr() ``` The conversion is const-propagating and mutability-preserving (a `const nptr` yields only `ptr`, never a mutable `ptr`). When a nullable source is named in a deduced local and the following code requires presence, keep the nullable wrapper itself, then check it explicitly before deref or implicit `ptr` use. Do not rely on a hidden `.as_ptr()` assertion for this: ```cpp auto target = engine->GetCritter(id); FO_VERIFY_AND_THROW(target, "Target critter not found"); target->Foo(); // checked nptr, direct deref UseTarget(target); // UseTarget takes ptr ``` Explicit `.as_ptr()` / `.as_nptr()` conversions are also valid when they make the borrow visible, resolve overload or template deduction, materialize a value for use after an owner is moved, or create a copyable lambda capture. Implicit conversion remains available when the destination `ptr` / `nptr` type is already unambiguous. When the source is a raw `T*`, use the global helper and keep the local declaration deduced: ```cpp auto target = make_ptr(raw_target); // raw T* -> ptr, asserts non-null auto maybe_target = make_nptr(raw_target); // raw T* -> nptr, preserves null ``` Owning wrappers borrow the same way: `refcount_ptr` / `refcount_nptr`, `unique_ptr` / `unique_nptr`, `unique_del_ptr` / `unique_del_nptr`, `unique_arr_ptr`, and `shared_ptr` all flow implicitly to `ptr` / `nptr` borrow sites through `get()`. The `ptr` conversion asserts non-null at the conversion point; the `nptr` conversion preserves absence. These conversions never transfer ownership. At call/typed-return/member sites with a known destination type, pass the owner directly. For local access, use the source owner itself or an `auto&` alias when no borrow type is required. When deduction, owner movement, or a lambda capture genuinely requires a borrowed value, materialize it as `auto borrow = owner.as_ptr();` / `auto maybe_borrow = owner.as_nptr();` and validate nullable state before dereference. If a nullable owner was just assigned from a factory with a reviewed non-null result and only a few member accesses follow, work with the source owner directly instead of creating a temporary borrowed local and re-checking it. When a dynamic cast is the only reason for a temporary borrow, call `dyn_cast()` on the owner directly: write `_views[i].dyn_cast()`, not `_views[i].as_ptr().dyn_cast()`. `unique_ptr` / `unique_nptr` owner casts return a borrowed `nptr`. `refcount_ptr` / `refcount_nptr` keep the existing owning result (`refcount_nptr`) when `U` is itself intrusive-refcountable, and return borrowed `nptr` for non-refcountable mixin/interface targets such as view interfaces. If ownership is intentionally acquired from a borrow, keep spelling that explicitly with `hold_ref()` / `try_hold_ref()` or `require_refcount_ptr(...)`. Reverse transitions from borrowed wrappers to owners remain explicit and reviewed: use `hold_ref()` / `try_hold_ref()` for intrusive refs, `adopt_unique_ptr(ptr)` for scalar unique adoption, `make_unique_del_ptr(...)` for custom-deleter adoption, `take_not_null()` for nullable-owner ownership narrowing, and `safe_alloc::make_shared(...)` / domain factories for shared ownership. There is no implicit `ptr` / `nptr` → owner conversion. When a narrowed `ptr` genuinely coexists with the nullable in one scope, name them by role so the two never collide — the clean domain name goes to the value the body works with, and boundary values exist only to be checked or narrowed: | Type | local / parameter (`snake_case`) | data member (`_camelCase`) | |---|---|---| | raw `T*` | `raw_target` | `_rawTarget` | | `nptr` narrowed away to a coexisting `ptr` | `maybe_target` / domain name | `_maybeTarget` / domain name | | `ptr` (non-null, narrowed) | `target` | `_target` | ```cpp // raw C-ABI pointer -> checked non-null static void Cleanup(sentry_options_t* raw_options) noexcept { if (raw_options != nullptr) { // check the raw pointer directly ptr options = raw_options; // clean name for the non-null ptr sentry_options_free(options.get()); } } ``` Because the common case now keeps the clean name on the checked `nptr` itself, do **not** introduce `nullable_` locals as a narrowing ritual. Do **not** invent per-call disambiguators such as `_lookup`, `_ref`, `_ptr`, or `_begin`, and **never copy an already-named nullable or raw pointer into an intermediate just to wear a prefix**. Bind a raw pointer straight to `ptr` after the `!= nullptr` check — do not route it through an `nptr` intermediate just to null-check it. When a value is **provably non-null** — `std::string::data()` / `string_view::data()` (never null), `&object`, or an already-non-null result — bind it **straight to `ptr` with no check and no `nptr` intermediate**; adding a `!= nullptr` test there is dead code. And do not introduce *any* wrapper for a method-local pointer that never escapes the function and is used only for local address arithmetic — a `ptr` (or a plain raw pointer for pure pointer math) is enough: ```cpp ptr view_begin = _sv.data(); // data() is never null — no nptr, no check ptr storage_begin = _s.data(); ptr storage_end = storage_begin.get() + _s.size(); if (view_begin < storage_begin || !(view_begin < storage_end)) { ... } ``` One exception: an **exported/ABI parameter whose name is fixed by an external contract** (or pinned in the smart-pointer-audit allowlist) keeps its name; narrow it to a `_ptr` local rather than renaming the parameter. The same naming applies to every narrowing form (`unique_nptr::take_not_null()`, `refcount_nptr::take_not_null()`, custom-deleter `take_not_null(...)`, …). `unique_ptr` owns one object in usable state. A moved-from object may be empty only until destruction or reassignment. If empty state is part of the normal object model, use `unique_nptr`. `unique_nptr` owns zero or one object. Use it for lazily-created resources, optional owned state, and objects that are created after the owner is constructed or cleared before the owner is destroyed. Use `unique_nptr::take_not_null()` when a nullable owner has been checked or otherwise proved present and ownership must move into a `unique_ptr`. The method asserts the non-null invariant and makes the narrowing visually reviewable. Use `take_not_null(unique_del_nptr&)` for the matching custom-deleter case when a checked nullable custom-deleter owner must move into `unique_del_ptr`. The helper keeps the stored deleter with the transferred owner, so call sites do not need to manually release raw storage or rebuild deleters. In strict builds, `unique_ptr::release()` and `unique_del_ptr::release()` return `ptr` instead of raw `T*`. `unique_nptr::release()` and `unique_del_nptr::release()` return `nptr` or raw ABI storage for the custom-deleter nullable owner. Unwrap with `.get()` only at an explicit ABI, allocator, or adoption boundary. Helpers that can fail a type cast or lookup while transferring unique ownership must return `unique_nptr`, because a failed cast is normal absence, not a valid `unique_ptr` state. `unique_del_ptr` / `unique_del_nptr` are custom-deleter owners used at external cleanup boundaries. Use `unique_del_ptr` only after a `ptr` or equivalent assertion proves the custom-owned object is present; it is a strict move-only non-null owner and rejects default/null construction in strict builds. Use the `unique_del_nptr` spelling for stored state that can be empty, lazily initialized, moved-from, or reset. When the custom deleter owns a typed C array or buffer, index the owner directly (`owner[index]`) instead of creating a borrowed-pointer temporary; `void` owners deliberately do not expose indexing. Opaque C resources declared as `void` may be owned directly as `unique_del_ptr` / `unique_del_nptr` when their cleanup function accepts the corresponding `void*`; do not introduce a C++ holder object merely to carry such a handle. `refcount_ptr` owns an intrusive reference and is non-null in usable state. Copying increments the reference count; destruction decrements it. `refcount_nptr` owns an intrusive reference when present and can be empty. Use it for optional entity/view/current state and lookup results that need to keep the object alive when found. Borrow `refcount_ptr` and `refcount_nptr` through the implicit owner→`ptr` / owner→`nptr` conversions. Use `refcount_nptr::take_not_null()` when a nullable intrusive owner has been checked or otherwise proved present and ownership must move into a `refcount_ptr`. The ownership-narrowing method asserts the non-null invariant and adopts the already-held reference without adding another one. Do not wrap ordinary nullable intrusive ownership in `optional>`; keep normal absence in `refcount_nptr`. A load-result contract that must distinguish "absent" from "error" returns `refcount_nptr` **plus a separate error flag** (e.g. a `bool& is_error` out-parameter), not `optional>`. The same rule applies to other wrappers: avoid `optional>`, `optional>`, `optional>`, `optional>`, and `optional>`; use the direct wrapper vocabulary or a named domain result type. `shared_ptr` and `weak_ptr` are engine-own shared-ownership types (no `std::shared_ptr` inside): an atomic control block owns the object through the strong count and the block itself through the weak count, the object is embedded in the same allocation by `safe_alloc::make_shared()` (which also honors the OOM backup pool), and destruction goes through a virtual hook so holders never need the complete pointee type. Types that need `shared_from_this()` / `weak_from_this()` derive from the engine-own `enable_shared_from_this`; the factory wires the embedded weak reference right after construction, so like `std::enable_shared_from_this` it is not usable inside the constructor. Casts are member methods: `shared_ptr::dyn_cast()` (dynamic cast sharing the same control block, empty on failure) and `shared_ptr::cast_no_const()` (const-stripping escape hatch, the shared-owner sibling of `get_no_const()`). A present `shared_ptr` borrows implicitly to `ptr`; a possibly-empty one borrows implicitly to `nptr`. These borrows do not change shared ownership; keep a shared owner alive for the full borrowed use. `unique_arr_ptr` and `unique_del_nptr` are likewise engine-own owners (array `delete[]` and type-erased `function` deleter respectively); `unique_del_nptr::get_deleter()` exposes the stored deleter for reviewed ownership handoffs such as `take_not_null()`. Class and struct state must not store C++ reference members (`T& _member` or `const T& _member`). Constructor parameters and short local aliases may still use references when that is the clearest borrow, but stored required borrowed dependencies must be reviewed non-null `ptr` members. Stored optional dependencies should use the matching nullable wrapper (`nptr`, `unique_nptr`, or `refcount_nptr`). Use value members only for data the object actually owns by value, not as a workaround for a borrowed dependency. Process/runtime command-line arguments enter through platform or DLL ABI `argc` / `argv` signatures, then immediately move into the wrapper vocabulary. Use `CommandLineArg` / `CommandLineArgs` for internal argument views; build any temporary `char**` array only at a final ABI handoff such as the client-runtime export call. A non-SDL entry point owns them through `ProgramArgs` (`Source/Common/Common.h`), which obtains UTF-8 text from the wide Windows command line instead of ANSI-code-page `argv` and lends a view for its lifetime. When a reviewed raw cleanup boundary must adopt object storage, route it through a named helper instead of constructing owners directly from `.get()`. Use `adopt_unique_ptr(ptr)` for scalar object cleanup and `make_unique_del_ptr(ptr, deleter)` / `make_unique_del_ptr(nptr, deleter)` for custom-deleter holders; the `ptr` overload returns `unique_del_ptr`, and the `nptr` overload returns `unique_del_nptr`. Domain-specific helpers may wrap these primitives, but call sites should not spell `unique_ptr {value.get()}` or `unique_del_ptr {value.get(), deleter}` directly. For a low-level byte/ABI reinterpret of the pointee type (`ucolor` <-> `uint8_t`, `void` -> `T`, etc.), use `ptr::reinterpret_as()` / `nptr::reinterpret_as()` rather than a raw `void*` roundtrip. It returns `ptr` / `nptr`, propagates the source pointee's `const` (so it can never silently strip `const`), and accepts a `ptr` source (as `GetPtrAs()` does). `ptr::void_cast()` / `nptr::void_cast()` are one-way handoff helpers for C/ABI surfaces: they return a raw `void*` directly. Recover a **concrete** typed nullable borrow from that opaque pointer with `cast_from_void(void_ptr)`, which returns `nptr` and uses `static_cast` internally. Its `T*` spelling must have a non-void ultimate pointee. A **void-of-void** indirection (`void**`, `const void* const*`, ...) — the AngelScript handle-slot plumbing in `ScriptSystem.h` and the script-call bridges — is not a concrete recovery, so reinterpret the *wrapper* with `ptr::reinterpret_as()` / `nptr::reinterpret_as()` (dereference with `*` to read the stored handle). If the source is already a wrapper, call `reinterpret_as()` directly on it rather than wrapping it again (`mem.reinterpret_as()`, not `ptr {mem}.reinterpret_as()`). A concrete-to-concrete storage reinterpret (e.g. a `unique_del_ptr` holding a typed object) likewise goes through an explicit deduced borrow: `auto storage = owner.as_ptr(); auto object = storage.reinterpret_as();`. If the source is a raw pointer rather than a wrapper, use `auto storage = make_ptr(raw_storage);` / `auto storage = make_nptr(raw_storage);`. For reinterpreting a mutable **byte span** as a typed value span, use the shared `bytes_to_objects(span)` helper (`CommonHelpers.h`) — it reinterprets the whole byte span (asserting the size is a whole multiple of `sizeof(T)`) into a `span` through `reinterpret_as`; take a subrange with `span::first`/`subspan` on the input or result when fewer elements are wanted. The inverse `object_to_bytes(T&)` exposes one object as its `span`. To build a typed **element span** from a borrowed pointer and a length, use `make_span(ptr, length) -> span` / `make_const_span(ptr, length) -> const_span` (`CommonHelpers.h`) instead of constructing `span{p.get(), length}` — they keep the `.get()` unwrap inside the helper. `make_const_span` also has a raw `const T*` overload for container-data sources (`make_const_span(vec.data(), n)`). (The other `make_span(...)` overloads take a *byte* size and return a `span` view; the `ptr` overload counts elements.) For a `ptr` / `ptr` holding text, `as_str(size_t len) -> string_view` builds the view directly rather than `string_view{p.get(), len}`. ### Always-on non-null enforcement The non-null invariant is enforced by an **always-on** runtime check, in every build configuration — not a debug-only `assert` that release strips. Constructing a `ptr` from a null raw pointer (the `ptr(T*)` ctor), every nullable/owner→`ptr` borrow (`nptr`, `refcount_nptr`, `unique_nptr`, `unique_del_nptr`, `shared_ptr`, …), and every ownership narrow (`unique_nptr::take_not_null()`, `refcount_nptr::take_not_null()`, custom-deleter `take_not_null(...)`, …) assert the invariant and, on violation, synchronously report the expression, source location, and native stack before terminating. The check is spelled `FO_BASIC_STRONG_ASSERT(expr)` and is owned entirely by `Essentials/FatalError.h` / `.cpp`, which sits after `StackTrace` / `BaseLogging` and before `SmartPointers` in the strict Essentials cascade. `SmartPointers` therefore depends only upward; it does not reach the later `ExceptionHandling` module. The `noexcept` reporter uses the early native fatal path rather than constructing `StrongAssertationException`, so it is safe from `noexcept` members. Any Essentials module after `FatalError` may use the same primitive; code at or below `BasicCore`, `GlobalData`, `StackTrace`, or `BaseLogging` must use a failure mechanism available in its own layer. Consequence: a null wrapped in `ptr` fails fast at the construction site even in release. The two common sources are (1) an empty container's `.data()` — `std::vector` (MSVC) / `std::string_view` / `std::span` return null when empty (`std::string` / `std::array` never do), and (2) a raw API that returns null on "not found" (`GetProcAddress`, `dlsym`, `GetModuleHandle(nullptr)` for the main module, …). Keep genuinely-nullable values in `nptr`, or — for a transient buffer pointer used once — pass `X.data()` straight into a consumer that takes `nptr`/raw instead of building an intermediate `ptr`. ## Refcount bridges Raw or borrowed pointer to intrusive-refcount ownership must be explicit. The target API is: ```cpp refcount_ptr held = entity_ptr.hold_ref(); refcount_nptr maybe_held = maybe_entity.try_hold_ref(); ptr borrowed = held; nptr maybe_borrowed = maybe_held; ``` When a raw pointer is unavoidable at an ABI, atomic, or allocator boundary, use the named refcount factories instead of direct construction: ```cpp refcount_ptr held_from_raw = refcount_ptr::from_add_ref(raw_entity); refcount_nptr maybe_held_from_raw = refcount_ptr::try_from_add_ref(raw_entity); refcount_ptr adopted = refcount_ptr::from_adopted_ref(raw_entity_with_existing_ref); ``` Direct `refcount_ptr(T*)`, `operator=(T*)`, and public `adopt_tag` construction are unavailable; engine/project code uses the named methods so raw/refcount transitions are visually reviewable. ## Non-null and explicit-bridge contract These rules are the unconditional behavior of the smart-pointer types. (They were previously gated by the `FO_STRICT_*` migration flags; once the migration completed the flags were removed and the strict behavior became the only behavior — there is no permissive mode.) - Direct raw `refcount_ptr(T*)`, `operator=(T*)`, and public `adopt_tag` construction are unavailable. Use the named factories above. - `ptr` has no default/null construction, `nullptr` assignment/comparison, `operator bool`, `get_pp()`, or `reset()` without a replacement pointer. - `unique_ptr` / `unique_del_ptr` / `refcount_ptr` have no default/null construction, `nullptr` assignment/comparison, `operator bool`, or default-null `reset()`; `unique_ptr::release()` and `unique_del_ptr::release()` return `ptr`, while nullable-owner `release()` methods keep their nullable/raw ABI return shape. Any two wrapper values (borrow or owner, same or different kind) compare directly with `==` / `!=` through a heterogeneous free `operator==` that forwards to the stored pointers — write `item == other_item` rather than unwrapping both sides with `.get()`. Project-side analyzers may reject `a.get() == b.get()` as a `DirectWrapperGetComparison` style violation, but the wrapper operators and `Test_SmartPointers.cpp` are the Engine-owned contract. New code must be valid under this contract; the nullable siblings (`nptr`, `unique_nptr`, `refcount_nptr`) exist for the genuinely-optional cases. ## Raw pointer allowlist Do not force wrappers into places where raw pointers are the clearer ABI or low-level representation: - `void*` and byte buffers with an adjacent size. - Allocator internals, placement new/delete, and pointer arithmetic. - C, OS, graphics, COM-style, and third-party ABI surfaces. - Process/runtime `argc` / `argv` entrypoints and final `char**` handoffs to compatible runtime modules. - AngelScript generic API plumbing, generated registration strings, handle-slot storage, and low-level `char*` / `char**` buffers and process argv. Script-visible export/event/hook handle signatures use `ptr` / `nptr`; see "Script binding boundary" below. - `std::atomic` until a dedicated atomic nullable wrapper exists. For engine-owned APIs outside these categories, prefer `ptr` or `nptr` for borrowed values and `unique_*` / `refcount_*` for owned values. ## Script binding boundary (`FO_SCRIPT_API`) `///@ ExportMethod` script bindings are generated by `BuildTools/codegen.py`, and every script-visible handle pointer in the generated ABI is spelled with `ptr` / `nptr` (including `///@ ExportEvent`, `FO_ENTITY_EVENT`, `///@ ExportRefType`, and `///@ EngineHook` declarations). Codegen rejects bare raw handle pointers in exported/event signatures, and `NativeDataProvider` / `NativeDataCaller` static-assert if a raw handle pointer reaches the native marshalling templates. - The engine/entity **receiver** (first parameter) is always non-null, so it is `ptr` / `ptr`. - A non-null entity/ref argument or return is `ptr`; a nullable one is `nptr`. Bare `T*` no longer carries a nullable contract at the export/event boundary; spell the contract explicitly with the wrapper. - **Container element types** use the wrapper vocabulary too: returns such as `vector>` and parameters such as `readonly_vector>` / `readonly_vector>` are the supported handle-container shapes. Codegen reduces the element to the raw handle only for the AngelScript metadata (the registered `T@[]` signature and the compatibility hash are unchanged), while the generated extern and `NativeCall` cast keep the wrapper element type so the glue matches the C++ signature. - `///@ ExportRefType` methods (e.g. the dialog accessors) likewise use `ptr` / `nptr` for handle returns and generated `ptr` receivers. The script `.Ret` metadata still reduces to the handle type, so the registered script signature and compatibility hash are unchanged. - The script-facing AngelScript registration is unchanged, so no `.fos`, bytecode, or save migration is required; only the C++ glue type changes. The client/server compatibility hash deliberately excludes the wrapper spelling. These script-binding shapes intentionally stay raw at the ABI edge: - Generic AngelScript plumbing, generated registration strings, handle slots, and `char*` / `char**` buffers and process argv. - Third-party/native C callback surfaces that are not script handle contracts. Bind those raw inputs to `ptr` / `nptr` at the first engine-owned line with `make_ptr` / `make_nptr`. Inside an export body, bind any remaining raw input to a wrapper before ordinary engine work and unwrap (`.get()`) only at the final ABI/handoff line. When a function's declared **return type is itself a raw pointer** (a `void*`/`T*` handed to a C or script API — SDL/ImGui/Spine allocators, script element accessors, generic readers), `return wrapper.get();` is allowed: the audit reads the function's return type (raw pointer, deduced `auto`, or a bare template parameter `-> T`) and treats such a `.get()` return as a genuine raw-pointer boundary rather than a wrapper unwrap. A `.get()` return whose enclosing function returns a **wrapper** (`nptr`/`ptr`) is still flagged — return the wrapper (`return x;`) or bind a named wrapper local. Direct `get_no_const()` is likewise allowed at the final mutable script ABI edge; no one-line `ScriptMutablePtr` / `Return*` bridge is required. A direct placement-new pointer write to `GetAddressOfReturnLocation()` is also a final AngelScript ABI handoff. ## Migration rules 1. Replace legacy `raw_ptr` call sites with `ptr` without changing behavior. 2. Replace legacy `nullable_raw_ptr` call sites with `nptr`. 3. Convert already-empty `ptr ... {}` declarations to `nptr`. 4. Convert already-empty `unique_ptr ... {}` declarations to `unique_nptr`. 5. Convert already-empty `refcount_ptr ... {}` declarations to `refcount_nptr`. 6. Review lookup APIs, current-state fields, and script/third-party boundary adapters before changing stricter contracts. 7. Let checked `nptr` values flow directly to `ptr` sites; use `auto borrow = value.as_ptr()` only when a distinct borrowed value is genuinely needed. 8. Use `unique_nptr::take_not_null()` when optional unique ownership is explicitly narrowed back to `unique_ptr`; use `take_not_null(unique_del_nptr&)` for the same checked narrowing into `unique_del_ptr`. 9. Let `refcount_ptr` / `refcount_nptr` owners borrow implicitly to `ptr` / `nptr`; use `refcount_nptr::take_not_null()` only when optional intrusive-refcount ownership is explicitly narrowed back to `refcount_ptr`. 10. Call owner `dyn_cast()` directly instead of spelling an intermediate `.as_ptr().dyn_cast()`; use explicit ownership helpers only when the cast result must acquire/retain ownership. 11. Change functions that return `nullptr` or `{}` from `ptr`, `unique_ptr`, or `refcount_ptr` to the matching nullable return type unless absence is made impossible. 12. Do not reintroduce the removed permissive `FO_STRICT_*` modes. When integrating an older branch, migrate optional state to the nullable wrappers before adopting the current header. ## Validation Use grep gates during the staged migration: ```powershell rg "\braw_ptr<" Source SourceExt rg "^\s*(mutable\s+)?ptr<[^\n;]+>\s+\w+[^;{}]*\{\}\s*;" Source SourceExt rg "^\s*(mutable\s+)?unique_ptr<[^\n;]+>\s+\w+[^;{}]*\{\}\s*;" Source SourceExt rg "^\s*(mutable\s+)?refcount_ptr<[^\n;]+>\s+\w+[^;{}]*\{\}\s*;" Source SourceExt ``` Run the embedding project's generated engine unit-test target after pointer-layer changes. If native script bindings or exported method signatures are touched, also run the script compilation/baking validation used by that project. The Engine repository does not currently ship a whole-tree textual or AST smart-pointer policy checker. Embedding projects can keep lightweight migration guards in their own source control, but those commands, task names, baselines, and allowlists are project-owned and are not normative Engine evidence. A project-side checker should use exact line-level allowlists for reviewed ABI and low-level raw-pointer boundaries instead of count budgets. It should reject class reference members and unguarded nullable-local dereferences directly, distinguish nullable-owner inventory from violations, and use a real `compile_commands.json` for any AST-backed declaration check. Keep those implementation details in the embedding project's documentation; move a checker into `BuildTools/` with Engine-owned tests before advertising one here as a standard command. ===== END DOCUMENT smart-pointers ===== ===== BEGIN DOCUMENT thread-safety-analysis ===== Source: Docs/en/contributing/coding-contracts/thread-safety-analysis.md Canonical URL: https://fonline.ru/Docs/en/contributing/coding-contracts/thread-safety-analysis.html Content SHA-256: b92dd0d64ee820868b29fc958040c34b8003dcfd043fd70da65a842de955dc29 --- layout: default title: Thread Safety Analysis locale: en document_id: thread-safety-analysis permalink: /Docs/en/contributing/coding-contracts/thread-safety-analysis.html --- # Thread Safety Analysis The engine annotates its conventional mutexes with [Clang Thread Safety Analysis](https://clang.llvm.org/docs/ThreadSafetyAnalysis.html) (TSA) so that lock misuse — touching guarded state without the lock, forgetting a required capability, returning while still holding a lock — is a **compile-time error** on every Clang toolchain. It is static, zero-cost, and complements (does not replace) runtime checks and concurrency tests. > TSA is a defense-in-depth layer for **conventional, lexically-scoped mutexes**. It deliberately does **not** model > cooperative / dynamically-acquired lock schemes (see *Exemptions*). > For exception-safety of lock acquisition — why an `EntityLock` post-grant invariant uses `FO_STRONG_ASSERT` (a > waiter that woke non-aborted must be `GRANTED` and own the lock, or the process must stop) — see > [ExceptionSafety.md](exception-safety.md). ## Toolchain & enforcement - Enabled in `BuildTools/cmake/stages/Init.cmake` for every Clang compiler id (native `clang`, `clang-cl`, AppleClang, Emscripten, Android NDK) as `-Wthread-safety -Werror=thread-safety` (routed through `/clang:` under the cl-style `clang-cl` driver). MSVC and GCC do not implement TSA, so the analysis never runs there — the **Clang build is the authoritative gate**. - The `FO_TSA_*` macros expand to `__attribute__((...))` only under `__clang__`; on every other compiler they are no-ops, so annotated code builds unchanged on MSVC/GCC. - Third-party libraries are silenced by `DisableLibWarnings` (`-w` plus `-Wno-error=` for the Clang-20+ legacy-C default-errors), so TSA only ever evaluates first-party engine + embedding-project code. ## Why the `fo::` mutex wrappers exist The project links the platform STL (libc++ is disabled), and **neither MS STL nor libstdc++ annotates `std::mutex` / `std::shared_mutex` as capabilities**. `FO_TSA_GUARDED_BY(std_mutex_member)` would therefore emit `'guarded_by' attribute requires arguments whose type is annotated with 'capability'` and check nothing. So guarded state must be protected with the engine's own annotated primitives (defined in `Source/Essentials/Threading.h`). They are **drop-in replacements for the std analogues** — same names in snake_case, same method names — so a lock site is a plain `std::` → `fo::` swap: | Type | Wraps / mirrors | Use for | |------|-----------------|---------| | `fo::mutex` | `std::mutex` | exclusive-only state | | `fo::shared_mutex` | `std::shared_mutex` | reader/writer state | | `fo::atomic_mutex` | atomic-state park/wake | short critical sections reached from `noexcept` code, where an OS mutex acquisition failure would have no reporting path | | `fo::scoped_lock` | `std::scoped_lock` / `std::lock_guard` | exclusive RAII guard for `mutex` *or* `shared_mutex` (CTAD: `scoped_lock lk {m}`) | | `fo::shared_lock` | `std::shared_lock` | shared (reader) RAII guard for `shared_mutex` | | `fo::unique_lock` | `std::unique_lock` | exclusive guard with manual `lock()`/`unlock()`, usable with `std::condition_variable_any` | `std::scoped_lock` / `std::unique_lock` / `std::shared_lock` are opaque to the analyzer under the platform STL, so the `fo::` guards must be used at lock sites for the analysis to track them. Engine code lives inside `FO_BEGIN_NAMESPACE`, so the names are used unqualified (`mutex`, `scoped_lock`, …). Include `Threading.h` where you use them (it sits low in the Essentials layer, just above `HashedString`, so even low-layer headers can include it). Condition variables use `std::condition_variable_any` (it accepts any lockable, including `fo::unique_lock`); pass the guard directly to `wait(...)`: ```cpp unique_lock lock {_dataLocker}; _workSignal.wait(lock, [this]() FO_TSA_REQUIRES(_dataLocker) { return _ready; }); ``` ## Annotating a mutex 1. Make the mutex a `mutex` / `shared_mutex`, declared **before** the fields it guards. 2. `FO_TSA_GUARDED_BY(_locker)` on every data member protected by it. 3. At lock sites (a plain `std::` → `fo::` swap): `std::scoped_lock`/`std::lock_guard` → `scoped_lock`; `std::shared_lock` → `shared_lock`; a plain exclusive `std::unique_lock` → `scoped_lock`; a `std::unique_lock` used with a condition variable or manual relock → `unique_lock` (and switch the cv to `std::condition_variable_any`). Declare guards with brace initialization uniformly: `scoped_lock lock {mutex};`, `shared_lock lock {mutex};`. 4. Private helpers that assume the lock is already held: `FO_TSA_REQUIRES(_locker)` (exclusive) or `FO_TSA_REQUIRES_SHARED(_locker)` (read) on the declaration. 5. Hand-rolled RAII guards: mark the class `FO_TSA_SCOPED_CAPABILITY`, the ctor `FO_TSA_ACQUIRE(mutex)`, the dtor `FO_TSA_RELEASE()`; a move ctor (if any) needs `FO_TSA_NO_ANALYSIS`. > Attribute placement: never put an attribute between `()` and a trailing `-> type`. Use a leading return type on > annotated methods (e.g. `bool try_lock() FO_TSA_TRY_ACQUIRE(true)`). > > `fo::thread` (the pool task handle, `threading::run_thread`'s return) also lives in `Threading.h` in the `fo` > namespace. ## Macro vocabulary `FO_TSA_CAPABILITY(name)`, `FO_TSA_SCOPED_CAPABILITY`, `FO_TSA_GUARDED_BY(x)`, `FO_TSA_PT_GUARDED_BY(x)`, `FO_TSA_ACQUIRED_BEFORE(...)`, `FO_TSA_ACQUIRED_AFTER(...)`, `FO_TSA_REQUIRES(...)`, `FO_TSA_REQUIRES_SHARED(...)`, `FO_TSA_ACQUIRE(...)`, `FO_TSA_ACQUIRE_SHARED(...)`, `FO_TSA_RELEASE(...)`, `FO_TSA_RELEASE_SHARED(...)`, `FO_TSA_RELEASE_GENERIC(...)`, `FO_TSA_TRY_ACQUIRE(...)`, `FO_TSA_TRY_ACQUIRE_SHARED(...)`, `FO_TSA_EXCLUDES(...)`, `FO_TSA_ASSERT_CAPABILITY(x)`, `FO_TSA_ASSERT_SHARED_CAPABILITY(x)`, `FO_TSA_RETURN_CAPABILITY(x)`, `FO_TSA_NO_ANALYSIS`. ## Exemptions (what TSA does NOT cover) - **`std::recursive_mutex`** — re-entrant acquisition cannot be modeled. Recursive locks stay raw `std::recursive_mutex` with no `GUARDED_BY`; mark them `// recursive: not modelable by TSA`. - **Single-threaded init/teardown** that sweeps guarded state while re-entering the locking code (so it cannot hold the lock) — mark the function (and any inner lambda separately, as a lambda is its own analysis scope) `FO_TSA_NO_ANALYSIS` with a comment stating why it is single-threaded. Use sparingly; never to hide a real race. - **Cooperative / dynamically-acquired lock schemes** whose held set is data-dependent and acquired non-lexically cannot be expressed; leave them unannotated (they reference no capability, so they need no escape hatch) and document the exemption in the owning subsystem doc. Embedding projects document their own guarded-field inventory and project-specific exemptions in their threading docs; this file owns only the reusable mechanism. ===== END DOCUMENT thread-safety-analysis ===== ===== BEGIN DOCUMENT api-change-management ===== Source: Docs/en/contributing/contract-change-management.md Canonical URL: https://fonline.ru/Docs/en/contributing/contract-change-management.html Content SHA-256: 36c2126f621391f6b8472a6ad6c7e119febc14f381ec276525488819b8d46526 --- layout: default title: Generated Contract Change Management locale: en document_id: api-change-management permalink: /Docs/en/contributing/contract-change-management.html --- # Generated Contract Change Management > Engine-owned maintainer guide. Use this page to compare the generated native API, CMake, main/helper BuildTools CLI, package, native-extension, prototype-format, map-format, model-format, text-format, effect-format, image-format, particle-format, font-format, audio, video, and AiControl protocol contracts across revisions and to dispose compatibility-sensitive changes before merge. ## Purpose FOnline publishes seventeen deterministic machine-readable models under `Docs/generated/`. `BuildTools/docs_contract_diff.py` compares all seventeen with the same models at a base revision and produces one JSON report for automation plus one Markdown report for review. The gate answers four separate questions: 1. Which domain and stable entry changed? 2. Is the change additive, documentation-only, policy-only, or structurally breaking? 3. Did the baseline revision promise compatibility for that entry or domain? 4. If review is required, where are the owner decision, migration, release-note, and compatibility dispositions? The comparator reports internal churn but does not promote an internal surface to public API. Stability remains source-owned: native symbols use `///@ ApiContract`; CMake, main CLI, package, helper CLI, native-extension, prototype-format, map-format, model-format, text-format, effect-format, image-format, particle-format, font-format, audio, video, and AiControl protocol models use their declared domain or entry stability. Classification and stability are separate stages. First classify the observed shape/prose/policy change as additive, documentation, policy, or breaking; then use baseline stability to decide whether a reviewed disposition is required. The ledger records the maintainer's disposition and never performs automatic classification. Neither `--write` nor `--enforce` authors migration, release-note, compatibility, or owner decisions. ## Disposition decision Compare normalized machine models by stable IDs, and retain each model's `contract_sha256` plus source provenance so formatting or ordering noise cannot masquerade as a compatibility change. For every review-required entry, record a stable `change_id` and complete the disposition ledger fields `migration`, `release_note`, `compatibility`, and `owner` before merge. The generated diff finds changes; it does not make the owner's migration or release decision. ## Covered domains | Domain | Canonical model | Entry matching | Current enforcement | | --- | --- | --- | --- | | Native API | [generated/api.json](../../generated/api.json) | Native symbol `id`; overloads retain their signature-hashed IDs | Baseline `stable`, `experimental`, and `deprecated` breaks require disposition; internal breaks remain visible | | CMake | [generated/cmake.json](../../generated/cmake.json) | `cmake.option.*`, `cmake.stage.*`, and `cmake.helper.*` IDs | The domain is `experimental`, so removals and shape changes require disposition | | BuildTools CLI | [generated/cli.json](../../generated/cli.json) | Command and argument `cli.*` IDs | The domain is `internal`; breaking changes are reported but do not create a compatibility promise | | Package | [generated/package.json](../../generated/package.json) | Declaration, option, target, platform, pack, payload, and argument `package.*` IDs | The domain is `internal`; breaking changes are reported but do not create a compatibility promise | | Helper CLI | [generated/helper-cli.json](../../generated/helper-cli.json) | Helper, subcommand, and argument `helper-cli.*` IDs | The domain is `internal`; parser/ownership changes are reported but do not create a compatibility promise | | Native extension | [generated/native-extension.json](../../generated/native-extension.json) | Role, hook, and binding-rule `native-extension.*` IDs | The domain is `experimental`; removals and structural changes require disposition, while binary compatibility between independently built revisions is not promised | | Prototype format | [generated/prototype-format.json](../../generated/prototype-format.json) | Section, directive, rule, built-in entity, and property `prototype-format.*` IDs | Grammar/rules are `experimental` and require disposition when broken; the derived property catalog is `internal` and remains visible without an added compatibility promise | | Map format | [generated/map-format.json](../../generated/map-format.json) | Section, directive, ownership, rule, and property `map-format.*` IDs | Grammar/ownership/rules are `experimental` and require disposition when broken; the revision-derived property catalog is `internal` | | Model format | [generated/model-format.json](../../generated/model-format.json) | Compile limit, asset, token, and rule `model-format.*` IDs | Grammar/composition rules are `experimental` and require disposition when broken; compile limits and derived asset facts remain visible under their declared stability | | Text format | [generated/text-format.json](../../generated/text-format.json) | Syntax, language, prototype-text, runtime, rendering, and validation `text-format.*` IDs | The parser/baker/runtime contract is `experimental`; removals and structural changes require disposition while project language and formatter policy remain outside the model | | Effect format | [generated/effect-format.json](../../generated/effect-format.json) | Compile-limit, section, option, resource, baking, runtime, script-method, and validation `effect-format.*` IDs | The baker/renderer/runtime contract is `experimental`; removals and structural changes require disposition while project shader catalogs, visual policy, and ScriptValue meanings remain outside the model | | Image format | [generated/image-format.json](../../generated/image-format.json) | Source-format, FOFRM field, filename-option, baking, runtime, and validation `image-format.*` IDs | The baker/default-client contract is `experimental`; private container entries are `internal`, while project asset catalogs, licenses, pack precedence, visual policy, and acceptance remain outside the model | | Particle format | [generated/particle-format.json](../../generated/particle-format.json) | Backend/format, registered object/family, XML, renderer, tooling, runtime, integration, and validation `particle-format.*` IDs | The `.spark`/`.spk`, `.efkproj`/`.efk`, and Engine integration contract is `experimental`; derived native-coverage records remain `internal`, while project catalogs, settings, effects, textures, models, budgets, and visual acceptance remain outside the model | | Font format | [generated/font-format.json](../../generated/font-format.json) | Descriptor format/field, binding, layout, rendering, and validation `font-format.*` IDs | The FOFNT/BMFont and client text pipeline contract is `experimental`; cache internals remain `internal`, while project slot assignment, glyph coverage, typography, and visual acceptance remain outside the model | | Audio | [generated/audio.json](../../generated/audio.json) | Format, delivery, decoding, playback, and validation `audio.*` IDs | WAV/Ogg baking, Vorbis runtime decoding, and client playback are `experimental`; documentation/test-gap records remain `internal`, while project catalogs, spatial/music policy, mastering, licensing, and audible acceptance remain outside the model | | Video | [generated/video.json](../../generated/video.json) | Format, delivery, decoding, fullscreen, embedded, and validation `video.*` IDs | Ogg/Theora and client presentation are `experimental`; missing-fixture and loop-risk records stay explicit, while project cinematics, subtitles, policy, assets, provenance, budgets, and visible acceptance remain outside the model | | AiControl protocol | [generated/ai-control-protocol.json](../../generated/ai-control-protocol.json) | Transport, method, command/event, security, integration, and validation `ai-control-protocol.*` IDs | The reusable wire and control protocol is `experimental`; game-specific schemas, actions, administrator tools, and MCP namespaces remain project-owned | Model source, repository/scope, or model-level contract changes are conservative domain breaks and always require disposition. This prevents a comparator or ownership boundary change from silently redefining what the gate covers. Project-authored remote calls remain a separate baked project catalog. Remaining authored file formats, updater behavior, and project configuration keys require their owning model before they can join this comparator. ## Source paths inspected - `BuildTools/docs_api.py` - `BuildTools/docs_api_diff.py` - `BuildTools/docs_cmake.py` - `BuildTools/docs_cli.py` - `BuildTools/HelperCliInterface.json` - `BuildTools/docs_helper_cli.py` - `BuildTools/NativeExtensionInterface.json` - `BuildTools/docs_native_extension.py` - `BuildTools/PrototypeFormatInterface.json` - `BuildTools/docs_prototype_format.py` - `BuildTools/MapFormatInterface.json` - `BuildTools/docs_map_format.py` - `BuildTools/ModelFormatInterface.json` - `BuildTools/docs_model_format.py` - `BuildTools/TextFormatInterface.json` - `BuildTools/docs_text_format.py` - `BuildTools/EffectFormatInterface.json` - `BuildTools/docs_effect_format.py` - `BuildTools/ImageFormatInterface.json` - `BuildTools/docs_image_format.py` - `BuildTools/ParticleFormatInterface.json` - `BuildTools/docs_particle_format.py` - `BuildTools/FontFormatInterface.json` - `BuildTools/docs_font_format.py` - `BuildTools/AudioInterface.json` - `BuildTools/docs_audio.py` - `BuildTools/VideoInterface.json` - `BuildTools/docs_video.py` - `BuildTools/AiControlProtocol.json` - `BuildTools/docs_ai_control_protocol.py` - `BuildTools/docs_package.py` - `BuildTools/docs_contract_diff.py` - `BuildTools/docs_validate.py` - `BuildTools/tests/test_docs_api_diff.py` - `BuildTools/tests/test_docs_contract_diff.py` - `BuildTools/tests/test_docs_helper_cli.py` - `BuildTools/tests/test_docs_native_extension.py` - `BuildTools/tests/test_docs_prototype_format.py` - `BuildTools/tests/test_docs_map_format.py` - `BuildTools/tests/test_docs_model_format.py` - `BuildTools/tests/test_docs_text_format.py` - `BuildTools/tests/test_docs_effect_format.py` - `BuildTools/tests/test_docs_image_format.py` - `BuildTools/tests/test_docs_particle_format.py` - `BuildTools/tests/test_docs_font_format.py` - `BuildTools/tests/test_docs_audio.py` - `BuildTools/tests/test_docs_video.py` - `BuildTools/tests/test_docs_ai_control_protocol.py` - `Docs/generated/api.json` - `Docs/generated/cmake.json` - `Docs/generated/cli.json` - `Docs/generated/helper-cli.json` - `Docs/generated/native-extension.json` - `Docs/generated/prototype-format.json` - `Docs/generated/map-format.json` - `Docs/generated/model-format.json` - `Docs/generated/text-format.json` - `Docs/generated/effect-format.json` - `Docs/generated/image-format.json` - `Docs/generated/particle-format.json` - `Docs/generated/font-format.json` - `Docs/generated/audio.json` - `Docs/generated/video.json` - `Docs/generated/ai-control-protocol.json` - `Docs/generated/package.json` - `Docs/contract-change-dispositions.json` - `Docs/en/contributing/decisions/0002-public-api-stability-contract.md` - `.github/workflows/validate.yml` ## Inputs and outputs The aggregate diff consumes: - one baseline directory or Git revision containing all available canonical models; - the current `Docs/generated/` model directory; - the cumulative `Docs/contract-change-dispositions.json` ledger. It writes revision-pair artifacts under ignored `Workspace/`: - `contract-diff.json` for automation and AI review; - `contract-diff.md` for maintainers. GitHub Actions uploads them as `contract-diff-` for 14 days. They are diagnostic artifacts, not current reference pages, so the GitHub Pages site continues to render only the current Markdown and generated references. ## Stable matching and normalization Each domain compares stable IDs, not display names or JSON array positions. Additions and removals never pair by a similar label. The native comparator retains its symbol-specific behavior: - a non-overloaded signature change normally modifies one symbol; - an overload signature change appears as one removed ID and one added ID under the same `family_id`; - source path and line movement cannot become a false breaking change. The CMake, main CLI, package, helper CLI, native-extension, prototype-format, map-format, model-format, text-format, effect-format, image-format, particle-format, font-format, audio, video, and AiControl protocol comparators flatten their model-owned entry collections by stable ID. Source provenance, including derived enum source paths/line numbers, generated summaries, derived usage strings, and model digests do not create duplicate changes. Nested description/help edits remain documentation changes; defaults, choices, cardinality, required flags, ownership/invocation metadata, platform/target matrices, signatures, stage order, hook fallbacks/call sites, role routing, prototype/map/model/text/effect/image/particle/font/audio/video grammar and resource applicability, compile limits, language normalization, runtime lookup, shader/image/particle/font/audio/video/AiControl runtime behavior, and payload semantics are structural contract data. Each domain records two hashes: - `model_sha256` covers the exact canonical model input; - `contract_sha256` excludes source provenance and descriptive prose while retaining shape and policy. A disposition is bound to the affected domain's baseline and current contract hashes. Unrelated prose or source-line movement cannot invalidate it, while a further contract change does. ## Automatic classifications | Classification | Examples | Merge effect | | --- | --- | --- | | `additive` | New symbol, option, command, argument, platform, pack, or payload entry | Reported; no breaking disposition | | `documentation` | Description, example, summary, notes, or help prose only | Reported; no breaking disposition | | `policy` | Non-withdrawing stability/since/support metadata | Reported; no breaking disposition | | `breaking` | Removal; signature/type/default/required/choice/order/matrix/payload shape change; stability withdrawal | Disposition depends on baseline stability, except model-source/scope changes which always require one | The old revision controls enforcement. A change cannot relabel a baseline-public surface as `internal` while deleting or reshaping it to bypass review. | Baseline stability | Breaking entry change | | --- | --- | | `stable` | Blocked until migration, release, compatibility, and owner handling are recorded | | `experimental` | Blocked until the owner records the change and release/migration disposition | | `deprecated` | Blocked until replacement/removal timing is explicitly disposed | | `internal` | Visible in the report; no compatibility disposition required | ## Local workflow Regenerate and verify every current model first: ```bash python BuildTools/docs_api.py --write python BuildTools/docs_reference.py --write python BuildTools/docs_cmake.py --write python BuildTools/docs_cli.py --write python BuildTools/docs_helper_cli.py --write python BuildTools/docs_native_extension.py --write python BuildTools/docs_prototype_format.py --write python BuildTools/docs_map_format.py --write python BuildTools/docs_model_format.py --write python BuildTools/docs_text_format.py --write python BuildTools/docs_effect_format.py --write python BuildTools/docs_image_format.py --write python BuildTools/docs_particle_format.py --write python BuildTools/docs_font_format.py --write python BuildTools/docs_audio.py --write python BuildTools/docs_video.py --write python BuildTools/docs_ai_control_protocol.py --write python BuildTools/docs_package.py --write ``` Compare with the intended integration base: ```bash python BuildTools/docs_contract_diff.py \ --baseline-git-ref origin/master \ --current-dir Docs/generated \ --dispositions Docs/contract-change-dispositions.json \ --json-output Workspace/contract-diff.json \ --markdown-output Workspace/contract-diff.md \ --write \ --enforce ``` For an explicitly saved local baseline containing all seventeen model files: ```bash python BuildTools/docs_contract_diff.py \ --baseline-dir Workspace/contract-baseline \ --current-dir Docs/generated \ --check \ --enforce ``` `--check` computes and enforces without writing report files. Prefer `--write` while resolving a failure because the Markdown report includes ready-to-fill ledger templates. `BuildTools/docs_api_diff.py` remains available for native-symbol investigation and regression tests. CI enforcement uses the aggregate command so a green API-only report cannot hide CMake, CLI, package, helper-CLI, native-extension, prototype-format, map-format, model-format, text-format, effect-format, image-format, particle-format, font-format, audio, video, or AiControl protocol drift. ## Bootstrap behavior `--allow-missing-baseline` is reserved for the first revision that introduces a canonical model to an existing branch. With a Git baseline, only missing domains enter visible `bootstrap` status; present domains are still compared and enforced. An unknown or unfetched Git revision remains an error. Directory baselines must contain all seventeen models. This avoids accidental partial local comparisons that look complete. After all models have landed, missing baseline files are not normal. Do not add bootstrap mode to local commands or CI changes merely to bypass a failing comparison. ## Disposition ledger `Docs/contract-change-dispositions.json` is a cumulative schema-v2 ledger. Every entry contains: - `domain`: `api`, `cmake`, `cli`, `package`, `helper-cli`, `native-extension`, `prototype-format`, `map-format`, `model-format`, `text-format`, `effect-format`, `image-format`, `particle-format`, `font-format`, `audio`, `video`, `gui-runtime`, or `ai-control-protocol`; - deterministic domain-prefixed `change_id`; - the baseline and current domain `contract_sha256` values; - owner classification: `breaking` or `compatible`; - non-empty rationale, migration, release-note, compatibility, and owner fields. Example: ```json { "domain": "cmake", "change_id": "cmake-change.modified.0123456789abcdef", "baseline_contract_sha256": "<64 lowercase hex characters>", "current_contract_sha256": "<64 lowercase hex characters>", "classification": "breaking", "rationale": "Why the change is intentional and what embedding projects observe.", "migration": "Docs/Migrations/Next.md#changed-option", "release_note": "Docs/ReleaseNotes/Next.md#changed-option", "compatibility": "Pinned projects must update the option and engine revision together.", "owner": "build-release" } ``` Historical entries may remain; unmatched entries are inert. Domain, change ID, and both hashes must match. Marking a detected change `compatible` does not waive the other fields. If migration or a release note is unnecessary, record the reviewed reason rather than an empty value. The validator proves ledger shape and exact binding, not approval quality. Review must reject placeholder text, incorrect compatibility claims, missing migrations, or an owner outside the affected domain. ## CI behavior The `Validate documentation` job checks out full history and selects: - `github.event.pull_request.base.sha` for pull requests; - `github.event.before` for pushes to `master`. After model/reference freshness checks, CI runs `docs_contract_diff.py --write --enforce`. A missing required disposition fails the job. The `if: always()` upload step preserves both reports for diagnosis. A multi-commit pull request is compared with its base branch commit, not the previous feature-branch commit. A multi-commit push uses the complete pushed range. The standalone validator rejects removal of the shared ledger, seventeen-model manifest contract, full-history checkout, base-ref argument, aggregate test, or enforcement switch. ## What requires human review For a public breaking change, review all of these: 1. The source declaration or manifest change is intentional. 2. A replacement or migration path exists, or the ledger explains why none is possible. 3. Release notes identify affected developers and supported revision lines. 4. Network/serialization changes include required compatibility or migration metadata. 5. Generated references, examples, and focused tests change together. 6. The owning runtime, scripting, build, or release domain accepts the support timeline. Internal changes need no compatibility promise, but surprising removals and shape churn still deserve review because they may reveal a missing stability classification. ## Limits of static diffing The aggregate report cannot detect: - behavior changes behind unchanged declarations; - updater protocol, authored formats, or project settings without canonical models; - incorrect prose claiming compatibility; - whether a migration guide or package path was exercised successfully; - support across release lines that have not been declared and tagged. Runtime, structural CMake, native-extension, prototype/map/model/text/effect/image/particle/font/audio/video/AiControl protocol, package, starter, and embedding-project tests remain required. A green seventeen-domain report proves only that the modeled declarative surfaces have an accepted revision transition. ## Troubleshooting - `Contract baseline git revision is unavailable`: fetch the exact revision; do not use bootstrap to hide a shallow checkout. - `does not exist at baseline revision`: expected only for a model's first landed revision. - `missing dispositions`: inspect `Workspace/contract-diff.md`, add reviewed entries, and rerun against the same revision pair. - disposition remains missing: copy the exact domain, change ID, and both contract hashes from the current report. - a description edit is classified as breaking: add a focused nested-field regression before changing policy or the ledger. - an internal change unexpectedly blocks: inspect model-source/scope drift first; ordinary internal entry changes are report-only. - many entries change together: verify stable IDs and the owning generator before accepting the report. ## Validation checklist 1. Regenerate and check all seventeen canonical models plus generated Markdown. 2. Run `test_docs_api_diff.py` and `test_docs_contract_diff.py`. 3. Compare against the intended base with `--write --enforce`. 4. Review every required disposition and replace every generated placeholder. 5. Run the affected native, script, CMake, packaging, starter, or project test. 6. Run `test_docs_validate.py` and `docs_validate.py`. 7. Inspect the CI contract-diff artifact and confirm each domain status. 8. Keep staging empty unless staging or commit was explicitly requested. ## See also - [GeneratedApiAndMetadata.md](../reference/metadata/index.md) - canonical models and generated-reference ownership. - [ADR-0002](decisions/0002-public-api-stability-contract.md) - accepted stability and change policy. - [Remote Calls](../reference/scripting/remote-calls.md) - project-owned remote-call catalog and compatibility boundary. - [Documentation maintenance](documentation/) - documentation change and revision-reconciliation workflow. - [Public Contract Index](../reference/public-contract/index.md) - generated cross-domain public contract index. ===== END DOCUMENT api-change-management ===== ===== BEGIN DOCUMENT adr-github-pages-markdown-publication ===== Source: Docs/en/contributing/decisions/0001-github-pages-markdown-publication.md Canonical URL: https://fonline.ru/Docs/en/contributing/decisions/0001-github-pages-markdown-publication.html Content SHA-256: bf9e7605e8e044f83467f2c2ef16cb7ae0a2b48132b1f44a9a33dfcfbb6a62dc --- layout: default title: "ADR-0001: GitHub Pages Markdown Publication and Locale Layout" locale: en document_id: adr-github-pages-markdown-publication permalink: /Docs/en/contributing/decisions/0001-github-pages-markdown-publication.html --- # ADR-0001: GitHub Pages Markdown Publication and Locale Layout - Status: Accepted - Date: 2026-07-10 - Amended: 2026-08-01 after the locale-aware site and rendered-browser gates landed - Owners: documentation, build-release ## Context FOnline already publishes its repository site through GitHub Pages, uses Jekyll configuration from `_config.yml`, and binds the production domain through root `CNAME = fonline.ru`. The documentation source is Markdown in the engine repository and must remain readable both in the GitHub repository UI and on the public site. The production documentation program also requires: - standalone engine documentation with no embedding-project filesystem dependency; - English canonical content and a complete Russian mirror for human-facing pages; - stable URLs, redirects, navigation, search, and machine-readable indexes; - pull-request validation that exercises the same constraints as production; - no second content tree whose generated output can drift from repository Markdown. Introducing a separate site application would duplicate ownership and move the publication contract away from the system already serving `fonline.ru`. ## Decision 1. GitHub Pages remains the production publisher and Jekyll remains the renderer. 2. Markdown committed to this repository is the canonical human-documentation source. 3. Root `_config.yml` and `CNAME` remain part of the tested publication contract. `Docs/documentation-manifest.json` records provider, generator, source format, domain, and owning paths. 4. Do not introduce Docusaurus, a parallel `website/` content tree, or checked-in generated HTML. 5. Jekyll layouts, includes, data files, supported plugins, theme overrides, and static assets may provide presentation and navigation, but must remain a thin rendering layer over Markdown. 6. The target public locale layout is: ```text Docs/ en/ # canonical English human docs ru/ # Russian mirror with identical relative paths and stable document IDs assets/ # shared published media, styles, and search assets _meta/ # internal plans/reports, excluded from public navigation ``` 7. Public pages use stable IDs and matching relative paths across locales. A language switch resolves by document ID/path rather than title text. 8. English is canonical for source synchronization because engine identifiers, source comments, symbols, and upstream collaboration are English. Russian pages are whole-document mirrors, not mixed-language fragments. 9. Translation freshness is tracked from a canonical-content hash. Production publication must not present a stale Russian page as current. 10. Existing public URLs receive GitHub Pages-compatible redirects or durable Markdown route pages before source files move. 11. The existing Pages source branch/folder remains unchanged by documentation restructuring. Repository administrators must verify and record that setting plus DNS ownership before a production migration. 12. Pull requests run fast Markdown/manifest/link checks and, in the site phase, a GitHub Pages-compatible Jekyll build that uploads `_site` as a review artifact. Production still deploys through the existing Pages route. ## Consequences ### Positive - GitHub and `fonline.ru` render the same authored files. - Documentation remains portable and useful without Node or a client-side application. - AI systems can consume clean Markdown and generated JSON directly. - The custom domain and publishing stack are reviewable in normal repository diffs. - Locale parity can be enforced by stable paths/IDs without a framework-specific translation registry. ### Costs - Navigation, static search, language switching, and version indicators must be implemented within GitHub Pages-supported Jekyll capabilities. - Moving the current flat English tree requires redirect planning before `Docs/en/` becomes canonical. - GitHub Pages provides one production site; pull-request previews are build artifacts unless a separate approved preview environment is added later. - Translation parity adds release work after the English information architecture freezes. ## Rejected alternatives - **Docusaurus or another separate site application:** rejected because it creates a second framework/content contract and is not the existing production route. - **Checked-in generated HTML:** rejected because generated output would compete with Markdown as source of truth. - **Sibling `Docs.EN` / `Docs.RU` roots:** rejected in favor of conventional lowercase locale directories under one documentation root. - **Mixed English/Russian pages:** rejected because they weaken routing, search, translation freshness, and machine retrieval. - **Immediate version snapshots:** deferred until the engine has release tags and an explicit support policy. ## Verification - `python BuildTools/docs_validate.py` checks manifest publication values, `_config.yml`, and `CNAME`/domain agreement. - `python BuildTools/docs_site.py --check` checks locale-aware navigation and bounded search derived from the manifest. - `python BuildTools/docs_site_artifact.py --site-dir _site` validates the rendered Jekyll routes and static endpoints. - `npm --prefix BuildTools/docs-browser run audit` checks desktop/mobile pages, interactions, screenshots, and axe-core results. - The `Validate documentation` and `Build documentation site` jobs run without an embedding project or native build and retain `_site` as a review artifact. - Standalone GitHub Markdown rendering remains a required route alongside Jekyll. ## Related documents - [ADR-0006](0006-documentation-version-locale-routing.md) - [ProductionDocumentationPlan.md](https://github.com/cvet/fonline/blob/master/Docs/ProductionDocumentationPlan.md) - [Documentation maintenance](../documentation/) - [documentation-manifest.json](../../../documentation-manifest.json) ===== END DOCUMENT adr-github-pages-markdown-publication ===== ===== BEGIN DOCUMENT adr-public-api-stability-contract ===== Source: Docs/en/contributing/decisions/0002-public-api-stability-contract.md Canonical URL: https://fonline.ru/Docs/en/contributing/decisions/0002-public-api-stability-contract.html Content SHA-256: 664b3ee699d8df18d02744182d1d93e447ef99d4ec654318695139785c7ca417 --- layout: default title: "ADR-0002: Public API Stability Contract" locale: en document_id: adr-public-api-stability-contract permalink: /Docs/en/contributing/decisions/0002-public-api-stability-contract.html --- # ADR-0002: Public API Stability Contract - Status: Accepted - Date: 2026-07-10 - Amended: 2026-08-02 after the native-codegen inventory received an owner-reviewed experimental scope contract - Owners: scripting, runtime, build-release, documentation ## Context FOnline exposes many surfaces to game projects: CMake helpers, BuildTools commands, settings, package layouts, native hooks, AngelScript methods/types/events/properties, metadata, serialized entities, network messages, file formats, and application/runtime ABIs. Reachability is not the same as a compatibility promise. Before this decision, `PUBLIC_API.md` mixed current, planned, and obsolete statements and could not be regenerated or checked against source. The locale-specific public contract index is now generated, while the owning model retains the actual stability classification and the root path remains a durable legacy route. Freezing every reachable symbol would also prevent necessary engine refactoring. Developers and AI agents need to know which surface is stable, which is experimental, where the authoritative declaration lives, and what work is required when a contract changes. ## Decision ### Stability labels Every generated public contract uses one of these labels: | Label | Meaning | |---|---| | `stable` | Supported for documented release lines; incompatible changes require migration and release disposition. | | `experimental` | Publicly usable for evaluation, but signature/behavior may change with explicit release notes. | | `internal` | Reachable implementation detail with no compatibility promise. | | `deprecated` | Previously supported surface scheduled for removal, with replacement and removal window documented. | Until a symbol or surface is explicitly classified, it is `internal`. Existing reachability, an example in a project, or inclusion in generated bindings does not implicitly make it stable. ### Source of truth 1. Source declarations, metadata annotations, parsers, settings declarations, CMake helpers, and tests are authoritative for current behavior. 2. Generated canonical contract models normalize those sources into stable IDs and machine-readable JSON. 3. Human reference pages, the locale-specific public contract indexes, and the root `PUBLIC_API.md` legacy route are generated from those models and supplemented with task-oriented examples and explanations. 4. Manual prose must not maintain declaration counts, signatures, or cross-domain inventories that can be generated. `Docs/generated/source-inventory.json` remains the independent source inventory; the eighteen contract models own their respective reusable surfaces. 5. Project documentation may describe how it consumes an engine API but cannot upgrade that API's stability label. ### Source-owned classification Native-codegen symbols use a separate `///@ ApiContract