FOnline Engine
Current 2026.1.25-dev GitHub
Documentation AGENTS.md

FOnline Engine — AI Maintainer Guide

AI guide; human entry: README.md. Docs: EN, RU mirror. Engine authoring is English: commit subjects/bodies, PRs/issues, comments, diagnostics, docs, plans and release notes. Russian only mirrors docs; chat language does not change this rule.

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: record old/new SHAs, audit every incoming source/test change, regenerate affected models, and document the reconciliation before dropping any safety stash.
  7. Rebase unpublished local commits onto the fetched base, even on branches with an upstream or earlier pushes. Never rewrite already-pushed commits. Use merge only to reconcile diverged published histories. Before pushing, verify git merge-base --is-ancestor <destination-remote-tip> HEAD. No reset/amend of pushed commits, force-push, or remote branch deletion.
  8. Every master update follows the mandatory Engine update/version contract: increment minor from the latest published master tip, use the current UTC year and YEAR.MAJOR.MINOR-dev, add dated EN/RU change and exhaustive migration notes, reconcile every affected owning/generated doc, and validate the exact publication range. Documentation, test, CI, dependency and revert changes count too. Release branches freeze the cut and advance only patch; do not create or publish them without the owner’s command.

Documentation Map

Checkout agents start here and follow the source guides; llms.txt and llms-full.txt are optional website retrieval outputs, not maintainer instructions. Run python BuildTools/docs_prepare.py before documentation validation. Build the site with python BuildTools/docs_site_build.py; dependency pins and public root endpoints live in ignored Workspace/Documentation/, the rendered site in Workspace/DocumentationSite/, and the authored config in Docs/Site/_config.yml. Delivery indexes and reports are ignored build outputs; reviewed Markdown, policies, images, translations, and public contract models remain versioned. Never stage the generated delivery files.

The full maintained index is Docs/en/index.md; use it when a topic is not listed here. Convention-critical docs for maintainers:

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 (<ProjectDevName>_UnitTests / RunUnitTests).
  • Build-system changes: update BuildTools/cmake/ProjectInterface.json when the project-facing CMake surface changes, update Project-Local Dependencies 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, 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, 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, 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, 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, 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, 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, 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 and/or 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, 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, 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, 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 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, 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, 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, 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, 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 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 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:

    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).
  • 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.
  • Only the current API; preserve existing meaning. Add or extend a contract without changing the meaning of existing valid values, inputs, outputs, units, IDs, defaults or side effects. A replacement uses a distinct name/type/key/format and removes the old surface. No deprecated aliases, fallback parsing, old-API shims or permissive coercions. An unmigrated affected consumer must fail explicitly during compilation/baking/validation; unaffected usage must retain its behavior. This applies to experimental and internal surfaces too. The narrow persisted-property conversion exception is existing MigrationRule handling; project-specific database business migrations remain project-owned. An old-build refusal may carry the temporary marker below, which never authorizes an old API bridge. ADR-0002 and the exhaustive migration record own the full rules.
  • 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.
  • 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.
  • 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

  • Managed projects and the runtime C# compiler enable overflow checking by default. Use a local unchecked only for intentional wrapping or bit truncation; preserve checked diagnostics elsewhere.

  • 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.
  • 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 <cmath>, 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.
  • 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(<Category>) 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_<Category> 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_<Category>). 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.
  • Separate semantically distinct stages inside functions and lambdas with a blank line: input/context validation, resource acquisition, ownership transfer, value conversion, callback invocation, and result handling each form their own group. Keep a declaration together with its immediate validation or initialization; do not compress several stages into one uninterrupted run. Match the surrounding code and review these groups manually: a formatter cannot infer them. Applies to native code and managed scripts.
  • Surround every control-flow statement (if/else, for/foreach, 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). This does not require blank lines just inside the opening or closing brace.
  • 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; 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 §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 (§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: <reason> on the declaration or the line above it, and the reason is mandatory. See local variables; 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<T>/nptr<T> 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<T> and every owner convert to ptr<T> implicitly, so do not write .as_ptr() where that implicit conversion applies — a ptr<T> 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; 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; 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<Sig> (an alias of move_only_function<Sig>) is the default, and copyable_function<Sig> 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.
  • 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.
  • 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.
  • 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.
  • Call the operating system through winapi:: or posix::, never directly. Source/Essentials/WinApi.* and Posix.* are the only places <Windows.h> 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.
  • A setting is read, never written. Source/Common/Settings.inc declares every entry with one macro, SETTING(<type>, <Group>, <Name>, <default>), 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.
  • A setting is addressed through its group. Source/Common/Settings.inc declares a group as SETTING_GROUP(<Group>, <virtual bases>) 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.
  • 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<T> when presence is guaranteed and nptr<T> 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 §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 #defined 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.
Start typing to search.