Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

X-Ray Forge

X-Ray Forge (XRF) is a TypeScript rewrite of the S.T.A.L.K.E.R.: Call of Pripyat script layer. It compiles gameplay code to Lua and builds the configs, UI forms, translations, and resources used by X-Ray.

This book covers the full development workflow: project setup, builds, CLI and desktop tools, runtime architecture, gameplay schemes, and debugging.

Start here

Generated files are written under target/. Edit sources in the engine or tools repository and rebuild; do not edit generated gamedata by hand.

Project repositories

XRF uses OpenXRay as its main engine reference. Game assets are split across base, extended, and locale repositories for English, Ukrainian, and Russian.

XRF changelog

This changelog records notable player-, modmaker-, and developer-facing changes in xrf-engine.

Latest check: xrf-engine 5875ddeb9a399ff371bc51151739e39888098a5a.

~1.0.0

August 2026

  • Database archives are packed by the XRF tools instead of xrCompress. compress now calls xrf-cli pack-archive, which reads the same configuration dialect and writes archives straight into target/db, so the SDK binary is no longer part of a build. A group that fits one volume is now named <target>.db rather than <target>.db0. Archives holding mostly text pack somewhat larger, because the packer uses one LZO level where xrCompress used its slowest.

  • Script declarations are discovered and documented automatically. Runtime callback, condition, effect, dialog, and task modules load from gamedata/declarations; builds emit an extern JSON manifest, and verify externs checks it against the declaration sources.

  • Lua modules load from loose files and packed archives. The bundled engine resolves require() through X-Ray’s virtual filesystem, with loose files overriding archived copies. This supports dynamic declarations and extensions in both development and packaged builds. The same rollup adds an inventory-info-removed callback and fixes sound resume, static spatial updates, duplicated HUD release callbacks, and release-build logging.

  • Strict payload validation moves to assembled gamedata. verify ltx no longer accepts --strict; expensive payload checks run through verify gamedata --strict, while regular LTX typing, include, and inheritance checks remain available.

  • Random upgrades no longer affect trader inventory. The enhanced-items extension now upgrades only weapons held by non-trader human NPCs. It no longer upgrades world items, outfits, helmets, or trader stock.

  • The start command can flush engine logs immediately. Pass --flushlog when launching a test session to retain the final log lines after a hard crash; it may incur a performance cost while active.

  • Mounted weapons use the correct shell particles. Mounted PKM weapons now reference the bundled shell effect, so their ejected casings render correctly.

  • Surge weather uses a bundled sky texture. Surge phases no longer reference the unavailable preblowout\\pre_blowout_0 texture.

  • Used particle effects ship with their textures. The resource payload removes unused particle definitions and adds the blood, fire, smoke, spark, explosion, and distortion textures required by the remaining effects.

  • The single-player package trims multiplayer data without removing required definitions. Multiplayer gameplay configs, unused UI assets, and server settings are omitted. Engine-required multiplayer screen and map-spot definitions remain because removing them caused crashes.

July 2026

  • NPCs keep their animation state while changing smart-terrain jobs. Reassigning an online NPC activates the new job directly instead of briefly clearing its active logic, preventing the ALife planner from resetting the NPC to idle between jobs.

  • Wounded NPCs settle their weapon before falling. The wounded action selects the idle item and prevents weapon reselection until it ends, avoiding an interrupt that can restart the wounded animation.

  • Danger hearing has explicit reaction ranges. NPCs respond to each danger type using squared-distance limits; enemy sounds are now considered within the configured 40-metre range instead of being disabled by a zero range.

  • Animpoint NPCs complete the requested turn before settling. Reaching an animpoint uses a latched facing check. Leaving the point or changing its cover invalidates the completed turn.

  • Post-combat idle yields to wounds and active danger. A wounded NPC is not captured by a post-combat animation, and a newly active danger state can take over instead of waiting for the post-combat delay.

  • Wounded NPCs do not enter danger from sounds. The hearing handler declines weapon sounds before assigning danger inertia to an NPC already in a wounded state.

  • Weapon-sound handling rejects irrelevant work early. The danger listener filters non-weapon sounds, non-creature sources, and out-of-range sounds before evaluating enemy relations.

  • The Zaton B52 bandit leader carries the required PDA. The quest character’s generated supplies now include device_pda_port_bandit_leader.

  • The CLI can start a targeted test session without navigating menus. It supports a new game or named save, an optional difficulty, and optional intro skipping; this is a developer workflow, not a change to normal in-game start behavior.

  • The CLI can build on-demand in-game quest checks. checks list, build, and clean manage flow scripts that observe quest progress, verify reached states, and report through the console, engine log, and a dedicated checks log.

  • Graphics presets load their shared settings with engine console syntax. The renderer presets use cfg_load for the common AtmosFear settings instead of treating an LTX #include directive as a console command.

  • Quest helpers resolve the requested target and anomaly zone. destroy_object keeps all story-target parameters, and the Zaton B29 artefact check searches the requested anomaly instead of the global artefact registry.

  • Psy post-process effects keep unique IDs. Removing one active psy effect no longer lets the next effect reuse an ID that belongs to another active post-process.

  • Simulation assignment state is cleaned up safely. Temporary squad-to-terrain assignments are cleared after use. Invalid or missing data follows fail-safe handling instead of leaving stale simulation state.

  • Missing music themes fail safely. Music initialization validates unavailable theme data instead of continuing with incomplete state.

  • Travel dialogs reject NPCs without a valid squad. Conversation predicates return unavailable for a missing squad instead of dereferencing it while checking routes, prices, or companion movement.

  • Squad-travel prices follow the simulated route. Travel charges the server-graph distance between the squad and destination terrain, rather than the NPC’s current world-position distance.

  • NPC sound state is independent. XRF keeps playable sound state per NPC instead of sharing a theme instance among all users. Concurrent dialogue and ambient playback no longer overwrite one another.

  • Trade resupply state is initialized and refreshed consistently. Traders retain the selected supply condition and refresh on the configured period instead of relying on incomplete descriptor state.

  • Mod-only packages can intentionally omit engine binaries. pack mod --skip-engine no longer validates or requires a configured engine when it will not copy one, keeping a script/resource-only package independent of local engine selection.

  • Camp performers return to idle after finishing. NPCs no longer remain stuck in guitar or harmonica animations after the corresponding camp story ends.

  • Localized interface text retains its accents. French, Italian, Polish, and Spanish text no longer contains broken characters; the French, Italian, and Spanish options screens also label the world- and HUD-FOV controls.

  • Boars and flesh recognise each other as friendly. Their relationship now matches vanilla’s monster-relation configuration rather than treating the species as neutral.

  • Malformed quest condition lists are repaired. XRF fixes broken vanilla condition-list data in the Zaton B7 and B20, Jupiter B8, and Pripyat underpass B400 quest logic so the conditions parse and evaluate as intended.

  • LTX validation covers game-script runtime schemes. Common physical, restrictor, monster, stalker, weather, sound, task, and model sections declare strict $scheme types so malformed fields and condition lists can be caught before runtime.

  • Literal NPC-name checks. NPC-name conditions now compare requested text literally while retaining the fast indexed loop. This prevents Lua pattern characters in a requested name from changing the match.

  • Dirty smart-terrain work has a global budget. Reselection work is queued, deduplicated, and limited per frame and per second instead of letting every invalidated terrain run at once.

  • The Jupiter guide transition uses the intended fade and handoff. First arrival as a visitor starts the missing black post-process, stops it after the welcome delay, and lets the guide enter its first-visit state when the journey condition applies.

  • Actor controls use persistent, ordered locks. Overlapping systems such as cutscenes, surge survival, and anabiotic sleep keep their input/UI lock until release, so one cannot restore controls held by another.

  • Tracy profiling tools and engine variants are bundled. XRF includes capture, viewer, export, and trace-import utilities together with gold-tracy and release-tracy engines. The older Lua-hook profiler was removed.

  • The LR300 sights align correctly. Adjusted HUD offsets remove the rifle’s misaligned aim view in both standard and 16:9 layouts.

  • Smart-terrain maintenance adapts to distance. Job maintenance is throttled by actor distance, while arrivals, departures, and state changes still mark the terrain for selection. The introducing change reports about 30% less per-frame job-processing work; it is not a general FPS benchmark.

  • Weather updates run on a 2.5-second actor cadence. The weather manager uses the actor binder’s throttled event instead of running on every actor update.

  • Unvisited restrictors check the actor at an interval. Map-discovery restrictors accumulate elapsed time and test the actor position only at the configured interval instead of every update.

  • Physical-object callbacks follow the online lifecycle. Hit, death, and use callbacks are installed once when an object comes online and cleared when it goes offline instead of being reassigned on every render update.

  • Scripted physical buttons no longer fail while logging their use. Logger format arguments now match their placeholders in physical-object and treasure paths.

  • Resolved condition and effect functions are cached. Parsed condition-list entries retain their resolved xr_conditions or xr_effects function reference after the first lookup.

  • Squad target outrank checks are staggered. Simulation no longer performs every squad’s full target-outrank check at once; it uses staggered rechecks and caches terrain-assignment counts.

  • Squads reuse unchanged ALife tasks. A squad retains its simulation task while its graph vertex is unchanged, avoiding repeated task allocation.

  • Game-graph lookups and distances have bounded caches. Repeated level-name, vertex-level, and graph-distance queries reuse session-stable values; the distance cache has a fixed size.

  • Smart-terrain job selection is incremental. Clean updates retain valid jobs and probe a bounded number of higher-priority candidates instead of reselecting every job.

  • NPC sound themes are registered on demand. Sound setup defers theme registration until a sound is actually needed instead of preparing every possible theme for every spawned NPC.

  • The save dialog tests real file existence. Existing-save detection uses the engine file object returned by FS.exist, so the overwrite warning follows the binding’s actual result type.

  • X-Ray 16 SDK integration is upgraded to v2. The earlier xray16 package adoption expands to shared Lua helpers, mocks, and TypeScript aliases, and the remaining local copies are removed.

  • Texture quality exposes the full engine range. The advanced-video slider always offers LOD values 0 through 4 instead of raising its minimum from an address-space probe.

  • Scheme condition types are memoized. Scheme switching records the parsed condition type after the first evaluation instead of repeating Lua pattern matching on every update.

  • Task updates are spread across time. Task objects randomize the next update interval, avoiding synchronized re-evaluation bursts while keeping periodic checks.

  • Treasure statistics see condition-list emptying. A treasure cleared by an empty condition list emits the same TREASURE_FOUND event as other collection paths, so event consumers record it.

  • Deimos cleanup removes the matching effectors. The reset path now removes the camera and secondary post-process effectors by their correct IDs.

  • Cutscenes disable and restore the game UI. Cutscene setup and cleanup pass the UI-lock state expected by the actor-input manager, including restoration of the remembered weapon slot.

  • Psy antennas play both channels. The psy-antenna loop uses both the left and right sound objects, restoring the intended stereo effect.

  • Surges do not replay task-side effects when the hide task is disabled. The surge manager records the task stage even when the configured section is empty, preventing duplicate alarms, sounds, and weather effects.

  • Task objects obey their update window. A task with a known state skips functor evaluation until the next scheduled update instead of using the inverted time check; title and description changes are then applied together.

  • NPC torches turn off when their light is no longer needed. Stalker torch state is applied for both outcomes, so daytime NPCs no longer keep their lamps on after a night-time or danger state.

  • Jupiter guide routes use the correct NPCs. Travel from Jupiter now resolves the Zaton guide and Pripyat assistant by their actual story IDs instead of selecting the wrong character.

  • Disabled anomaly zones still update field switching. Turning a zone off suppresses artefact respawn but no longer prevents its configured anomaly fields from cycling.

  • NPCs can use their configured cover chatter while approaching cover. A sound_idle configured for a cover scheme now plays both while the NPC moves to cover and after it has arrived.

  • Close-combat state updates in one evaluation. When enemy memory is already stale, the camper evaluator can enter and clear close-combat state in the same evaluation instead of exposing an incorrect extra planner update.

  • Patrols accept their full seven-NPC limit. The patrol manager now permits seven registered NPCs and rejects only an eighth participant.

  • Exclusive smart-terrain jobs reserve a fallback slot. When an exclusive job’s suitable condition is false, the terrain retains an unconditional low-priority slot instead of leaving the NPC without a selectable job.

  • NPCs can hold smart cover during combat. The missing combat action for use_in_combat is registered again, so the combat planner has an action to run without forcing the NPC out of its scripted cover.

  • Helicopters follow the intended patrol movement math. The rewrite now uses vanilla’s square-root velocity formula, ignores repeated waypoint callbacks, and keeps a dying helicopter registered until its normal object teardown.

  • Night predators wait until late night to hunt vegetarian monsters. The monster_predatory_night simulation role uses a 21:00-to-day-start window instead of the broader 19:00 night window.

  • Zombied NPCs fire at their visible enemy. The standing fire state now passes the selected enemy as its target instead of losing that target while starting the firing animation.

  • Critically wounded NPCs can still choose an enemy. Critical wounds take precedence over a combat-ignore override, allowing the NPC to fight back.

  • Night vision is toggled from its actual state. XRF checks the device’s current state before changing it.

May 2026

  • Bundled screen-space shader add-ons were removed. The interactive-grass and shadow-cascade extensions, color grading presets, shader selectors, and their options-page controls are no longer part of the engine package.

May 2025

  • Script builds can inject Tracy zones. build --inject-tracy-zones instruments generated Lua for engine-level profiling with a compatible Tracy-enabled engine.

  • The CLI can verify assembled gamedata. verify gamedata checks target/gamedata through the bundled XRF tools, with verbose and strict modes for deeper asset validation.

March 2025

  • Generated NPC loadouts can use multiple sections and probabilistic attachments. Modders can generate separate loadout sections and configure scope, silencer, and launcher attachment probabilities.

January 2025

  • XRF adopts an expanded particle library with a round-trip editing workflow. The CLI can unpack particles.xr into editable LTX files, repack those files, and verify both representations.

December 2024

  • Dead creatures ignore late sound callbacks. Stalker and monster binders decline hearing events after death instead of forwarding them into danger and scripted sound logic.

  • Weapon inventory icons match their items. The Protecta and Winchester 1300 use corrected icon-grid positions in inventory and trade interfaces.

  • Teleport effects play their intended tinnitus sound. The actor-teleport helper uses the engine’s sound-path separator so its 2D sound resolves.

  • The debug panel can dump live Lua state. Managers contribute bounded diagnostic data to _appdata_\\dumps\\lua_data.json, and the existing merged-system.ini dump uses the corrected file-writing path.

  • The runtime exposes extended engine callbacks. OpenXRay/CoC hooks cover save/load completion, level changes, server-object removal, input, inventory focus and trade filtering, AI visibility, and weapon selection.

August 2024

  • Translation workflows use the native XRF tools. Translation builds moved to the bundled JSON/XML converter in July; initialization and validation followed in August, replacing their separate Node implementations.

May 2024

  • Advanced video options expose renderer-specific controls. The menu includes shadow-map quality and tessellation where supported, plus the always-active window setting.

  • AF3 weather cycles replace vanilla weather selection. XRF selects af3_* weather sections from a dynamic AtmosFear graph, including moon-phase variants for clear and partly cloudy periods. It ships the matching weather, ambient-channel, and weather-effect configuration.

  • Original new-game spawn position. XRF can set the vanilla start vertex and position only while creating a new game, leaving loaded saves untouched.

  • Equipment icons have a round-trip editing workflow. The resource sprite is split into per-item source images, and the CLI can unpack or rebuild the combined equipment texture and its descriptors.

  • Jupiter scanner spots show the right artefact information. After the scientist scanner reward, map hints update the existing spot with the current artefacts or an empty notice, including the corrected JUP B211 target and hint.

  • Map-display maintenance uses a shared five-second actor event. Terrain and sleep markers use the throttled ACTOR_UPDATE_5000 event instead of a per-actor-update callback and timer.

March 2024

  • Bundled native XRF tools are available to the project CLI. Their first integration provides reproducible LTX formatting and verification, including a non-writing formatter check mode for local use and CI. Later workflows use the same tools for translations, icons, particles, assembled gamedata, and extern manifests.

  • The CLI can unpack ALife spawn data. spawn unpack converts the configured all.spawn into an inspectable output tree with path, destination, force, and verbose controls.

  • LTX verification understands typed project schemas. verify ltx checks includes, inheritance, section types, required fields, and the initial $scheme declarations. Runtime game-script coverage expands in July 2026.

February 2024

  • Engine switching tolerates broken links. The CLI can replace a corrupted engine symlink instead of failing while trying to inspect its target.

January 2024

  • Gameplay XML can be authored as typed TSX. The config builder renders gameplay TSX into XML, allowing character profiles, dialogs, and related generated data to share components and helpers.

  • NPC character descriptions and loadouts use typed generators. Reusable faction, profile, weapon, food, drug, and item presets replace hand-maintained XML spawn lists while retaining counts, condition, probability, and attachment flags.

  • Optional screen-space shader presets were added. Players could select new or vanilla shader packages, choose color grading, and enable interactive grass and shadow cascades. These bundled add-ons were removed in May 2026.

  • Disabled extensions no longer crash save loading. Extension restoration checks for an unregister hook before calling it, so a disabled module can load safely even when it does not define one.

  • Achievement reward caches replenish. Achievement rewards extension records the Detective and Mutant Hunter awards in save data. Every 12 in-game hours after an award, it replenishes the configured medical supplies or armour-piercing ammunition in the Zaton or Jupiter reward box and posts a notification. Players can turn it off from the Extensions menu.

  • Saving does not create an inactive psy controller. Save/load code serializes the psy-antenna manager only when it already exists, avoiding state changes caused solely by saving the game.

  • Signal lights restore their flight state after loading. Scripted flare force, particles, and timing resume from saved state instead of being left partially initialized.

  • Generated character loadouts retain their first item. The character-description builder emits a spawn section that preserves the first configured supply item.

December 2023

  • XRF removes its authored multiplayer menu. Multiplayer login, server, profile, and demo classes and forms are deleted, leaving the project-owned main menu focused on single-player. Engine-required multiplayer-named UI definitions remain a separate packaging concern.

  • Creatures restore a consistent online position. Stalkers and monsters use an explicit spawn vertex, remembered offline vertex, or assigned smart-terrain job position when synchronizing after net_spawn.

October 2023

  • Randomized upgrades. The enhanced-items extension can add random upgrades when equipment first comes online.

  • Progressive smart-terrain discovery. XRF initially hides smart-terrain map spots and blocks same-level travel to a terrain until the actor has visited it.

  • Extensions can be enabled and disabled from the menu. Modules that allow toggling expose the control in the Extensions dialog, and disabled modules are skipped during registration.

  • An optional start-position extension is introduced. Its first version applies an alternative position only to a new game started on Zaton. It was later reworked into the Original start position extension.

  • Post-combat idle has a shorter default delay. NPCs wait 5 to 10 seconds after combat instead of vanilla’s 10 to 15 seconds. An explicit post_combat_time value still overrides the default.

  • Vendor-logo videos are skipped by default. XRF disables the GSC, ATI, and AMD sequence; the intro renders an empty image item instead.

September 2023

  • Translation tooling expands to eight locales. English, French, German, Italian, Polish, Russian, Spanish, and Ukrainian sources can be initialized, merged with imported JSON, and checked for missing or invalid strings.

  • The development panel adds registry filters, profiling, and quest controls. Developers can filter live objects, time selected runtime portions, and inspect or manipulate task state.

  • Verbose runtime systems can write dedicated logs. Selected managers can write to their own file, either alone or alongside the main engine log.

  • World-object lifecycle events are available to runtime modules. Actors, creatures, physical objects, zones, smart terrains, covers, helicopters, and inventory classes emit online/offline registration events.

  • The browser UI preview command is removed. Form debugging moves to the bundled engine’s ImGui and in-game development tools.

  • Advanced video options expose grass and window controls. Players can change grass detail height and radius and select an engine window mode instead of using the old fullscreen checkbox.

  • NPC loading preserves the remembered level vertex. When optional script save data omits a vertex, the binder keeps the stored offline position instead of replacing it with an empty value.

  • Psy antennas initialize sound with valid paths and intensity math. The manager starts its sound objects, resolves the tinnitus assets with engine separators, and uses cubic power rather than a bitwise XOR expression.

  • Treasure icons show rarity. Treasure map marks can distinguish common, rare, epic, and unique stashes instead of using only the generic treasure icon.

  • Off-level smart terrains maintain their jobs. Job creation, update, and selection are no longer skipped merely because the actor is on another level.

  • Zombied danger actions require a living NPC. The planner uses the alive property instead of the unrelated ALife property when deciding whether a zombied stalker can move toward danger.

  • The Jupiter B4 conversation uses a bound terrain check. Its predicate calls the local smart-terrain helper instead of an unavailable external, preventing the dialogue path from failing.

  • Sleeping reliably opens its time-selection dialog. The sleep manager creates the dialog when needed, fixing the path where the sleep UI was absent.

  • Field-of-view controls are available in the options UI. XRF adds world-FOV (55 to 115) and HUD-FOV (0.40 to 1.00) sliders to advanced video options.

  • Game packages seed explicit FOV values. pack game copies XRF’s root user.ltx, setting initial world FOV to 80 and HUD FOV to 0.7. Players can change these starter values in the XRF options UI.

August 2023

  • XRF introduces a scripted dynamic-weather manager. It selects weather-graph states, changes good and bad periods, coordinates surges and time changes, and saves and restores its state.

  • Engine declarations and build plugins move into the versioned xray16 package. The shared package replaces the earlier declaration submodule and becomes the base for the broader X-Ray 16 SDK integration.

  • Advanced video options have separate frame caps. Gameplay and menu rendering use independent sliders for rs_fps_limit and rs_fps_limit_in_menu.

  • The bundled OpenXRay engine adds broader input and UI support. The update includes keyboard and gamepad navigation for PDA, dialog, task-list, and message-box interfaces, plus renderer, audio, animation, grenade, and shutdown fixes.

  • The bundled engine includes an ImGui UI debugger and more script hooks. Developers can inspect UI state with F10 and use additional Lua-visible UI callbacks added by the same OpenXRay rollup.

  • NPC assistance can retarget while active. When a corpse-search or wounded-help evaluator selects another target, XRF reissues movement instead of keeping the original corpse or wounded NPC.

  • Corpse-loot checks stop after the first valuable. Before selecting a corpse to loot, XRF stops inventory iteration when it finds the first valuable item. Vanilla’s evaluator scans the full inventory after setting its shared flag.

July 2023

  • XRF supports modular gameplay extensions. It discovers and registers modules from the extensions directory, persists their load order, passes each module its descriptor, and lets extensions load relative LTX files or override the runtime system.ini. The main menu initially managed ordering; enable/disable controls followed in October.

  • XRF save data has a separate sidecar. Runtime-specific dynamic data is stored beside each .scop save, and runtime systems can react before and after save or load operations.

June 2023

  • Optional resource repositories can be cloned from the CLI. The command lists configured repositories and can clone a selected resource or locale payload with safe, force, and verbose modes.

  • Development links can be rebuilt in one command. relink removes and recreates the configured game, gamedata, and log junctions, with an explicit force option when an existing target must be replaced.

  • Development commands locate Call of Pripyat through Steam. XRF resolves Steam app 41700 automatically and uses the configured game path as a fallback.

  • The CLI can format LTX files. The first formatter normalized line endings and surrounding whitespace.

  • Existing translation XML can be imported. The translation workflow converts files or directories back into XRF JSON, detects or accepts the source encoding, and preserves multiline values.

  • The development panel gains world-control tools. It can spawn squads and monsters, change actor relations, dump the merged system.ini, and teleport among levels, patrols, and positions.

  • Runtime events include game start and game-time timers. Modules can subscribe to game start and register cancellable one-shot timeouts or repeating intervals driven by active game time.

  • Static resource builds skip unchanged files. The builder compares source and target metadata before copying. It does not remove stale target files discovered by the comparison.

  • ALife catches up briefly after actor reinitialization. XRF permits all ALife objects to update for the first three seconds after actor reinitialization, then restores the configured limit of 20 objects per update. This initializes the world promptly while retaining the normal update budget.

  • Nearby weapon fire can put NPCs on alert. XRF passes heard sounds to the stalker danger controller. Eligible NPCs enter danger and move toward a nearby hostile shooter, or assist an ally engaging a mutual enemy. Vanilla’s hearing callback only evaluates configured on_sound transitions.

  • Uncompressed game packages can copy the complete built tree. The pack workflow defaults to building and compressing, with explicit opt-out flags for loose-development packages.

April 2023

  • The CLI uses one structured command surface. Build, engine, link, log, open, parse, start, and verification tools share subcommands, help text, and option parsing instead of being invoked as unrelated scripts.

  • The CLI can create compressed packages and complete game builds. The first package workflow can build gamedata, compress database archives, and copy the configured engine and root payload into a distributable game.

  • Packaging distinguishes mods from complete games. pack mod produces a mod-only layout, while pack game includes the configured standalone game payload. Complete game packages use the bundled gold engine by default unless another variant is selected.

  • The development dialog gains modular inspection tools. Its sections inspect planner, inventory, registry, relation, scheme, weather, and ALife state and can spawn test items.

  • Runtime state uses a central save lifecycle. Managers serialize through the save manager instead of embedding save/load orchestration in the actor binder.

  • Surge shelter searches are level-scoped. The surge manager initializes the current level’s cover list and compares squared distances instead of scanning every configured shelter for each lookup.

  • Gameplay options expose language and OpenXRay pickup controls. Players can change language and toggle simplified pickup, multi-item pickup, and automatic magazine unloading after pickup.

  • The widescreen minimap has a corrected layout. XRF supplies separate 16:9 frame and background dimensions instead of reusing the standard-aspect form.

  • Optimized packages can remove Lua logging. pack --optimize builds scripts with Lua logger calls stripped from the generated output.

  • Game packages can ship compressed databases with only required loose runtime files. When compression is selected, the packer copies database archives and an explicit runtime gamedata allowlist.

  • Build filters can target generated assets. build --filter first targeted generated forms and configs; June expanded it to static UI, configs, and resources.

March 2023

  • Runtime and CLI code can be tested outside the game. The Jest harness supplies Lua and X-Ray stand-ins, while Fengari preserves important native Lua behavior for engine-facing TypeScript tests.

  • The project is renamed from XRTS to XRF. Commands, documentation, artifact metadata, and repository references adopt the X-Ray Forge name.

  • Automated workflows build and validate XRF artifacts. CI runs project checks and can publish assembled gamedata archives with source-revision metadata.

  • LTX configuration can be generated from typed TypeScript. The builder supports sections, imports, root fields, and engine binding expressions, allowing generated configs to share constants and helpers.

  • Translations are built from a shared multi-locale source. String tables are maintained in JSON and rendered to locale-specific engine XML with the required Windows-1251 encoding.

  • Scheme transitions use direct condition dispatch. Active logic resolves each transition type and calls its mapped handler instead of traversing vanilla’s long sequence of Lua-pattern branches.

February 2023

  • The script runtime is authored in TypeScript. XRF completed the migration of the vanilla Lua layer to TypeScript compiled through TypeScriptToLua, enabling source typechecking while still emitting Lua for X-Ray.

  • Decorated TypeScript classes can implement engine-facing luabind classes. The compiler bridge emits the constructors, inheritance, methods, and properties expected by X-Ray.

  • Large binary and resource payloads are versioned independently. Repository setup pins cli/bin and src/resources as submodules while the authored engine source remains in the main repository.

  • Builds can layer separately versioned resources. Configured base, override, and locale roots are merged into the assembled gamedata. Locale selection and the option to omit additional override roots followed in April.

January 2023

  • The CLI can verify a local XRF development setup. verify project checks the project configuration, game executable, selected engine, gamedata and log links, and configured resource roots.

  • The CLI can serialize a directory tree as JSON. The parse command provides a machine-readable inventory for resource and generated-data maintenance.

  • Runtime systems communicate through typed game events. Managers and extensions can subscribe, unsubscribe, and emit lifecycle and gameplay events without direct coupling.

December 2022

  • The XRF project CLI assembles authored sources into runnable gamedata. Its first build pipeline compiles scripts and generates or copies configs, translations, UI forms, and resources into target/gamedata.

  • Builds include output metadata. XRF records build flags and timing, host information, file counts, sizes, and the produced file list beside the assembled gamedata. Source-revision metadata followed in March 2023.

  • The CLI manages a local Call of Pripyat development install. Developers can link gamedata and logs, start the game, inspect logs, open configured folders, and list, select, inspect, or restore bundled engines. The initial engine set included release, gold, and later mixed.

  • The main menu includes a development dialog. F11 opens a debug panel with runtime-inspection and developer-control sections.

  • UI XML can be generated from reusable TSX forms. Project-owned layouts are expressed as typed JSX components and rendered to engine XML during the build.

Credits

XRF builds on work from the S.T.A.L.K.E.R. and OpenXRay modding ecosystem.

This community project is not affiliated with GSC Game World.

Installation

This guide prepares a local xrf-engine checkout, connects it to an installed game, and starts XRF.

Requirements

  • Windows.
  • Node.js and npm.
  • Git with submodule support.
  • An installed copy of S.T.A.L.K.E.R.: Call of Pripyat.

The CLI looks for Steam app 41700. For a non-Steam installation, set targets.stalker_game_fallback_path in cli/config.json.

Set up the project

Clone the engine repository and install dependencies:

git clone https://github.com/xray-forge/xrf-engine.git
cd xrf-engine
npm install
npm run setup

npm run setup initializes the src/resources and cli/bin submodules used for assets, engines, and tools.

Run the project verification before linking:

npm run verify

Create development links for the game folder, target/gamedata, and logs:

npm run cli -- link

List the bundled engines and select one:

npm run cli -- engine list
npm run cli -- engine use release

engine rollback restores the original game executable.

Build and start

Build the gamedata output:

npm run build

Start the configured game executable:

npm run cli -- start_game

For faster testing, the start command can create or load a session directly:

npm run cli -- start_game --new --no-intro
npm run cli -- start_game --load quicksave

The build output is written to target/gamedata. Do not edit files there by hand; edit sources under src/engine and rebuild.

Update submodules

Update submodules when resource or binary repositories change:

git submodule update --init --recursive

Use a local OpenXRay build

For a locally built OpenXRay executable, follow the OpenXRay Windows build guide, then configure or copy the executable into the engine location used by the CLI. Use npm run cli -- engine info to inspect the currently selected engine.

XRF Engine

XRF is a TypeScript implementation of the Call of Pripyat script layer. The build compiles it to Lua and assembles configs, UI XML, translations, and static resources into target/gamedata.

The engine repository also includes a local Node CLI for common modding tasks:

  • building target/gamedata;
  • linking the project to a local game installation;
  • switching bundled OpenXRay engine binaries;
  • verifying project data;
  • packing mod or game distributions;
  • formatting and checking generated data sources.

Source Layout

The main engine source is under src/engine:

  • scripts contains TypeScript entry points that become Lua scripts.
  • core contains runtime systems, schemes, managers, objects, and utilities.
  • configs contains static and generated LTX/XML config sources.
  • forms contains JSX/XML UI form sources.
  • translations contains JSON and XML translation sources.
  • extensions contains optional gameplay modules.

Build output and generated artifacts go under target/.

Development model

Edit source files, then build. target/ is generated output; do not edit it by hand.

Building

Build XRF into target/gamedata before testing, linking, or packaging.

npm run build
npm run cli -- build

Targets

build runs every target by default:

  • scripts
  • externs
  • ui
  • configs
  • translations
  • resources

By default, build runs all targets. Use --include for focused builds and --exclude to skip targets:

npm run cli -- build --include scripts configs
npm run cli -- build --include externs
npm run cli -- build --exclude resources

Options

  • -i, --include <targets...>: build only selected targets.
  • -e, --exclude <targets...>: build all targets except selected targets.
  • -v, --verbose: print verbose build logs.
  • -l, --language <language>: build with a locale from cli/config.json.
  • -f, --filter <patterns...>: regular-expression filters for source paths.
  • -c, --clean: remove target/gamedata before building.
  • --nl, --no-lua-logs: strip Lua logger calls from the compiled script output.
  • --na, --no-asset-overrides: skip configured override and locale resource roots.
  • --itz, --inject-tracy-zones: inject Tracy profiling zones into compiled scripts.

--filter requires an explicit --include; it cannot be used with the default all-target build.

What a full build does

When all targets run, the build performs these steps:

  1. optionally clean target/gamedata;
  2. compile TypeScript scripts to Lua;
  3. generate the extern manifest and copy it to target/gamedata/extern.json;
  4. render dynamic UI forms and copy static UI XML;
  5. render dynamic configs and copy static LTX/XML configs;
  6. build translations;
  7. copy static resources;
  8. write metadata.json;
  9. collect the build log.

Examples

npm run cli -- build
npm run cli -- build --clean
npm run cli -- build --include ui
npm run cli -- build --include externs
npm run cli -- build --include configs --filter system.ltx
npm run cli -- build --exclude resources
npm run cli -- build --no-lua-logs --inject-tracy-zones

Output

The command writes target/gamedata, target/gamedata/metadata.json, target/gamedata/extern.json, and target/xrf_build.log. Change source files and rebuild rather than editing generated output.

Build Clean

build --clean removes the previous target/gamedata tree, then runs the selected targets.

Use a clean build when generated output may contain stale files, for example after deleting or renaming scripts, configs, forms, translations, or resources:

npm run cli -- build --clean
npm run cli -- build -c

It never changes src/engine, src/resources, or configured external resource repositories.

When to use it

Use a clean build after:

  • deleting or renaming source files;
  • changing build filters and wanting to remove output from previous filters;
  • switching between resource sets or generated config layouts;
  • preparing output before compress, pack game, or pack mod.

Skip it during tight iteration when the selected step deterministically overwrites the file you changed.

Building Scripts

The scripts target compiles entry points in src/engine/scripts and their imports to Lua with TypeScriptToLua.

The output is written into target/gamedata as X-Ray script files. The generated bundle also includes lualib_bundle.script, which provides helper functions emitted by TypeScriptToLua.

Build scripts

npm run cli -- build --include scripts
npm run cli -- build -i scripts

Use --no-lua-logs when you want compiled scripts without Lua logger calls:

npm run cli -- build -i scripts --no-lua-logs

Use --inject-tracy-zones when profiling with Tracy support:

npm run cli -- build -i scripts --inject-tracy-zones

Watch mode

Use watch mode during script development:

npm run watch:scripts

Additional script watch commands are available for optimized and Tracy-instrumented builds:

npm run watch:scripts-optimized
npm run watch:scripts-tracy
npm run watch:scripts-tracy-optimized

Type checking

Run TypeScriptToLua type checking without emitting files:

npm run typecheck

When script compilation reports diagnostics, use typecheck for a focused failure report.

Type Definitions

X-Ray engine APIs are exposed through TypeScript declarations from the xray16 package and the XRF typedefs under src/typedefs. Use the generated type documentation when checking engine class, method, and enum names:

Luabind classes

XRF uses custom TypeScriptToLua transforms for luabind-style classes. Classes that need engine-compatible luabind registration use project decorators and generated Lua class shapes instead of plain TypeScriptToLua metatable classes.

Building the extern manifest

Build externs after changing an extern(...) declaration or its JSDoc.

npm run cli -- build --include externs

Output

  • src/engine/declarations/extern.json — tracked contract.
  • target/gamedata/extern.json — packaged sidecar for gamedata tools.

The game does not load either file. Do not edit them by hand. Manifest source paths are relative to the declarations root.

Check the tracked manifest

npm run cli -- verify externs

This compares declarations with the tracked manifest without writing. Regenerate when it reports drift.

Requirements

Missing or unrenderable callable types are emitted as unknown. Values need value as Type. The build fails for duplicates, dynamic names, and unsupported declaration shapes.

Building UI

The UI target writes game UI XML to target/gamedata/configs/ui from src/engine/forms:

  • .ts and .tsx files that export create() are rendered to .xml with renderJsxToXmlText;
  • static .xml files are copied as-is;
  • *.test.* files are ignored by the replication helper.

Build UI

npm run cli -- build --include ui
npm run cli -- build -i ui

Use a filter when you only need a subset of files:

npm run cli -- build -i ui --filter main_menu

Filters are regular-expression strings matched against source file paths.

Authoring forms

Dynamic forms use JSX-compatible TypeScript. The generated output is XML for the game engine, so form layout still has to respect X-Ray UI constraints such as absolute coordinates and parent-relative positioning.

Look at existing files in src/engine/forms before adding new forms. Reuse shared helpers and components where they already exist.

Aspect ratios

X-Ray UI XML often has separate layout expectations for 16:9 and 4:3 modes. Keep generated forms compatible with the target screen mode and verify in game when changing layout-sensitive XML.

Building Configs

Build configs into target/gamedata/configs from src/engine/configs.

The target processes:

  • dynamic .ts files export create() or config and render to .ltx;
  • dynamic .tsx files export create() and render to .xml;
  • static .ltx and .xml files are copied as-is;
  • *.test.* files are ignored.

Build configs

npm run cli -- build --include configs
npm run cli -- build -i configs

Use filters for focused rebuilds:

npm run cli -- build -i configs --filter system.ltx

Filters are regular-expression strings matched against source file paths. The build command does not allow filters with the implicit all target, so combine --filter with --include.

Source types

Dynamic LTX configs use structured TypeScript descriptors and renderJsonToLtx. This is useful when a config needs shared constants, loops, generated sections, or tests.

Static LTX is still appropriate for simple files that do not need build-time logic.

Dynamic XML configs use JSX and renderJsxToXmlText. UI XML is separate: it is built from src/engine/forms.

Validation

Use the LTX verifier after config changes:

npm run cli -- verify ltx

Building Translations

The translations target writes game string tables to target/gamedata/configs/text from src/engine/translations through the bundled XRF tools binary.

Build translations

npm run cli -- build --include translations
npm run cli -- build -i translations

Selecting a Language

The default locale is configured in cli/config.json as locale. The current default is ukr.

Override it for a build with --language:

npm run cli -- build --include translations --language eng

Supported locale keys are listed in cli/config.json under available_locales.

Locale Resource Packs

Resource packs for voice and localized assets are configured under resources.mod_assets_locales in cli/config.json. When asset overrides are enabled, the resources build includes override folders plus the locale folders for the selected language.

Sources

XRF uses JSON translation sources for generated multilingual output and can also copy static XML translation files where they are part of the source tree. Use the translation CLI commands when converting existing game XML into JSON sources.

Building Resources

The resources target copies static game assets into target/gamedata.

The base resource root is configured in cli/config.json as resources.mod_assets_base_folder and points to src/resources. Additional override and locale roots can be configured in the same file.

Build resources

npm run cli -- build --include resources
npm run cli -- build -i resources

Use filters for focused resource copies:

npm run cli -- build -i resources --filter textures

Filters are regular-expression strings matched against source file paths.

Incremental builds

Directory resources are compared before copy. Non-clean builds skip unchanged files.

Additional Assets

Clone configured resource repositories with:

npm run cli -- clone --list
npm run cli -- clone extended
npm run cli -- clone locale-ukr

The relevant configuration keys are:

  • resources.mod_assets_override_folders for general overrides;
  • resources.mod_assets_locales for locale-specific assets.

Disable override roots for a build with --no-asset-overrides:

npm run cli -- build --no-asset-overrides

CLI

The engine repository includes a local Node CLI. Run it from the repository root:

Use the npm wrapper:

npm run cli -- <command>

Common package scripts wrap frequently used commands:

npm run build
npm run verify
npm test

Commands

It builds and packages the project, manages resources and engine binaries, links a local game, and handles common format, spawn, particle, translation, and verification tasks.

See the command list for a quick index.

Paths and output

Run commands from the repository root unless a page says otherwise. Most defaults are repository-relative and come from cli/config.json.

Generated output belongs under target/: built gamedata, parsed helper files, coverage, packed archives, and package output. Source edits belong under src/engine, src/resources, cli, or the relevant external resource repository.

Installed command

package.json exposes the binary name xrf, but local development should prefer npm run cli -- ... so the command uses the repository version and local dependencies.

If the package is installed globally, the equivalent shape is:

xrf build
xrf verify project

Configuration

Most CLI defaults live in cli/config.json: locale, resource roots, build source paths, target paths, compression tools, package roots, and game executable settings.

When a command cannot find the game, resources, or generated output, check the command page first and then inspect the matching config key in cli/config.json.

CLI Configuration

The engine CLI reads project defaults from cli/config.json.

This file controls:

  • default locale and supported locale keys;
  • base, override, and locale resource roots;
  • source paths for configs, scripts, translations, extensions, and UI forms;
  • target output path;
  • compression and helper binary paths;
  • default package engine and root package assets;
  • game executable and fallback game path.

Locale

locale is the default language used by builds. available_locales defines the accepted locale keys.

Override the locale for a build with:

npm run cli -- build --language eng

Locale-specific resource roots are configured in resources.mod_assets_locales.

Resources

resources.mod_assets_base_folder points to the base resources folder. In the engine repo this is src/resources.

resources.mod_assets_override_folders lists additional asset roots. resources.mod_assets_locales maps locale keys to locale resource roots. These roots are included by the resources build unless --no-asset-overrides is passed.

Build Paths

The build section maps source folders:

  • configs -> src/engine/configs
  • scripts -> src/engine/scripts
  • translations -> src/engine/translations
  • extensions -> src/engine/extensions
  • ui -> src/engine/forms

Generated output goes under target/gamedata.

Game path

The targets section contains the Steam app id, fallback game path, and executable name. Set stalker_game_fallback_path when the CLI cannot locate a non-Steam installation.

Commands and scripts

Run package scripts from the repository root:

npm run <script>

Run CLI commands through the local wrapper:

npm run cli -- <command>

Package scripts

  • setup: initialize and update submodules.
  • verify: run verify project.
  • build: build scripts, configs, UI, translations, and resources.
  • pack:mod: build a mod package.
  • pack:game: build a game package.
  • watch:scripts: rebuild scripts when TypeScript sources change.
  • typecheck: run TypeScriptToLua type checking without emitting files.
  • typecheck:tests: type-check test sources with TypeScript.
  • lint: run ESLint with the full repository rule set.
  • test: run Jest.
  • test:coverage: run Jest coverage.
  • format: rewrite Markdown, TypeScript, and LTX files using the configured formatters.
  • help: print CLI help.

CLI Commands

  • build: build target/gamedata.
  • clone: clone configured additional resource repositories.
  • compress: compress built gamedata into archives.
  • engine: inspect, switch, list, or roll back bundled engines.
  • format ltx: format LTX files.
  • icons: pack and unpack equipment icons and texture descriptions.
  • link, unlink, relink: manage project links to the local game installation.
  • logs: print the last lines from the linked game log.
  • open_game_folder, open_project_folder: open configured folders.
  • pack: create mod or game packages.
  • particles: pack or unpack particles.xr.
  • parse: parse directory trees or game externals.
  • spawn: unpack ALife spawn files.
  • start_game: start the configured game executable.
  • translations: initialize, convert, and check translation files.
  • verify: run project, gamedata, LTX, and particles verification commands.

Use npm run cli -- <command> --help for current command-specific options.

Build

build compiles and copies XRF source assets into target/gamedata. Use it before linking the project into a game installation, packaging a mod, or validating generated configs/scripts.

npm run cli -- build

What it builds

TargetSourceOutput
scriptssrc/engine/scriptsLua .script files in target/gamedata/scripts and related output folders.
externssrc/engine/declarationssrc/engine/declarations/extern.json and target/gamedata/extern.json.
uisrc/engine/forms plus static UI XMLUI XML under target/gamedata/configs/ui.
configssrc/engine/configsStatic and generated config files under target/gamedata/configs.
translationssrc/engine/translationsXML string tables under target/gamedata/configs/text.
resourcesconfigured resource rootsStatic assets copied into target/gamedata.

target/gamedata/extern.json is metadata for gamedata tools; the game runtime does not load it. The build also writes target/gamedata/metadata.json and stores the build log in target/xrf_build.log.

Options

  • -i, --include <targets...>: build only selected targets. Choices are scripts, externs, ui, configs, translations, and resources.
  • -e, --exclude <targets...>: skip selected targets. This conflicts with --include.
  • -f, --filter <targets...>: filter copied/generated files by regular-expression strings. Use it only with a specific included target, not with the default all target set.
  • -l, --language <language>: use a locale from cli/config.json. The default is ukr.
  • -c, --clean: remove target/gamedata before building.
  • --nl, --no-lua-logs: strip Lua logger calls from the compiled script output.
  • --na, --no-asset-overrides: skip configured override and locale resource roots when copying resources.
  • --itz, --inject-tracy-zones: inject Tracy profiling zones while compiling scripts.
  • -v, --verbose: print verbose build logs.

Examples

npm run cli -- build --clean
npm run cli -- build --include scripts configs
npm run cli -- build --include externs
npm run cli -- build --include configs --filter system.ltx
npm run cli -- build --exclude resources
npm run cli -- build --include scripts --no-lua-logs

Failure notes

  • Unsupported locales fail before asset copying starts.
  • --filter with the default all include set fails; choose a concrete target first.
  • TypeScriptToLua diagnostics fail the script build. Run npm run typecheck for a focused error list.

Clone

clone downloads optional resource repositories configured in cli/config.json. The command clones into the parent folder of the engine repository, next to xrf-engine.

npm run cli -- clone <repository>

Repository names

Use --list to print the configured names:

npm run cli -- clone --list

Current configured names include:

  • extended: full base gamedata assets for custom game packages;
  • locale-eng: English locale resources;
  • locale-ukr: Ukrainian locale resources;
  • locale-rus: Russian locale resources.

Options

  • -l, --list: print configured repository names and exit.
  • -v, --verbose: print verbose logs.
  • -f, --force: remove an existing target folder before cloning.
  • -s, --safe: treat an already cloned target folder as success.

--force and --safe conflict with each other.

Examples

npm run cli -- clone extended
npm run cli -- clone locale-ukr --safe
npm run cli -- clone extended --force

Failure notes

The command fails when the repository name is missing, not configured, or already cloned without --safe or --force. It runs git clone, so network and credentials errors come from Git.

Compress

compress packs built target/gamedata content into database archives with xrf-cli pack-archive. Use it after build when you need DB archives for a game package.

npm run cli -- compress

Targets

Compression target definitions live in cli/compress/configs/compress.json.

TargetPacked content
configsconfigs, spawns, anims, and ai.
levelslevels.
resourcestextures and meshes.
shadersshader-related .xr files and the shaders folder.
soundssounds, stored without compression.

Output archives are written under target/db as <target>.dbN.

Options

  • -i, --include <targets...>: compress selected targets. Defaults to all.
  • -c, --clean: remove target/db before writing archives.
  • -v, --verbose: print packing output.

Examples

npm run cli -- build --clean
npm run cli -- compress --clean
npm run cli -- compress --include configs shaders

Inputs and output

The command reads built files from target/gamedata and writes database archives under target/db. It does not build missing gamedata by itself, so run build first when source files changed.

Use --include for fast package checks when only one archive group changed. Use --clean before producing a package that should not contain stale archives from a previous target set.

Failure notes

target/gamedata must exist. If compression target names are wrong, the command prints the valid names from the compression config.

Engine

engine manages bundled OpenXRay engine binaries under cli/bin/engines. It switches the game installation’s bin folder to a selected bundled engine by creating a junction.

npm run cli -- engine <command>

Commands

CommandPurpose
engine listPrint available bundled engine names.
engine infoPrint whether the active game bin folder is linked and whether a backup exists.
engine use <engine>Switch the configured game installation to a bundled engine.
engine rollbackRestore the backed-up original bin folder.

Examples

npm run cli -- engine list
npm run cli -- engine use release
npm run cli -- engine info
npm run cli -- engine rollback

How switching works

When engine use <engine> sees an unlinked game bin folder, it renames it to the XRF backup path and then links the selected bundled engine. When the current bin is already linked, it removes the old link and creates a new one.

engine rollback only restores the backup when a backup folder exists and the current bin folder is an XRF-linked engine with bin.json.

pack game --engine <type> selects a bundled engine for package output without switching the local game installation.

Format

format contains project-specific formatting commands. The current CLI subcommand formats LTX config sources with the same formatter used by validation tooling.

npm run cli -- format ltx

Options

  • -c, --check: check formatting without rewriting LTX files.
  • -v, --verbose: print verbose formatter logs.

Examples

npm run cli -- format ltx
npm run cli -- format ltx --check

Inputs and output

format ltx reads LTX config sources from the repository and either rewrites them or reports formatting differences when --check is set. It is useful before verify ltx because it normalizes layout without changing config meaning.

Run the check form in review or CI-style validation. Run the writing form when you intentionally want the formatter to update files.

Package script difference

The package script is broader:

npm run format

It runs Prettier for JavaScript, TypeScript, TSX, and Markdown, then ESLint fix, then npm run cli -- format ltx. Use the CLI command when you only want LTX formatting.

Icons

icons wraps texture tooling from cli/bin/tools/xrf-cli. It packs and unpacks equipment icon sprites and UI texture description sprites using project paths from cli/config.json.

npm run cli -- icons <command>

Commands

CommandReadsWrites
icons unpack-equipmentsrc/resources/textures/ui/ui_icon_equipment.dds and src/engine/configs/system.ltxsrc/resources/textures_unpacked/ui/ui_icon_equipment
icons pack-equipmentunpacked equipment icons and system.ltxsrc/resources/textures/ui/ui_icon_equipment.dds
icons unpack-descriptionsUI texture descriptions and packed texturessrc/resources/textures_unpacked
icons pack-descriptionsUI texture descriptions and unpacked texturessrc/resources/textures

Options

All icon commands support:

  • -v, --verbose: print verbose logs.
  • -s, --strict: enable strict mode.

Description commands also support:

  • -d, --description <name>: process one file under src/engine/forms/textures_descr.

Examples

npm run cli -- icons unpack-equipment
npm run cli -- icons pack-equipment --strict
npm run cli -- icons unpack-descriptions --description ui_actor.xml
npm run cli -- icons pack-descriptions --description ui_actor.xml

Workflow

Unpack before editing icon sprites or checking generated sprite coordinates. Pack after editing the unpacked files or texture descriptions. Use --description when working on a single UI texture description file instead of the whole description set.

Equipment commands are tied to the equipment icon atlas. Description commands are tied to XML texture description files under src/engine/forms/textures_descr.

Failure notes

Equipment commands depend on valid system.ltx icon coordinates. Description commands depend on XML description names and matching source textures.

Link

link, unlink, and relink connect the build output and game folders used during local development.

npm run cli -- link

What gets linked

LinkSourceDestination
Game linkconfigured game roottarget/game_link
Gamedata linktarget/gamedataconfigured game gamedata folder
Logs linkconfigured game logs foldertarget/logs_link

Game paths come from Steam detection or the fallback values in cli/config.json.

Commands

  • link: create the game, gamedata, and logs links.
  • unlink: remove the gamedata link, logs link, and game link.
  • relink: run unlink, then link.

link and relink support -f, --force to remove existing link targets first. Use it carefully: if a real game gamedata folder exists, --force removes it before creating the link.

Examples

npm run cli -- build --clean
npm run cli -- link
npm run cli -- relink --force
npm run cli -- unlink

Failure notes

If linking fails, verify the configured game path, executable name, and Steam installation detection. verify project checks the same paths.

Lint

Linting is exposed through package scripts, not a Commander subcommand. Use it for TypeScript, TSX, and CLI source checks. The default command applies the full repository rule set, including unused import and local variable checks.

npm run lint

Commands

ScriptPurpose
npm run lintRun ESLint with the full repository rule set.

The command stores its cache under target/eslint/cache.json.

Examples

npm run lint
npm run lint -- --fix

Inputs and output

Lint reads TypeScript, TSX, and JavaScript sources from the repository and reports rule violations to the terminal. It reuses the ESLint cache, so repeated runs after small edits are faster than a cold run.

Use npm run typecheck for TypeScriptToLua script type checks and npm run typecheck:tests for test TypeScript checks. Use npm test for Jest.

Logs

logs prints the tail of the active engine log from the configured game logs folder.

npm run cli -- logs
npm run cli -- logs 100

Behavior

The optional argument is the number of lines to print. Invalid values fall back to 15, and the command caps output at 200 lines.

The command detects the log file from the configured game paths:

  • when bin/bin.json exists, it expects openxray_<username>.log;
  • otherwise it expects xray_<username>.log.

It reads from the real game logs folder. If you ran link, the same folder is also reachable through target/logs_link.

Examples

npm run cli -- logs
npm run cli -- logs 50
npm run cli -- logs 500

The last example still prints at most 200 lines.

When to use it

Use logs after start_game or a manual game launch to inspect the newest script/runtime messages without navigating to the game logs folder. It is the fastest check after a crash during startup, a failed script reload, or a missing file reported by the engine.

For longer inspection, open the linked logs folder or the real game logs directory and use an editor that can follow file updates.

Failure notes

If no active log is found, start the game once, check the configured game path, or run npm run cli -- link to create the logs link for easier inspection.

Open

Open commands launch the system file explorer for common project and game folders.

npm run cli -- open_game_folder
npm run cli -- open_project_folder

Commands

CommandOpens
open_game_folderThe configured or detected S.T.A.L.K.E.R. game root.
open_project_folderThe xrf-engine repository root.

open_game_folder uses the same game path resolution as link, logs, start_game, and verify project. open_project_folder uses the repository root detected from the CLI process location.

Examples

npm run cli -- open_game_folder
npm run cli -- open_project_folder

When to use it

Use open_game_folder when checking linked gamedata, engine logs, or installed game binaries after build and link. Use open_project_folder when a script or tool printed a project-relative path and you want to inspect the source tree from Explorer.

These commands do not build, link, or verify files. They only open the resolved folders.

Failure notes

If the game folder does not open, check cli/config.json under targets or run npm run cli -- verify project to see which path is being resolved.

Pack

pack creates distributable mod or game folders from the project.

npm run cli -- pack <type>

<type> must be mod or game.

Output

TypeOutput folderContents
modtarget/mod_packagegamedata, and optionally bundled engine binaries.
gametarget/game_packageengine bin, root assets, gamedata, and optionally compressed db archives.

Options

  • --nb, --no-build: package already built assets without running build.
  • --nc, --no-compress: for game packages, skip archive compression and copy all gamedata.
  • --na, --no-asset-overrides: pass through to build and skip override/locale resource roots.
  • -e, --engine <type>: use a bundled engine from cli/bin/engines.
  • --se, --skip-engine: do not include engine binaries. This is allowed for mod packages and rejected for game.
  • -o, --optimize: build scripts without Lua logs.
  • -v, --verbose: print verbose logs.
  • -c, --clean: remove the package output folder first.

Examples

npm run cli -- pack mod --clean
npm run cli -- pack mod --skip-engine --no-build
npm run cli -- pack game --clean --optimize
npm run cli -- pack game --engine release
npm run pack:mod
npm run pack:game

Failure notes

Game packages require a valid bundled engine. Compressed game packages also require successful build and compression steps, because target/db is copied into the package.

Particles

particles wraps the external XRF tools binary for packing and unpacking particles.xr.

npm run cli -- particles <command>

Commands

CommandDefault inputDefault output
particles unpacksrc/resources/particles.xrsrc/resources/particles_unpacked
particles packsrc/resources/particles_unpackedsrc/resources/particles.xr

Options

Both subcommands support:

  • -p, --path <path>: source file or source directory.
  • -d, --dest <dest>: output file or output directory.
  • -v, --verbose: print verbose logs.
  • -f, --force: remove an existing output before writing.

Examples

npm run cli -- particles unpack
npm run cli -- particles unpack --force
npm run cli -- particles pack
npm run cli -- particles pack --path src/resources/particles_unpacked --dest src/resources/particles.xr

Workflow

Unpack first when you need to inspect or edit particle definitions as files. Pack after edits to rebuild particles.xr for resources or packaging. Use --force when replacing a previous unpacked folder or packed output.

The command delegates to the bundled XRF tools binary. If you need lower-level particle conversion commands outside the engine repository defaults, use the Tools CLI particle commands directly.

npm run cli -- verify particles-packed
npm run cli -- verify particles-unpacked

Parse

parse contains utility commands that generate JSON or HTML support files under target/parsed.

npm run cli -- parse <command>

Commands

CommandPurposeOutput
parse dir_as_json <path>Flatten a directory tree into a JSON object keyed by normalized file names.target/parsed/<folder>.json

dir_as_json resolves <path> relative to the repository root.

Options

parse dir_as_json supports:

  • -e, --no-extension: omit file extensions in JSON values.

Examples

npm run cli -- parse dir_as_json src/resources/textures
npm run cli -- parse dir_as_json src/resources/textures --no-extension

Output usage

Use dir_as_json when another script needs a compact index of files under a resource folder. The command writes generated support data under target/parsed, so treat the result as disposable build output.

Use xrf-cli export-externs when checking the script declaration surface exposed by conditions, effects, and dialogs.

Failure notes

dir_as_json requires a path argument.

Spawn

spawn contains ALife spawn file utilities. The engine CLI currently exposes the unpack workflow.

npm run cli -- spawn unpack

Defaults

FieldDefault
Sourcesrc/resources/spawns/all.spawn
Destinationtarget/all_spawn

The command delegates to cli/bin/tools/xrf-cli unpack-spawn.

Options

  • -p, --path <path>: source spawn file path.
  • -d, --dest <dest>: output directory.
  • -v, --verbose: print verbose logs.
  • -f, --force: remove an existing unpacked destination before writing.

Examples

npm run cli -- spawn unpack
npm run cli -- spawn unpack --force
npm run cli -- spawn unpack --path src/resources/spawns/all.spawn --dest target/all_spawn

Output

The output directory contains the unpacked spawn representation produced by the XRF tools CLI. The engine wrapper is intended for inspection and verification workflows in this repository; it does not expose pack or repack commands.

Keep generated unpack output under target/ unless you are intentionally preparing source data for another tool.

Failure notes

The source spawn file must exist. Use the Tools CLI spawn commands when you need lower-level spawn info, pack, repack, or verification operations.

Start

start_game starts the configured game executable.

npm run cli -- start_game

The executable path is resolved from the same game-path logic used by link, open_game_folder, logs, and verify project. Configuration lives in cli/config.json under targets:

  • stalker_game_steam_id: Steam app id used for automatic detection;
  • stalker_game_fallback_path: fallback game folder when Steam detection is not enough;
  • stalker_app_path: executable name inside the game folder.

Typical workflow

npm run cli -- build --clean
npm run cli -- link
npm run cli -- start_game

Inputs and output

The command reads the configured game target and launches the executable from that folder. It does not rebuild scripts, copy gamedata, or wait for the engine process to finish.

Use it after build and link when you want to test the currently linked project output. Use logs after the game starts if you need the script engine error output.

Failure notes

If the process does not start, run npm run cli -- verify project and check the resolved game folder and executable. The command starts the executable asynchronously, so later runtime errors are written to the engine log rather than to the CLI process.

Test

Tests are run through package scripts rather than a Commander subcommand. The project uses Jest with the config at cli/test/jest.config.ts.

npm test

Common commands

CommandPurpose
npm testRun the Jest suite.
npm test -- <path-or-pattern>Run focused tests.
npm run test:coverageRun Jest with coverage output.
npm run typecheckRun TypeScriptToLua type checking without emitting scripts.
npm run typecheck:testsType-check test sources with TypeScript.

Coverage output is written under target/coverage_report.

Examples

npm test
npm test -- src/engine/scripts/register.test.ts
npm run test:coverage
npm run typecheck
npm run typecheck:tests

Notes

Runtime tests use fixtures and mocks under src/fixtures for X-Ray APIs, Lua behavior, engine helpers, and CLI utilities. Use focused Jest paths first when changing a specific manager, scheme, binder, or CLI helper.

The typecheck commands do not execute tests. They catch TypeScript and TypeScriptToLua issues that can pass Jest when a mocked runtime path is not exercised. For gameplay logic changes, run the focused Jest test and npm run typecheck before broader validation.

Translations

translations contains helper commands for JSON translation sources and original X-Ray XML string tables.

npm run cli -- translations <command>

Commands

CommandPurpose
translations init <path>Add all configured language keys to a JSON translation file or folder.
translations to_json <path>Convert XML string table files or folders into JSON translation sources.
translations checkVerify the project translation sources under src/engine/translations.

The configured languages come from cli/config.json. Current locales are eng, fra, ger, ita, pol, rus, spa, and ukr.

Options

init supports:

  • -v, --verbose: print verbose logs.

to_json supports:

  • -l, --language <locale>: language key to fill from the XML text.
  • -c, --clean: clean the output path before writing.
  • -o, --output <path>: output file or directory. Defaults to target/parsed.
  • -e, --encoding <encoding>: source XML encoding. If omitted, the command tries to read the XML header and then uses locale defaults.
  • -v, --verbose: print verbose logs.

check supports:

  • -l, --language <locale>: check one language instead of all languages.
  • -s, --strict: fail on missing entries.
  • -v, --verbose: print verbose logs.

Examples

npm run cli -- translations init src/engine/translations/st_dialogs.json
npm run cli -- translations to_json configs/text/eng --language eng --output src/engine/translations
npm run cli -- translations check
npm run cli -- translations check --language ukr --strict

Failure notes

to_json requires a known locale. When converting a folder, the output path must be a directory, not a single file.

Verify

verify validates project setup and generated data.

npm run cli -- verify <command>

npm run verify runs verify project.

Commands

CommandChecks
verify projectProject setup and links.
verify gamedataAssembled target/gamedata.
verify externsTracked extern manifest.
verify ltxLTX structure and $scheme values.
verify particles-packedPacked particles.xr.
verify particles-unpackedUnpacked particle files.

Options

  • verify gamedata -c, --checks <checks...>: run only the listed checks instead of all of them.
  • verify gamedata -r, --report <report>: write the structured verification report as JSON.
  • verify gamedata -v, --verbose: print verbose external-tool logs.
  • verify gamedata -s, --strict: fully validate expensive asset payloads, including complete sound decoding.
  • verify ltx -v, --verbose: print verbose external-tool logs.
  • Particle verification commands support -v, --verbose.

Selecting checks

A full run validates everything and is slow. When iterating on one kind of asset, narrow it:

npm run cli -- verify gamedata --checks meshes weapons animations

Available checks: animations, levels, ltx, meshes, particles, particles-usage, scripts, shaders, sounds, spawns, textures, weapons, weathers. Unknown names are rejected before the tool runs.

Structured report

--report writes the findings as JSON instead of leaving them only in the log. Because the format is stable, a report from a known-good build can be kept as a baseline and later runs compared against it, which is more reliable than reading console output when a change is expected to alter some findings but not others.

npm run cli -- verify gamedata --checks weapons --report target/verify-weapons.json

Examples

npm run cli -- verify externs
npm run cli -- verify gamedata --verbose

Build gamedata before verify gamedata. verify externs does not write files. Regenerate a stale manifest with npm run cli -- build --include externs.

Failure notes

verify project reports setup problems without failing. Other checks fail on invalid data or tool errors.

Extensions

Extensions are optional modules discovered in gamedata/extensions. They keep gameplay changes separate from the core script layer.

At game startup, XRF scans folders containing main.script, restores their saved order and enabled state, and registers enabled modules.

Extension Entry Point

Each extension has its own folder under extensions and an entry file named main.script after build output.

An extension module can export:

  • register(isNewGame, extension): called when the extension is enabled; this is required for a usable module.
  • unregister(isNewGame, extension): optional cleanup hook called when the extension is disabled.
  • save(data): optional hook for extension dynamic data.
  • load(data): optional hook for restoring extension dynamic data.

The TypeScript sources for built-in extensions live under src/engine/extensions.

State and ordering

Loaded extensions are stored in the runtime registry by name. The load order and enabled state are saved to extensions_order.scopo in the game saves folder.

The main menu can reorder extensions and toggle those that opt in with canToggle.

Built-In Extensions

The current engine source includes these extension folders:

Source folderExtension nameDefault state
achievements_rewardsAchievement rewardsenabled
enhanced_items_dropEnhanced items drop (with upgrades)disabled
enhanced_location_progressionEnhanced location progressionenabled
enhanced_treasuresEnhanced treasuresenabled
original_start_positionOriginal start positiondisabled

The built-in modules are useful references when adding an extension:

Config Files

Extensions can open extension-local LTX files through the extension utilities. main.ltx is the default relative file name when no file name is provided.

Scope

XRF supports discovery, ordering, enablement, registry registration, and save/load hooks. It does not currently define extension dependencies, packaging metadata, or extension-specific build steps.

Achievement Rewards

Achievement rewards periodically fills two actor reward boxes after the relevant achievements are earned.

The source lives under src/engine/extensions/achievements_rewards.

Default state

The extension exports:

export const name = "Achievement rewards";
export const enabled = true;

It is enabled by default unless saved extension state overrides it.

Behavior

On registration, the extension subscribes to EGameEvent.ACTOR_UPDATE.

Each actor update checks these info portions:

  • detective_achievement_gained;
  • mutant_hunter_achievement_gained.

When the configured period has elapsed, it spawns reward items into the configured actor treasure box and emits a tip notification.

Rewards

The reward period is 12 * 60 * 60 game seconds.

Reward targets:

AchievementStory boxItems
Detectivezat_a2_actor_treasuremedkit and antirads
Mutant hunterjup_b202_actor_treasurearmor-piercing ammo and shells

The spawn count is fixed in the update code: detective rewards spawn with count 4, and mutant hunter rewards spawn with count 5.

Saved data

The extension persists two timestamps in dynamic extension data:

  • lastDetectiveAchievementSpawnAt;
  • lastMutantAchievementSpawnAt.

The timestamps are serialized through the time helpers and restored on extension load.

Tuning

  • Keep reward timing in AchievementRewardsConfig.ts.
  • Keep achievement checks in update.ts.
  • Add save/load fields when adding a new persistent reward timer.
  • Keep notification captions aligned with translation ids.

Enhanced Items Drop

Enhanced items drop (with upgrades) can add random upgrades to weapons, outfits, and helmets when they first go online.

The source lives under src/engine/extensions/enhanced_items_drop.

Default state

The extension exports:

export const name = "Enhanced items drop (with upgrades)";
export const enabled = false;

It is disabled by default. Saved extension state can enable it.

Behavior

On registration, the extension subscribes to:

  • ITEM_WEAPON_GO_ONLINE_FIRST_TIME;
  • ITEM_OUTFIT_GO_ONLINE_FIRST_TIME;
  • ITEM_HELMET_GO_ONLINE_FIRST_TIME.

For each item, onItemGoOnlineFirstTime reads the owner id. Actor-owned items are skipped.

The extension then calculates a random chance and applies a different rate depending on whether the item belongs to:

  • a trader;
  • another owner;
  • the world.

If the chance passes one of the configured thresholds, the extension calls addRandomUpgrades.

Upgrade Tiers

The default config lives in EnhancedDropConfig.ts.

TierChanceUpgrade count
Common201
Rare123
Epic67
Legendary130

The final upgrade count includes random dispersion from ADD_RANDOM_DISPERSION.

Saved data

This extension does not export save or load. It changes items when first-online item events are emitted.

Editing Notes

  • Keep chance and count tuning in EnhancedDropConfig.ts.
  • Keep item filtering in enhanced_items_drop_utils.ts.
  • Do not apply this extension to actor-owned starting items unless that behavior is intentional.

Enhanced Location Progression

Enhanced location progression requires a smart terrain to be visited before it appears on the map or can be selected as a same-level travel target.

The source lives under src/engine/extensions/enhanced_location_progression.

Default state

The extension exports:

export const name = "Enhanced location progression";
export const enabled = true;

It is enabled by default unless saved extension state overrides it.

Behavior

On registration, the extension sets:

mapDisplayConfig.REQUIRE_SMART_TERRAIN_VISIT = true;

This flag is checked by map display and travel code.

Map Spots

updateTerrainsMapSpotDisplay shows global terrain spots only when:

  • REQUIRE_SMART_TERRAIN_VISIT is disabled; or
  • the actor has the "<terrain>_visited" info portion.

Restrictor lifecycle code gives visited info portions when the actor reaches the matching restrictor.

Travel

TravelManager.isSmartAvailableToReach rejects smart terrain travel targets on the current level when REQUIRE_SMART_TERRAIN_VISIT is enabled and the terrain has not been visited.

Saved data

This extension does not export save or load. It changes runtime config during extension registration.

Editing Notes

  • Keep the progression toggle in main.ts.
  • Check map and travel behavior together when changing this flag.
  • Preserve visited info portion naming because other code checks "<terrain>_visited".

Enhanced Treasures

Enhanced treasures uses a treasure’s type to choose its map icon.

The source lives under src/engine/extensions/enhanced_treasures.

Default state

The extension exports:

export const name = "Enhanced treasures";
export const enabled = true;

It is enabled by default unless saved extension state overrides it.

Behavior

On registration, the extension sets:

treasureConfig.ENHANCED_MODE_ENABLED = true;

The treasure map helper uses this flag when choosing the map spot icon for a treasure descriptor.

Map Spots

When enhanced mode is disabled, every treasure uses the generic treasure map mark.

When enhanced mode is enabled, getTreasureMapSpot maps treasure type to mark:

Treasure typeMap mark
COMMONtreasure
RAREtreasure_rare
EPICtreasure_epic
UNIQUEtreasure_unique

Treasure state itself is still owned by TreasureManager and treasureConfig.TREASURES.

Saved data

This extension does not export save or load. Treasure manager state is saved by TreasureManager.

Editing Notes

  • Keep the extension toggle in main.ts.
  • Keep treasure type-to-icon behavior in map_spot_treasure.ts.
  • Keep treasure state changes in TreasureManager, not in this extension.

Original Start Position

Original start position changes the actor’s initial vertex and position for a new game.

The source lives under src/engine/extensions/original_start_position.

Default state

The extension exports:

export const name = "Original start position";
export const enabled = false;

It is disabled by default. Saved extension state can enable it.

Behavior

The extension receives isNewGame from the extension registration flow.

When isNewGame is true, it calls:

set_start_game_vertex_id(287);
set_start_position(createVector(268, 20, 560));

When isNewGame is false, it does nothing. This prevents loaded saves from having their actor position changed.

Saved data

This extension does not export save or load.

Editing Notes

  • Keep the isNewGame guard.
  • Use engine start-position APIs only during new-game startup.
  • Update tests if the vertex id or vector changes.
  • Check the extension registration state when the code looks correct but the actor still starts elsewhere.

XRF Tools

xrf-tools is the XRF companion workspace. It contains reusable Rust format crates, a CLI, and a Tauri desktop application.

Use it for tasks that are awkward to do by hand:

  • reading and unpacking X-Ray archives;
  • verifying and formatting LTX configs;
  • converting and checking translations;
  • inspecting script exports;
  • packing and unpacking equipment icons and texture descriptions;
  • inspecting or converting spawn, particles, OGF, and OMF data.

Repository layout

  • crates/: reusable Rust crates for X-Ray formats and project validation.
  • bin/xrf-cli: command-line tool.
  • bin/xrf-app: Tauri backend for the desktop application.
  • bin/xrf-ui: React frontend for the desktop application.

The engine repository uses a bundled tools binary from cli/bin for some build and asset operations.

Choose an interface

Use the CLI when the command must be repeatable, run in CI, or become part of an engine build step. Examples include LTX verification, translation builds, archive unpacking, spawn conversion, and texture packing.

Use the desktop app when you need to inspect structured project data with navigation: archives, configs, dialogs, exports, icons, spawns, and translations. Some app routes are read-only or prototype workflows; the page for each app tool calls out what is currently wired.

Implementation source

Tool behavior comes from the tools workspace, not from the book text:

  • CLI commands: xrf-tools/bin/xrf-cli/src/commands;
  • desktop backend commands: xrf-tools/bin/xrf-app/src;
  • desktop frontend routes: xrf-tools/bin/xrf-ui/src/applications;
  • reusable format logic: xrf-tools/crates.

Tools Application

main window

The XRF tools application is a Tauri desktop app with Rust commands and a React UI. Use it for interactive inspection and one-off data operations; use the CLI for scripted work.

The application source is split across:

  • xrf-tools/bin/xrf-app: Tauri backend plugins and commands;
  • xrf-tools/bin/xrf-ui: React routes, pages, stores, and components;
  • xrf-tools/crates/*: reusable parsers, verifiers, packers, and project readers.

Tools

ToolUse it forWrites files
Archive editorBrowse .db archive projects, read files, and unpack archives.Unpack workflow writes files.
Config editorVerify and format LTX config projects.Formatter can write files.
Dialog editorInspect the current dialog graph prototype.No production data workflow is wired.
Exports viewerBrowse parsed condition, dialog, and effect declarations.No
Icon editorOpen equipment sprites and pack equipment icons.Pack workflow writes DDS output.
Spawn editorInspect, import/export, save, pack, and unpack spawn data.Save/export/pack/unpack workflows write files.
Translation editorOpen and inspect translation JSON projects.No write workflow is exposed in the current UI.

Project paths

The app stores selected XRF project and configs paths in local storage. Several tools use those paths to prefill common locations such as src/engine/configs, target/gamedata/spawns/all.spawn, target/game_link, and src/engine/translations.

Choose the CLI when

Use Tools CLI when the task must be repeatable, scripted, or run in CI.

Archive Editor

The archive editor opens X-Ray .db archive projects, shows their metadata, reads individual files as text, and can unpack them to a folder.

Screens

ScreenRoutePurpose
Navigator/archives_editorChoose between opening an archive project and unpacking archives.
Open/editor/archives_editor/editorOpen an archive project and browse loaded archive data.
Unpacker/archives_editor/unpackerSelect a packed archive folder and an output folder, then unpack.

Backend commands

The Tauri archives-editor plugin exposes:

  • open_archives_project;
  • close_archives_project;
  • has_archives_project;
  • get_archives_project;
  • read_archive_file;
  • unpack_archives_path.

unpack_archives_path opens the selected archive path as an ArchiveProject and unpacks it in parallel with 32 workers.

Workflow

  1. Use Open to inspect an archive project without extracting it.
  2. Use file reading from the editor view when you need to inspect a text file inside the archive.
  3. Use Unpack when you need the archive contents on disk.

Default project-aware paths point the unpacker at target/game_link for source archives and target/unpacked_archives for output when an XRF project path is configured.

CLI equivalent

Use xrf-cli unpack-archive for repeatable unpacking from scripts.

Dialog Editor

The dialog editor is a prototype for dialog graphs. It is not a production workflow for dialog data.

Current routes

RoutePurpose
/dialog_editorNavigator with an Open entry.
Experimental graph routeTest graph page using React Flow dialog and phrase nodes.

The current app source does not register a dialog-editor Tauri plugin and does not expose import, export, save, or validation commands for real dialog XML.

What is implemented

  • A navigator page.
  • A test graph with dialog and phrase node components.
  • Client-side graph interactions through React Flow.

What is not implemented

  • Opening engine dialog XML.
  • Saving dialog XML.
  • Validating phrase links or script predicates/actions.
  • Synchronizing dialog text with translation files.

Production workflow

For real dialog changes, edit the engine sources directly:

  • dialog XML under src/engine/configs/gameplay;
  • dialog extern declarations under src/engine/scripts/declarations/dialogs;
  • dialog text under src/engine/translations.

See Dialog configs for the current production documentation.

Config Editor

The config editor works with LTX projects. Verification and formatting are the currently wired workflows.

Screens

ScreenRoutePurpose
Navigator/configs_editorChoose Explorer, Verifier, or Formatter.
Explorer/configs_editor/explorerRoute and form shell for opening config folders. The current source does not wire the open action.
Verifier/configs_editor/verifierVerify an LTX project folder and display validation errors.
Formatter/configs_editor/formatterCheck formatting or format all LTX files under a folder.

Backend commands

The Tauri configs-editor plugin exposes:

  • verify_configs_path;
  • check_format_configs_path;
  • format_configs_path.

Verification opens the folder with scheme checking enabled and strict checking disabled. Formatting uses the shared xrf-ltx formatter.

Workflow

  1. Select the configs folder. When an XRF project path is configured, the app can prefill src/engine/configs.
  2. Run Verifier after config edits to inspect include, inheritance, section, and scheme errors.
  3. Run Formatter in check mode first when reviewing changes.
  4. Disable check mode only when you intend the formatter to rewrite files.

CLI equivalent

Use xrf-cli verify-ltx and xrf-cli format-ltx for repeatable checks. In the engine repository, use npm run cli -- verify ltx and npm run cli -- format ltx.

Exports Viewer

The exports viewer reads declaration folders and shows exported conditions, dialog functions, effects, and parameters.

Use it when editing config logic and you need to confirm which script externs are available.

Screens

ScreenRoutePurpose
Navigator/exports_editorOpen export sources.
Exports view/exports_editor/exportsBrowse parsed declarations and filter the displayed exports.

Backend commands

The Tauri exports-editor plugin exposes:

  • open_xr_exports: parse conditions, dialogs, and effects folders together;
  • get_xr_exports;
  • close_xr_exports;
  • open_xr_effects;
  • parse_xr_effects;
  • get_xr_effects;
  • close_xr_effects;
  • has_xr_effects.

Source folders

When an XRF project path is configured, the UI can derive default declaration paths from:

  • src/engine/scripts/declarations/conditions;
  • src/engine/scripts/declarations/dialogs;
  • src/engine/scripts/declarations/effects.

Notes

The viewer is read-only. Edit the engine declaration sources, then reload the viewer.

Use the CLI-generated externs reference when you need a static artifact under target/parsed. Use the app viewer when you are browsing declarations interactively while editing configs.

If a condition or effect is missing from the viewer, check that it is declared in the expected conditions, dialogs, or effects folder and that the project path points at the engine repository you are editing.

Icon Editor

The icon editor works with equipment icon sprites and their descriptors. Opening a sprite and packing equipment icons are the fully wired workflows.

Screens

ScreenRouteStatus
Equipment editor/icons_editor/icons_equipmentOpens a DDS equipment sprite with system.ltx descriptors and displays it.
Equipment pack/icons_editor/icons_equipment_packPacks separate icon files into an equipment DDS sprite.
Equipment unpack/icons_editor/icons_equipment_unpackRoute and form shell. No backend unpack command is wired in the current UI.
Description editor/icons_editor/icons_descriptionRoute shell.
Description pack/icons_editor/icons_description_packRoute and form shell.
Description unpack/icons_editor/icons_description_unpackRoute and form shell.

Backend commands

The Tauri icons-editor plugin exposes:

  • open_equipment_sprite;
  • reopen_equipment_sprite;
  • get_equipment_sprite;
  • close_equipment_sprite;
  • pack_equipment.

open_equipment_sprite reads the DDS as a PNG preview and reads equipment descriptors from system.ltx. pack_equipment packs source icons into a DDS using BC3 RGBA compression.

Workflow

  1. Open the equipment sprite with the matching system.ltx.
  2. Inspect section icon rectangles in the equipment editor.
  3. Use Equipment pack when separate icon files should be packed into the final equipment DDS.

When an XRF project path is configured, the app tries to prefill common paths such as src/engine/configs/system.ltx and src/resources/textures/ui/ui_icon_equipment.dds.

CLI equivalent

Use xrf-cli unpack-equipment-icons, xrf-cli pack-equipment-icons, xrf-cli unpack-texture-description, and xrf-cli pack-texture-description when you need workflows that are not fully wired in the app.

Spawn Editor

The spawn editor reads and inspects ALife spawn data. It can open packed files, import and export unpacked folders, and save a packed file.

Screens

ScreenRoutePurpose
Navigator/spawn_editorChoose Open, Unpack, or Pack.
Editor/spawn_editor/editorOpen and inspect the current spawn file.
Unpack/spawn_editor/unpackUnpack a spawn file into a folder.
Pack/spawn_editor/packPack an unpacked spawn folder into a spawn file.

Data views

The backend exposes accessors for:

  • header data;
  • graph data;
  • ALife spawn objects;
  • artefact spawn points;
  • patrols;
  • full loaded spawn file data.

Backend commands

The Tauri spawns-editor plugin exposes:

  • open_spawn_file;
  • import_spawn_file;
  • export_spawn_file;
  • save_spawn_file;
  • close_spawn_file;
  • get_spawn_file;
  • get_spawn_file_header;
  • get_spawn_file_graphs;
  • get_spawn_file_alife_spawns;
  • get_spawn_file_artefact_spawns;
  • get_spawn_file_patrols;
  • has_spawn_file.

Workflow

  1. Use Open for a packed all.spawn file.
  2. Inspect header, graph, ALife, artefact, and patrol chunks in the editor.
  3. Use Unpack to export a packed spawn file into editable structured data.
  4. Use Pack to rebuild a packed spawn file from an unpacked folder.

When an XRF project path is configured, default paths point at target/gamedata/spawns/all.spawn, target/spawns/unpacked, and target/spawns/repacked/repacked.spawn.

Safety

Spawn files are binary game data. Keep a backup before saving or replacing a spawn file.

Translation Editor

The translation editor opens XRF translation projects for read-only inspection.

Screens

ScreenRoutePurpose
Navigator/translations_editorOpen a translation project.
Project view/translations_editor/projectInspect the loaded translation project.

Backend commands

The Tauri translations-editor plugin exposes:

  • open_translations_project;
  • read_translations_project;
  • get_translations_project;
  • close_translations_project.

Both open/read paths use TranslationProject::read_project.

Workflow

  1. Open a translation project folder or file.
  2. Inspect loaded translation ids and language values in the project view.
  3. Close and reopen after editing translation sources outside the app.

When an XRF project path is configured, the app can prefill src/engine/translations.

Limitations

The app keeps the parsed translation project in the Tauri plugin state and displays it in the project view. It does not currently expose a save action, build XML output, or run translation verification from the UI.

Use the app for inspection while comparing ids across languages. Use the CLI when you need to initialize files, build game XML, or validate translation structure.

CLI equivalent

Use xrf-cli initialize-translation, xrf-cli build-translation, and xrf-cli verify-translation for write, build, and verification workflows. In the engine repository, use npm run cli -- translations ... for XML-to-JSON conversion and project translation checks.

Tools CLI

The Rust xrf-cli binary provides repeatable asset inspection, conversion, packing, unpacking, formatting, and verification outside the engine repository wrapper.

xrf-cli <command> --help

The engine CLI wraps selected operations through npm run cli -- .... Use xrf-cli directly for scripts and format-specific workflows.

Command groups

GroupCommands
Archiveunpack-archive
Externsexport-externs
Gamedataverify-gamedata
LTXformat-ltx, verify-ltx
Modelsinfo-ogf, patch-ogf-motion-refs, patch-ogf-texture-refs, info-omf, repack-omf, filter-omf-motions, rename-omf-motions, duplicate-omf-motion
Particleinfo-particles, unpack-particles, pack-particles, repack-particles, re-unpack-particles, verify-particles
Spawninfo-spawn, unpack-spawn, pack-spawn, repack-spawn, verify-spawn
Textureinfo-dds, crop-dds, unpack-equipment-icons, pack-equipment-icons, verify-equipment-icons, unpack-texture-description, pack-texture-description, patch-thm-bump
Translationinitialize-translation, build-translation, verify-translation, parse-translation

Logging

Some commands offer --silent or --verbose. Rust logging honors RUST_LOG when it is set.

Read and write commands

Inspection commands such as info-ogf, info-omf, info-dds, info-spawn, and info-particles read input files and print parsed metadata. Conversion commands such as archive unpacking, spawn packing, particle packing, texture packing, and translation building write output paths.

Use explicit input and output paths when documenting or scripting a command. Some commands use named options such as --path and --dest; verify-gamedata uses its assembled gamedata root as a positional operand. Explicit paths make generated assets easier to reproduce.

Usage pattern

Use the command-specific page when working with a file format. Each page lists the required input, output behavior, and the commands that are safe to run as read-only inspection versus commands that write files.

When running from the engine repository, prefer the engine CLI wrapper if it already exposes the workflow. Use xrf-cli directly when you need a lower-level command that the engine wrapper does not register. On Windows, use xrf-cli.exe or the bundled path under cli/bin/tools.

Archive CLI

Archive commands work with X-Ray .db database archives.

pack-archive

pack-archive builds database archives from a folder, replacing the xrCompress tool of the original SDK.

xrf-cli pack-archive --path target\gamedata --dest target\db --name gamedata

A set that fits in one volume is written as <name>.db; a larger one splits into <name>.db0, <name>.db1 and so on. The engine mounts any file whose extension starts with db or xdb, so the index is a convenience rather than a requirement. Files the engine expects compressed are compressed and the rest are stored, matching what the engine loads. Identical files are stored once and referenced twice.

Give the archive a [header]. Without one the engine assumes a .db is an encrypted Shadow of Chernobyl archive and decrypts it into nonsense; --xdb is the other way to say an archive is not that.

Options

  • -p, --path <path>: folder to pack, normally a gamedata root. Required.
  • -d, --dest <dest>: destination folder for the volumes. Defaults to packed.
  • -n, --name <name>: base name of the volumes. Defaults to gamedata.
  • --ltx <path>: configuration file selecting what to include. Options given on the command line win over it.
  • --store: store every file instead of compressing.
  • --max-size <megabytes>: maximum volume size. Defaults to and is capped at 1900.
  • --xdb: write volumes with the xdb extension.
  • --no-skip-list: keep editor and source leftovers the game build normally drops.
  • -s, --silent: disable command logging.

Configuration file

Without --ltx the whole folder is packed. The file uses the same dialect xrCompress accepted:

[options]
exclude_exts = *.txt,*.json

[include_folders]
configs = true
scripts = true

[include_files]
gamemtl.xr

[header]
auto_load = true
entry_point = $fs_root$\gamedata\

[include_folders] and [exclude_folders] map a path to whether it applies recursively; .\ names the packed root. [include_files] lists names one per line. [header] is written into the archive verbatim, and its entry_point is where the engine mounts the contents, so an archive of a gamedata tree needs it.

unpack-archive

unpack-archive opens an archive project and exports the contained files to a folder.

xrf-cli unpack-archive --path gamedata.db0 --dest unpacked

Options

  • -p, --path <path>: path to a .db archive file. Required.
  • -d, --dest <dest>: destination folder. Defaults to unpacked.
  • --parallel <count>: number of parallel unpack workers. Defaults to 32.
  • --dry: read and summarize the archive without writing files.
  • -s, --silent: disable command logging.

Relative destination paths are resolved from the current working directory.

Output

Without --silent, the command prints:

  • archive count;
  • file count;
  • compressed size;
  • real unpacked size;
  • unpack duration when files are written.

With --dry, the command still reads the archive metadata but does not write the extracted files. Use it to confirm that a database can be opened before spending time on a full unpack.

Examples

xrf-cli unpack-archive --path .\db\configs.db0 --dest .\unpacked\configs
xrf-cli unpack-archive --path .\db\textures.db0 --dest .\unpacked\textures --parallel 8
xrf-cli unpack-archive --path .\db\sounds.db0 --dry

Failure notes

The source path must point to a readable X-Ray database archive. If the destination already contains files, choose a new folder or clean it before running the command.

Gamedata CLI

Gamedata commands validate one assembled gamedata directory. Run them after building gamedata and before launching or packaging it.

verify-gamedata

xrf-cli verify-gamedata ./target/gamedata

ROOT is the required positional path to the assembled gamedata directory. The command reads configs from ROOT/configs and requires ROOT/configs/system.ltx.

Options

  • -i, --ignore <names...>: ignored files or folders. Multiple names are comma-separated.
  • --checks <checks...>: selected verification checks. If omitted, all checks run.
  • --report <path>: write a JSON verification report.
  • --silent: disable logging.
  • -v, --verbose: enable verbose logging.
  • -s, --strict: fully validate expensive asset payloads.

Accepted check names are animations, levels, ltx, meshes, particles, particles-usage, scripts, shaders, sounds, spawns, textures, weapons, and weathers. The script check parses emitted .script files with the LuaJIT syntax dialect.

If --ignore is omitted, the command ignores common repository and unpacked-source entries: .git, .idea, particles_unpacked, textures_unpacked, .gitignore, .gitattributes, README.md, and LICENSE.

Checks and rules

A check is a group of related verification, selected with --checks. Inside a check, each individual violation is attributed to a rule, and the rule identifier is what appears in findings and in the JSON report. Rule identifiers are <check>.<rule>:

CheckRules
animationsanimations.player-hud, animations.hud-item, animations.motion-collision
levelslevels.ai-guid, levels.ai-node-count, levels.ai-version, levels.cform-version, levels.details-pair, levels.file-empty, levels.file-truncated, levels.graph-duplicate, levels.graph-guid, levels.header-version, levels.level-guid, levels.ltx-read, levels.map-texture, levels.missing-bundle, levels.missing-file, levels.orphan-bundle, levels.roster-conflict, levels.shader-reference, levels.shaders-chunk, levels.texture-reference, levels.undeclared-map
ltxltx.formatting, ltx.schema, ltx.verification
meshesmeshes.path, meshes.read, meshes.validation, meshes.motion-read, meshes.motion-validation, meshes.shader-library
particlesparticles.library, particles.texture
particles-usageparticles-usage.reference, particles-usage.spawn, particles-usage.spawn-custom-data
scriptsscripts.path, scripts.read, scripts.syntax
shadersshaders.renderer-root, shaders.lua-syntax, shaders.source-read, shaders.source-invalid, shaders.include-missing, shaders.include-cycle, shaders.include-syntax
soundssounds.files, sounds.references
spawnsspawns.path, spawns.read
texturestextures.path, textures.read, textures.dds, textures.bump
weaponsweapons.validation
weathersweathers.definitions, weathers.files, weathers.validation

checks.execution is reported when a check itself fails to run, rather than when content is invalid.

The animation rules validate player HUD and item motions. Missing item motions are allowed where the engine falls back to idle; duplicate motion names across banks in one HUD namespace are reported because their resolution is ambiguous.

textures.bump resolves the bump each .thm declares the way CTextureDescrMngr::LoadTHM does, by the name in the descriptor rather than by a _bump suffix convention. A name that resolves to nothing still takes the _bump shader path, because bump_exist() only checks the name is non-empty: the loader substitutes ed\ed_dummy_bump and logs ! Fallback to default bump map on every load, so the surface is flat and the log is noisy. Importing a texture under a new path is the usual way to produce one, since the copied descriptor keeps pointing into the source layout. Repoint it with patch-thm-bump --to, or patch-thm-bump --off when the bump does not exist and is not going to.

JSON report

Pass --report to write the result for CI or other tooling:

xrf-cli verify-gamedata ./target/gamedata --checks sounds,weathers --report ./verification-report.json

The report is a single object with camelCase keys:

{
  "checks": [
    {
      "durationMs": 114,
      "findings": [],
      "status": "passed",
      "summary": "122/122 weather files valid",
      "verificationType": "weathers"
    }
  ],
  "durationMs": 311,
  "status": "passed"
}

status is one of passed, failed, error, incomplete, or skipped. The top-level status is the most severe individual status. incomplete means a check could cover only part of its expected input; durationMs is null when a check did not run.

Each entry of findings describes one violation:

FieldMeaning
ruleIdRule identifier from the table above.
assetPathRoot-relative asset path. May be absent.
messageHuman-readable description.

Findings are ordered by asset path, rule, then message, so two reports over the same gamedata can be compared directly.

Examples

xrf-cli verify-gamedata ./target/gamedata
xrf-cli verify-gamedata ./target/gamedata --checks scripts,ltx
xrf-cli verify-gamedata ./target/gamedata --checks weathers
xrf-cli verify-gamedata ./target/gamedata --checks sounds --strict
xrf-cli verify-gamedata ./target/gamedata --report ./verification-report.json
xrf-cli verify-gamedata ./target/gamedata --ignore .git,textures_unpacked --strict

Result

The command exits with a non-zero status unless the overall result is passed, including when verification is skipped or incomplete. In normal logging mode it prints each failure message before exiting.

The command validates the files present in the assembled tree, including generated scripts and configs. It does not validate source repositories or files that were not included in the build.

LTX CLI

LTX commands format and verify .ltx and .ini config files. Use them for standalone config projects or when you need the lower-level tool behind the engine repository’s format ltx and verify ltx commands.

format-ltx

Formats one file or every LTX file under a folder.

xrf-cli format-ltx --path ./gamedata/configs
xrf-cli format-ltx --path ./gamedata/configs --check

Options:

  • -p, --path <path>: file or folder to format. Required.
  • -c, --check: check formatting without rewriting project files.
  • -s, --silent: disable logging.
  • -v, --verbose: enable verbose logging.

--check is implemented for folders. Single-file mode formats the file.

verify-ltx

Verifies an LTX project folder, including its schemes and case-sensitive include paths.

xrf-cli verify-ltx --path ./gamedata/configs

Options:

  • -p, --path <path>: configs root folder. Required.
  • --silent: disable logging.
  • -v, --verbose: enable verbose logging.

Failure notes

verify-ltx expects a directory, not a single file. It fails when includes, inheritance, section fields, or scheme validation produce errors.

Only sections that declare $schema are checked against a scheme. Other sections, including array-style sections, are valid without one. A scheme can use $strict = true when its own section shape is fully known.

Scheme definitions are documented in Script config schemes.

OGF CLI

OGF commands inspect X-Ray model files and safely rewrite motion or texture references.

Commands

CommandPurposeWrites files
info-ogfPrint header, textures, bones, lods, and motion refs.No
patch-ogf-motion-refsReplace the motion references stored inside a model.Yes
patch-ogf-texture-refsRename a texture reference stored inside a model.Yes

info-ogf

xrf-cli info-ogf --path ./meshes/example.ogf

Options:

  • -p, --path <path>: path to an .ogf file. Required.
  • -s, --silent: disable logging.
  • -v, --verbose: turn on verbose logging, which lists each level of detail individually.

Output

The command reads the model and prints available metadata:

  • header version, model type, shader id, bounding box, and bounding sphere;
  • texture and shader names;
  • description chunk data when present;
  • bones and parent names when present;
  • motion references when present;
  • progressive mesh level of detail counts when present;
  • unparsed chunk ids, so an incomplete read is visible rather than silent;
  • child model texture, shader names, and level of detail counts for nested OGF data.

Progressive level of detail data usually belongs to the nested child visuals rather than the root, so expect the [n] progressive lods lines rather than a single figure for the model. Pass -v to list each level with its index buffer offset, triangle count, and vertex count.

When to use it

Use info-ogf to confirm that a mesh file can be parsed, to inspect texture references, or to compare model metadata without opening a graphical tool.

The command is read-only: it does not rewrite chunks, normalize paths, or repair model data. If a model fails to parse, first confirm the file is an OGF from the expected game version, then compare the reported failure with neighboring meshes from the same source archive.

patch-ogf-motion-refs

An animated model stores the paths of the OMF files it loads animations from. This command rewrites those paths, which is what lets a model be moved to a different directory layout without re-exporting it from the SDK.

xrf-cli patch-ogf-motion-refs --path ./meshes/wpn_ak74_hud.ogf --refs "dynamics\weapons\wpn_ak74\wpn_ak74_hud_animation"
xrf-cli patch-ogf-motion-refs --path ./hands.ogf --dest ./hands.patched.ogf --refs "dynamics\weapons\wpn_hand\hud_animation\*.omf"
xrf-cli patch-ogf-motion-refs --path ./hands.ogf --refs "first\animation" "second\animation"

Options:

  • -p, --path <path>: path to an .ogf file. Required.
  • -d, --dest <dest>: path to the resulting file. Defaults to rewriting the source file in place.
  • -r, --refs <refs>...: one or more motion references to store. Required.
  • --dry-run: report what the rewrite would produce without writing anything.

Paths use backslashes and omit the .omf extension, matching how the engine resolves them. A reference ending in \*.omf is a wildcard: the engine loads every OMF in that directory.

What is preserved

Only the motion references chunk is rebuilt. Every other chunk, including geometry, bones and IK data, is copied byte for byte, so patching cannot alter the model itself.

The source file’s chunk form is also preserved. Older models store references as one comma-separated string, newer ones store a counted list; a file keeps whichever form it already used rather than being silently upgraded.

Safety checks

Model geometry cannot currently be re-serialized from parsed data, so the command verifies rather than assumes:

  • Before writing, it rewrites the file’s existing references and requires the result to reproduce the source byte for byte. If it does not, the chunk copy would be lossy for that file and the command refuses to touch it.
  • After writing, it reads the result back and requires the references to match what was requested.

If the second check fails, the write is undone: an in-place edit is restored from the original bytes, and a separate destination file is removed. A model without a motion references chunk is refused before anything is written.

When to use it

Use it when relocating animation banks, for example when consolidating per-weapon hand animations into one shared directory and pointing every hands model at it with a wildcard. Confirm the result with info-ogf.

patch-ogf-texture-refs

A model stores the path of every texture it uses. This command renames one of those paths, which is what lets an imported texture follow your project’s naming without re-exporting the model.

xrf-cli patch-ogf-texture-refs --path ./meshes/wpn_ak74u.ogf --from "wpn\wpn_aksu\wpn_aksu" --to "wpn\wpn_ak74u\wpn_ak74u"
xrf-cli patch-ogf-texture-refs --path ./model.ogf --dest ./model.patched.ogf --from "old\name" --to "new\name" --dry-run

Options:

  • -p, --path <path>: path to an .ogf file. Required.
  • -d, --dest <dest>: path to the resulting file. Defaults to rewriting the source file in place.
  • --from <name>: the texture reference to rename, matched exactly. Required.
  • --to <name>: the reference to write in its place. Required.
  • --dry-run: report what the rewrite would produce without writing anything.

It also accepts -s, --silent and -v, --verbose. Paths use backslashes and omit the extension. Matching is exact; rename one source name per run.

Texture names live inside nested child visuals. The command walks those children and rebuilds every texture chunk whose name matches, including when the replacement has a different length.

What is preserved

Only matching texture chunks are rebuilt. Every other chunk, including geometry, is copied byte for byte; each paired shader name is preserved.

Safety checks

The command applies three guards:

  • Before writing, it renames the reference to itself and requires a byte-for-byte copy of the source.
  • A --from that matches nothing is refused, and the error lists the references the model actually has, so a typo cannot pass as a silent no-op.
  • After writing, it reads the result back and requires the old name to be gone and the new name to be present.

If the final check fails the write is undone: an in-place edit is restored from the original bytes, and a separate destination file is removed.

When to use it

Rename the texture files first, patch every model that referenced them, then confirm with info-ogf and verify-gamedata. A missed model leaves a dangling reference.

OMF CLI

OMF commands inspect, re-serialize, and edit the motion set of X-Ray motion files.

Commands

CommandPurposeWrites files
info-omfPrint version, motions, bones, and animation parts.No
repack-omfRead a motion file and write it back, or verify it round-trips.Only with --dest
filter-omf-motionsKeep only selected motions, dropping the rest.Yes
rename-omf-motionsRename motions using a name map.Yes
duplicate-omf-motionCopy a motion under a new name, optionally clearing its loop.Yes

How motions are stored

Both editing commands rely on the same structure, which is worth knowing before using them.

A motion file holds two parallel lists: motion definitions, which carry the name and playback parameters, and motion payloads, which carry the keyframes. They are paired by position. The engine looks a motion up by its definition name and then reads the payload stored at the same position, so the two lists must always be filtered and renamed together. Both commands maintain that pairing; the names an LTX anm_* key refers to are the definition names.

info-omf

xrf-cli info-omf --path ./meshes/example.omf

Options:

  • -p, --path <path>: path to an .omf file. Required.

Output

The command reads the motion file and prints:

  • OMF version;
  • motion count and motion names;
  • total bone count;
  • animation part names;
  • bones assigned to each animation part.

With -v, each motion is listed individually with its keyframe count, flags, speed, power, accrue, and falloff. Keyframes and speed together give the effective duration. The flags matter more than they look: bit 0b10 means the motion plays once and stops, and a motion without it loops forever. Vanilla idles read 0b00 while draw, shoot and bore motions read 0b10.

When to use it

Use info-omf when checking whether a motion file is readable, whether expected motions are present, or how motion parts map to skeleton bones.

The command is read-only and prints the parsed structure; it does not merge, split, or repair motion files. If an expected animation is absent, check the source OMF first and then inspect the model or config that references the motion name.

repack-omf

xrf-cli repack-omf --path ./meshes/example.omf --dest ./meshes/example.repacked.omf
xrf-cli repack-omf --path ./meshes/example.omf --verify
xrf-cli repack-omf --path ./meshes

Options:

  • -p, --path <path>: path to an .omf file, or to a directory containing .omf files. Required.
  • -d, --dest <dest>: path to the resulting .omf file. Required when writing a single file. Rejected when --path is a directory.
  • --verify: compare the re-serialized bytes against the source instead of writing output.

The command has three modes:

  • Write. A file path plus --dest reads the source and writes a re-serialized copy.
  • Verify one file. A file path plus --verify re-serializes in memory and compares against the source bytes. Nothing is written.
  • Verify a directory. A directory path walks it recursively, verifies every .omf beneath it, and prints a summary. Nothing is written, and --dest is rejected.

Unmodified files round-trip byte for byte. The writer preserves the original chunk order and reproduces the nested motion chunk ids, so a repacked file that differs from its source indicates a real change, not serialization noise.

When to use it

Use directory verification as a regression check after changing OMF parsing or serialization, and before relying on generated motion banks. Because it fails on any mismatch, it works as a build or pre-commit gate.

Use single-file write mode when you need a normalized copy of a motion file.

filter-omf-motions

Shared animation banks often carry motions for many weapons at once. This command extracts the subset you need.

xrf-cli filter-omf-motions --path ./shared_bank.omf --dest ./wpn_ak74_hud_animation.omf --keep-prefix ak_74_
xrf-cli filter-omf-motions --path ./bank.omf --dest ./trimmed.omf --keep idle --keep-prefix ak_74_ pist_

Options:

  • -p, --path <path>: source .omf file. Required.
  • -d, --dest <dest>: resulting .omf file. Required.
  • -k, --keep <name>...: exact motion names to keep.
  • --keep-prefix <prefix>...: keep motions whose name starts with the prefix.
  • --dry-run: report the outcome without writing anything.

At least one of --keep or --keep-prefix must be given, and a motion is kept if it matches any of them. Prefixes are matched literally, so ak_74_ and ak74_ select different motions.

Surviving definitions are renumbered so their internal motion index stays consistent with their new position.

--dest is required rather than defaulting to an in-place rewrite. Trimming a shared bank in place would destroy it for every other weapon that sources from it, and the input is often a read-only reference dump.

rename-omf-motions

xrf-cli rename-omf-motions --path ./trimmed.omf --dest ./renamed.omf --map ./ak74.json
xrf-cli rename-omf-motions --path ./trimmed.omf --dest ./renamed.omf --map ./ak74.json --strict

Options:

  • -p, --path <path>: source .omf file. Required.
  • -d, --dest <dest>: resulting .omf file. Required.
  • -m, --map <map>: JSON object mapping existing motion names to new ones. Required.
  • --strict: require every motion in the file to appear in the map.
  • --dry-run: report the outcome without writing anything.

The map is a flat JSON object:

{
  "ak_74_draw": "ak74_draw",
  "ak_74_idle_move": "ak74_idle_moving",
  "ak_74_grenade_off": "ak74_switch_off"
}

Motions absent from the map keep their current name. Pass --strict when a file is meant to be fully normalized; it fails and lists the motions that have no entry, which is the difference between a deliberate partial rename and an incomplete map.

Renaming updates the definition name and the payload name together, so the new name is what the engine resolves.

duplicate-omf-motion

Copies one motion under a second name, so a bank can provide an animation it does not ship.

xrf-cli duplicate-omf-motion --path ./wpn_hand_pm_hud_animation.omf --from pm_idle --to pm_idle_bore --play-once

Options:

  • -p, --path <path>: path to an .omf file. Required.
  • -d, --dest <dest>: path to the resulting file. Defaults to rewriting the source file in place.
  • --from <name>: motion to copy, matched exactly. Required.
  • --to <name>: name to give the copy. Required.
  • --play-once: clear looping on the copy so it plays once and ends.

Both the definition and the keyframe payload are duplicated rather than aliased, and the copy is pointed at its own new payload slot. That is deliberate: motion definitions and payloads are ordinal pairs, and a definition without a payload would break every later filter or rename. The file grows by one animation’s worth of keyframes.

An unknown --from, or a --to that already exists, is refused rather than producing an unreachable motion.

Why --play-once exists

Some engine states are left only from the animation end callback. Bore is the one that bites: OnAnimationEnd switches it back to idle, and nothing else does. Point anm_bore at a looping motion and the weapon sits in the bore state indefinitely, appearing frozen until some other action forces a state change.

That is the situation when an imported animation pack ships no bore at all. Copying the weapon’s idle and clearing its loop yields a motion that holds the idle pose and then ends, which is enough for the engine to leave the state. Confirm the result with info-omf -v: the copy should read 0b10.

Shared options

All commands accept -s, --silent to disable logging and -v, --verbose to enable verbose logging. Under filter-omf-motions and rename-omf-motions, --verbose prints the resulting motion list.

Under repack-omf, --verbose additionally reports every file that verified as byte identical. Without it, verifying a directory prints only mismatches, read failures, and a closing summary, and verifying a single file that passes prints nothing at all. Scripts can therefore treat single-file verification as silent on success and rely on the exit status.

Failure notes

repack-omf exits with a non-zero status when any file mismatches or cannot be processed. In directory mode it also reports the count of mismatched and errored files. A successful run exits zero.

A file that cannot be read fails before anything is written. The most common cause is a truncated source: a chunk header declares more bytes than the file actually contains. Such files are damaged at the source and need to be re-extracted, usually from the original packed archive with unpack-archive rather than from an unpacked dump.

Writing rejects data it cannot represent faithfully rather than emitting a corrupt file. Motion marks exist only in version 4, so writing a version 3 file that carries marks fails, as does writing a file whose motion count and motion definition count disagree.

The editing commands refuse work that would produce a file the engine cannot use, before writing anything:

  • filter-omf-motions fails when no motion matches, since an empty motion file is not useful and the usual cause is a mistyped prefix.
  • rename-omf-motions fails when the map matches nothing, and when a rename would give two motions the same name, because lookups are by name and a duplicate makes one of them unreachable.

Particle CLI

Particle commands inspect, verify, pack, unpack, and round-trip particles.xr data.

Commands

CommandPurposeWrites files
info-particlesPrint version, effect count, and group count.No
unpack-particlesExport a packed particles.xr into a folder.Yes
pack-particlesBuild a packed particles.xr from an unpacked folder.Yes
repack-particlesRead a packed file and write it to another packed file.Yes
re-unpack-particlesRead an unpacked folder and export it to another unpacked folder.Yes
verify-particlesCheck whether packed or unpacked particle data can be parsed.No

Examples

xrf-cli info-particles --path ./particles.xr
xrf-cli unpack-particles --path ./particles.xr --dest ./particles_unpacked --force
xrf-cli pack-particles --path ./particles_unpacked --dest ./particles.xr --force
xrf-cli repack-particles --path ./particles.xr --dest ./particles.repacked.xr
xrf-cli re-unpack-particles --path ./particles_unpacked --dest ./particles_unpacked_roundtrip
xrf-cli verify-particles --path ./particles.xr
xrf-cli verify-particles --path ./particles_unpacked --unpacked

Shared options

Read commands require -p, --path <path>. Write commands that create output also use -d, --dest <dest>. unpack-particles and pack-particles support -f, --force to remove an existing destination before writing.

Failure notes

Packing fails if the output file already exists and --force is not supplied. Unpacking fails if the destination folder already exists and --force is not supplied.

Spawn CLI

Spawn commands inspect, verify, pack, unpack, and round-trip ALife .spawn files.

Commands

CommandPurposeWrites files
info-spawnPrint header, object, artefact spawn, patrol, and graph counts.No
unpack-spawnExport a packed spawn file into a folder.Yes
pack-spawnBuild a packed spawn file from an unpacked folder.Yes
repack-spawnRead a packed spawn file and write it to another packed file.Yes
verify-spawnCheck whether a packed spawn file can be parsed.No

Examples

xrf-cli info-spawn --path ./all.spawn
xrf-cli unpack-spawn --path ./all.spawn --dest ./all_spawn --force
xrf-cli pack-spawn --path ./all_spawn --dest ./all.spawn --force
xrf-cli repack-spawn --path ./all.spawn --dest ./all.repacked.spawn
xrf-cli verify-spawn --path ./all.spawn

Options

pack-spawn:

  • -p, --path <path>: unpacked spawn folder. Required.
  • -d, --dest <dest>: output .spawn file. Defaults to unpacked.
  • -f, --force: remove an existing output file first.

unpack-spawn:

  • -p, --path <path>: source .spawn file. Required.
  • -d, --dest <dest>: output folder. Defaults to unpacked.
  • -f, --force: remove an existing output folder first.
  • -s, --silent: disable logging.

info-spawn, verify-spawn, and repack-spawn require -p, --path <path>. repack-spawn also requires -d, --dest <dest>.

Failure notes

Packing and unpacking reject existing destinations unless --force is supplied.

Texture CLI

Texture commands inspect DDS files and pack or unpack icon-related assets.

Commands

CommandPurpose
info-ddsPrint DDS size, metadata, mipmap, format, and compression details.
crop-ddsCrop a pixel region out of a DDS file into a new DDS or PNG file.
unpack-equipment-iconsSlice an equipment icon sprite into section icon files using system.ltx.
pack-equipment-iconsPack section icon files into an equipment DDS sprite using system.ltx.
verify-equipment-iconsReport inventory icon grid rects that overlap each other.
unpack-texture-descriptionSlice textures based on XML texture descriptions.
pack-texture-descriptionPack textures based on XML texture descriptions.
patch-thm-bumpRepoint the bump texture a .thm descriptor declares.

DDS inspection

xrf-cli info-dds --path ./textures/ui/ui_icon_equipment.dds

The command prints file size, metadata size, pixel data size, dimensions, mipmap information, pitch or linear size when present, block size, bits per pixel, FourCC, and D3D/DXGI format when known.

Region cropping

xrf-cli crop-dds --source ./textures/ui/ui_icon_equipment.dds --output ./wpn_ak74.dds --x 1000 --y 0 --width 250 --height 100

--source, --output, --x, --y, --width, and --height are required. Coordinates start at the top left. An out-of-bounds region is rejected rather than truncated. The command also accepts -s, --silent and -v, --verbose.

The .png extension selects lossless PNG; any other name produces a BC3 DDS. Prefer PNG as a later packing source to avoid another lossy decode and encode. pack-equipment-icons prefers <section>.png over <section>.dds.

Use this to lift a single icon out of a sprite sheet when the sheet belongs to another project, where unpack-equipment-icons cannot help because that project’s configs do not declare $inventory_icon.

Fitting into different bounds

--fit-width and --fit-height scale the cropped region after cropping. They must be given together.

xrf-cli crop-dds --source ./ui_actor_weapons.dds --output ./upgrade_ak74.dds --x 0 --y 400 --width 300 --height 100 --fit-width 295 --fit-height 110

Scaling preserves aspect ratio and centers the result on a transparent canvas instead of stretching it. A matching region is unchanged. pack-equipment-icons uses the same fitting for mismatched icons.

Fitting matters where a target rectangle is fixed by an XML texture description, since pack-texture-description requires the file to match the declared size exactly and does not rescale.

Equipment icons

xrf-cli unpack-equipment-icons --system-ltx ./configs/system.ltx --source ./textures/ui/ui_icon_equipment.dds --output ./textures_unpacked/ui/ui_icon_equipment
xrf-cli pack-equipment-icons --system-ltx ./configs/system.ltx --source ./textures_unpacked/ui/ui_icon_equipment --output ./textures/ui/ui_icon_equipment.dds --strict

pack-equipment-icons also accepts --gamedata <path> for resource lookup, plus -v, --verbose and -s, --strict. unpack-equipment-icons supports -v, --verbose.

Both commands act only on sections with $inventory_icon = true and all four grid fields: inv_grid_x, inv_grid_y, inv_grid_width, and inv_grid_height. Grid fields alone do not opt a section in; $inventory_icon = false excludes it.

The explicit flag matters because inherited grid fields are common on abstract base sections that have no icon of their own.

Without --strict, an opted-in section whose icon file is missing is skipped with a warning. With --strict the command packs nothing and reports every such section in a single error, so the whole list can be fixed in one pass.

Checking the grid before moving an icon

xrf-cli verify-equipment-icons --system-ltx ./configs/system.ltx

Reports every pair of sections whose grid rects cover a shared cell, with the cell and how many cells overlap, and exits non-zero when any are found.

This covers a case packing cannot. pack-equipment-icons warns when two sections write different art to the same slot, which is the common and usually harmless case, because variants such as _nimble, _snag and the pri_a15_ quest copies inherit their base weapon’s position by design. Identical rects are therefore not reported here either.

What it catches instead is a rect reaching into a neighbour: widen a 1x1 icon to 2x1 and it may quietly take a cell another weapon already occupies. Both pack without complaint, and whichever writes last wins those pixels, so the loser silently shows the wrong art. Run this before widening or relocating any icon, and again afterwards.

Grid coordinates are cells, not pixels; a cell is 50x50, hardcoded in the engine.

Bump declarations

A texture’s bump map is not found by naming convention. CTextureDescrMngr::LoadTHM reads the .thm beside the texture and takes its bump name verbatim.

A name that resolves to nothing does not turn bump mapping off. bump_exist() only tests that the name is non-empty, so the renderer still selects the _bump shader variant, and the loader substitutes ed\ed_dummy_bump while logging ! Fallback to default bump map: <name> on every load. The surface renders flat while still paying for the bump path, and the log fills with fallbacks.

This bites whenever a texture is imported under a different path than it had at its source, because the copied descriptor keeps pointing into the source layout. On renderer_r4 the bump is consumed through CTexture::Preload, so it is not a setting the player can opt out of.

xrf-cli patch-thm-bump --path ./textures/wpn/wpn_pm/wpn_pm.thm --to "wpn\wpn_pm\wpn_pm_bump"

--path is required, plus either --to or --off. --dest writes elsewhere instead of rewriting in place, --dry-run reports what would change without writing, and -s, --silent and -v, --verbose control logging. The bump name is engine style, backslash separated and without an extension.

For a bump that does not exist and is not going to, declare none rather than leaving a dangling name:

xrf-cli patch-thm-bump --path ./textures/tile/tile_walls_red_01.thm --off

--off sets mode to none and clears the name, which is the form STextureParams itself writes, so the result stays diffable against vanilla. Prefer it over leaving the name in place: a dangling name still selects the bump shader path and still logs a fallback on every load.

Omitted fields keep their existing value, so --to alone preserves a use_parallax mode across a rename.

The patch rewrites only the bump chunk and copies every other chunk byte for byte. It first proves that rewriting the declaration the file already has reproduces the source exactly, then confirms the written file reads the requested name back, and reverts rather than leaving a half-written descriptor behind.

Run verify-gamedata -c textures afterwards: it resolves every declared bump the same way the engine does and reports the ones that point at nothing.

Texture descriptions

xrf-cli unpack-texture-description --description ./configs/ui/textures_descr/ui_actor.xml --base ./textures --output ./textures_unpacked --parallel
xrf-cli pack-texture-description --description ./configs/ui/textures_descr/ui_actor.xml --base ./textures_unpacked --output ./textures --strict

Description commands require --description and --base. If --output is omitted, output defaults to the base path. Both support -v, --verbose, -s, --strict, and --parallel.

A description can name several sheets; both commands rewrite all of them by default. Repeat --file <name> to select one or more. Use the declared path (ui\ui_actor_weapons) with either separator, or an unambiguous bare name (ui_actor_weapons). Missing and ambiguous names are errors.

xrf-cli pack-texture-description --description ./configs/ui/textures_descr/ui_actor_upgrades.xml --base ./textures_unpacked --output ./textures --file ui_actor_weapons --strict

The engine repository wraps the common equipment and description workflows through npm run cli -- icons ....

Translation CLI

Translation commands work with XRF JSON translation projects and generated gamedata string tables.

Commands

CommandPurpose
initialize-translationEnsure translation files have the expected language keys.
build-translationBuild translation JSON into gamedata output files.
verify-translationCheck translation completeness.
parse-translationPlaceholder command. It accepts --path but does not currently parse or write translations.

Initialize

xrf-cli initialize-translation --path ./translations

Options:

  • -p, --path <path>: translation file or folder. Required.
  • -s, --silent: disable logging.
  • -v, --verbose: enable verbose logging.

Build

xrf-cli build-translation --path ./translations --output ./gamedata/configs/text --language ukr

Options:

  • -p, --path <path>: translation file or folder. Required.
  • -o, --output <path>: output folder. Required.
  • -l, --language <language>: target language. Defaults to all.
  • -s, --silent: disable logging.
  • -v, --verbose: enable verbose logging.
  • --sort: toggles sorting for dynamic translation files.

Verify

xrf-cli verify-translation --path ./translations --language ukr --strict

Options:

  • -p, --path <path>: translation file or folder. Required.
  • -l, --language <language>: target language. Defaults to all.
  • --strict: exit with a non-zero status when translations are missing.
  • -s, --silent: disable logging.
  • -v, --verbose: enable verbose logging.

Notes

Use the engine repository’s npm run cli -- translations ... workflow to convert original X-Ray XML string tables to XRF JSON sources.

Extern exports

export-externs reads TypeScript extern(...) declarations.

xrf-cli export-externs <declarations-root> --format json --output <path>

Manifest source paths are relative to the declarations root.

Formats

  • json: tracked { "exports": ... } contract; CRLF by default.
  • xml: <externs><exports> document; LF by default.
  • html: collapsed namespace reference; LF by default.

Use --line-endings lf|crlf to override the default.

xrf-cli export-externs src/engine/declarations --format html --output target/parsed/externs.html

Check

--check <artifact> validates without writing and cannot be combined with --output.

xrf-cli export-externs src/engine/declarations --check src/engine/declarations/extern.json

The format is inferred from the extension unless --format is provided. JSON is compared semantically; XML and HTML are compared as rendered text with line-ending differences ignored.

The engine wrapper is npm run cli -- verify externs. It is not a build or CI gate.

Requirements

Names must be unique string literals. Missing or unrenderable callable types are emitted as unknown. Values need value as Type. The command skips *.test.ts, *.spec.ts, and __test__ sources.

Useful Links

This page collects external tools and references commonly useful for X-Ray and S.T.A.L.K.E.R. modding.

X-Ray Engine and Modding

Asset Tools

Development Tools

  • Windows Terminal: terminal for running build, CLI, and validation commands on Windows.
  • TypeScriptToLua: TypeScript-to-Lua compiler used by the XRF script build.
  • Tauri: framework used by the XRF tools desktop app.

Game SDK

The X-Ray Game SDK is the editor/toolchain used for assets such as levels, spawns, particles, models, and other engine-native data.

XRF does not replace the SDK. The project adds source-controlled script/config/UI/translation workflows and helper tools around game data, while SDK-style tools remain useful for native X-Ray asset authoring.

When to Use the SDK

Use SDK tools when you need to work with data that is not represented well as text source:

  • level editing;
  • spawn and graph authoring;
  • particle authoring;
  • model or animation workflows;
  • engine-native visual/editor data.

Use XRF source files and CLI tools when the change belongs to scripts, LTX/XML configs, UI XML, translations, or repeatable validation.

OpenXRay Reference

OpenXRay includes SDK-related work and documentation in its repository:

When SDK output is committed back to a project, keep generated binary output separate from hand-authored XRF sources.

Boundary with XRF

Treat SDK output as source only when the project intentionally owns that binary or editor-authored asset. Do not edit target/ output by hand and do not treat packed game data as the source of truth.

For script/config changes, prefer XRF text sources and validation commands. For native asset changes, use the SDK or format-specific XRF tools, then document how the generated asset should be reproduced.

Common handoff pattern

  1. Author or inspect the native asset in the SDK or a format-specific tool.
  2. Export the asset into the project-owned resource location.
  3. Rebuild or repack with XRF CLI commands.
  4. Verify the resulting game data in game, especially for spawn, level, model, animation, and particle changes.

Lua Debugger

Lua debugging for X-Ray scripts is limited by the engine runtime, luabind objects, and the fact that XRF source starts as TypeScript before being emitted as Lua.

For most day-to-day script work, start with:

  • focused Jest tests around TypeScript source;
  • XRF logs and game logs;
  • generated Lua inspection under target/gamedata;
  • engine-side debugging when the issue crosses into C++ or luabind behavior.

Breakpoints

Breakpoints in original TypeScript source are not equivalent to breakpoints in emitted Lua. If you attach a Lua debugger, set breakpoints against generated Lua paths and verify that the generated script names match what the engine loads.

Luabind classes, userdata, and C++ callbacks may not expose enough Lua-level state for convenient inspection.

Practical workflow

Start with the generated Lua file that corresponds to the TypeScript module. Confirm that the file exists under target/gamedata/scripts after a script build, then compare the emitted Lua names with the stack trace or log line from the engine.

Use Lua debugging for runtime-only questions such as callback order, engine object state, and values passed through luabind. Use Jest and TypeScript tests for parser, manager, scheme, and utility behavior that can be reproduced outside the engine.

When a Lua debugger cannot inspect userdata, add temporary logging around the TypeScript source and rebuild scripts. Keep those logs local or remove them before committing documentation or code changes.

Visual Studio Lua Debugger Research

The previous research link for Lua debugging is:

Treat this as research material, not a documented XRF-supported debugging workflow.

Debugging

Start with the narrowest tool that can explain the problem. Most XRF issues can be isolated with a focused build, runtime logs, and the debug panel before a native engine debugger is needed.

Choose the right tool

ProblemStart with
Build output is missing or staleThe relevant build command and project verification
A script throws or behaves incorrectlyLogs and a focused test
An NPC uses the wrong scheme or animationAI and logics
A form is missing or misalignedUI forms
Weather does not switch as expectedWeather
Lua code is slow or uses too much memoryPerformance statistics
A Lua API or engine callback behaves unexpectedlyEngine debugging

The XRF debug panel can inspect objects, managers, tasks, treasures, memory, profiling, and merged runtime data without leaving the game.

Basic workflow

  1. Reproduce the issue with a known engine variant and save.
  2. Rebuild only the affected target when possible.
  3. Check the engine and Lua logs for the first relevant error.
  4. Inspect live state with the debug panel or an engine overlay.
  5. Reduce the problem to a focused test, config section, scheme, or engine API call.
  6. Verify the fix with the same engine, save, and reproduction steps.

Do not build scripts with --no-lua-logs while investigating runtime behavior. Use a disposable save for console commands, object mutation, teleportation, or forced weather changes.

AI and logics

Use this page when an NPC is alive in game but its scheme, planner, relation, or animation state does not match what you expect.

Start with the in-game overlays when you need visual context. Switch to log dumps when you need the exact script state or GOAP action IDs.

Capture an NPC

NPC capture is an engine debug feature. It is useful when you need to inspect the world from the NPC perspective.

  1. Run the game with a mixed or debug engine build.
  2. Hold left Alt.
  3. Click the NPC.

The camera switches to the captured object. Release the capture before continuing normal gameplay testing.

AI overlays

Enable stalker and monster overlays from the console:

ai_dbg_stalker on
ai_dbg_monster on

ai_dbg_stalker on renders debug details for stalker objects:

Stalker AI debug overlay

ai_dbg_monster on renders debug details for monster objects:

Monster AI debug overlay

Use the matching off command to hide each overlay.

Planner and scheme logs

The XRF debug panel can print the selected object’s script state to the log. Open the main menu, press F11, switch to the object section, then choose one of the log buttons.

The object section can print:

  • the active scheme, active section, logic section, smart terrain, enemy, portable store, and scheme-specific state;
  • the engine action planner and the XRF state planner action IDs;
  • the state manager target state, current animation state, animstate, look target, combat flag, and ALife flag;
  • inventory contents, best weapon, best item, and relation data.

Planner show(...) output is only available when the engine exposes it. If the log says to run in mixed or debug mode, restart with a debug-capable engine build and repeat the dump.

Performance stats

Use engine stats when you need frame-level AI cost, not a script-level dump:

rs_stats on
ai_stats on

The AI stats overlay shows rows in this shape:

[name][min_time][avg_time][max_time][call_rate][call_count][total_time]
AI performance stats overlay

For Lua call profiling, use the XRF debug panel general section. It can start or stop the ProfilingManager, print hooked call counts, print manually marked profiling portions, show Lua memory use, and force Lua garbage collection.

Run Lua profiling with -nojit when you need cleaner call stats. The profiler logs a warning when LuaJIT is enabled because JIT compilation can change the measured call pattern.

Useful console commands

Common AI debug toggles are listed in the game engine command reference:

AI debug console commands

Engine debugging (C++)

Use engine debugging when the problem crosses from TypeScript/Lua into xray runtime behavior: scheduler timing, rendering, physics, UI initialization, luabind bindings, or console command implementation.

For script-only behavior, start with logs and the XRF debug panel first. They are faster and do not require rebuilding the engine.

Set up the engine project

OpenXRay has build guides for both supported development paths:

For local XRF development on Windows, the usual flow is:

  1. Build or select a mixed/debug-capable engine.
  2. Link XRF output and logs to the game folder with the XRF CLI link command.
  3. Start the engine from Visual Studio when you need C++ breakpoints.
  4. Start from the XRF CLI when you only need game logs and Lua output.

Debug Lua from Visual Studio

Lua debugging is possible through the Visual Studio Lua debugger extension, but it is limited by the engine and by the compiled Lua output.

  1. Install the LuaDkmDebugger Visual Studio extension.
  2. Use an engine configuration that loads Lua debug symbols.
  3. Run the game from Visual Studio.
  4. Set breakpoints in the generated Lua files, not in TypeScript source.

XRF TypeScript is compiled to Lua by TypeScriptToLua. Visual Studio will see the generated Lua code that the engine executes.

What to debug in C++

Use the engine source when you need to verify:

  • console command behavior such as rs_stats, rs_fps, ai_stats, set_weather, or run_string;
  • luabind signatures exposed through level, game, game_object, UI classes, planners, and packets;
  • UI XML initialization behavior in CScriptXmlInit and CUI controls;
  • engine-only debug overlays, stats, scheduler, physics, and renderer behavior.

Limitations

  • TypeScript breakpoints are not available in the engine. Debug generated Lua or C++ instead.
  • LuaDkmDebugger support is old and does not reliably inspect every luabind class or userdata value.
  • Some debug console commands are compiled only into mixed/debug engine builds.
  • Optimized script builds can strip Lua logger calls when built with --no-lua-logs.

Debug with logs

Logs are the quickest way to inspect XRF runtime behavior. By default, forge.ltx enables debug mode, regular Lua logging, and the separate Lua log file.

Find the logs

The engine writes the main log into the game logs folder. XRF tooling can link that folder into the project as target/logs_link.

Common files are:

  • openxray_<username>.log when a custom OpenXRay binary descriptor is present;
  • xray_<username>.log for the base engine name;
  • xrf_lua.log when separate Lua logging is enabled;
  • module-specific files such as xrf_profiling.log when a logger is configured with a file target.

The exact prefix depends on the logger configuration and the engine variant.

Check the logs

With a prebuilt engine:

  1. Select the engine build you want to run.
  2. Link the game folders with the XRF link command.
  3. Start the game.
  4. Inspect target/logs_link.

With Visual Studio:

  1. Start the engine project from Visual Studio.
  2. Check the Visual Studio Output window.
  3. Check the same game log files if the message was written through the engine logger.

The XRF CLI can print the end of the active engine log:

npm run cli -- logs
npm run cli -- logs 100

The default count is 15 lines. Values are capped at 200 lines. The CLI looks for the linked logs folder and then chooses openxray_<username>.log or xray_<username>.log depending on the detected engine.

Write Lua logs

Use LuaLogger from runtime code:

const logger: LuaLogger = new LuaLogger($filename);

logger.info("Spawned object: %s", object.name());
logger.table(state);
logger.pushSeparator();
logger.printStack();

LuaLogger formats messages with the current engine time, file prefix, level, and formatted text. It writes to the engine log unless a file-only logger is configured. When separate_lua_log_enabled is true, it also writes to the shared Lua log file.

Use a file logger when the stream is noisy or belongs to a specific subsystem:

const logger: LuaLogger = new LuaLogger($filename, {
  mode: ELuaLoggerMode.DUAL,
  file: "profiling",
});

DUAL writes both to the named file and to the normal engine log.

Flushing

Use the engine flush console command after generating important logs:

flush

The profiling manager calls flush after printing profiling reports so the latest stats are persisted before you leave the session.

Build-time logging flag

For performance or release-like builds, script compilation can strip Lua logger calls:

npm run cli -- build -i scripts --no-lua-logs

Do not use that flag when you are investigating runtime behavior.

Debug UI forms

XRF UI forms are authored as TSX/XML sources and loaded at runtime through xray CUI classes. Debug UI problems by checking both sides: the generated form structure and the runtime code that initializes named nodes.

Source and runtime files

Most UI work touches one of these areas:

  • form sources in src/engine/forms;
  • reusable form components in src/engine/forms/components;
  • runtime window classes in src/engine/core/ui;
  • XML render/build helpers in cli/utils/xml;
  • generated output under target, which should be regenerated instead of edited by hand.

When a runtime class calls xml.InitStatic, xml.Init3tButton, xml.InitScrollView, or another CScriptXmlInit method, the named XML node must exist in the form source.

Check the form path

Runtime code usually resolves a form by path:

const base: TPath = "menu\\debug\\DebugDialog.component";
this.xml = resolveXmlFile(base);

If a form does not appear, verify:

  • the base path matches the TSX/XML source path;
  • the build generated the matching XML file;
  • every node name used by the runtime class exists in the form;
  • both 4:3 and 16:9 variants were updated when the form has paired variants.

Use engine UI debugging

OpenXRay includes UI inspection tools for layout and control rectangles. They are most useful for positioning issues, wrong sizes, and overlapping controls.

The show_wnd_rect_all console command is available in XRF command constants and can help identify active UI control bounds in debug builds:

show_wnd_rect_all 1
show_wnd_rect_all 0

Some UI debug tools depend on the engine build. If the command has no effect, run with a mixed/debug-capable engine.

Common failure points

  • A runtime Init... call references a missing XML node.
  • A section exists in TSX but is not registered by the runtime dialog.
  • A button is visible but not registered with Register and AddCallback.
  • Text or controls fit in one aspect ratio but not in the paired 16:9 form.
  • The generated XML in target is stale after changing the TSX source.

References

Circular references

Circular references usually show up in XRF as initialization problems: a manager asks for another manager while modules are still loading, or a debug dump walks a graph of tables that point back to each other.

Manager resolution

Use getManager(ManagerClass) for normal manager access. It initializes the manager on first use and stores it in the runtime registry.

Use getManagerByName("ManagerName") only when importing the manager class would create a circular module dependency. It returns an already initialized manager by name and cannot create one by itself.

const surgeManager: SurgeManager = getManagerByName("SurgeManager") as SurgeManager;

If the result can be missing during startup, handle null instead of assuming the manager exists.

Trace module loading

forge.ltx has a debug flag for logger creation:

[debug]
resolve_log_enabled = true

When enabled, each LuaLogger prints a Declared logger message when it is created. This is noisy, but useful when you need to see which modules are loaded before a crash or circular dependency failure.

Turn it back off after the investigation:

[debug]
resolve_log_enabled = false

Dump circular data

The XRF JSON helper used by debug dumps is circular-reference aware. When it sees a table it has already visited, it writes:

"<circular_reference>"

When the dump exceeds the configured depth limit, it writes:

"<depth_limit>"

Use the debug panel general section to dump Lua state to _appdata_\\dumps\\lua_data.json, then search for these markers to find strongly connected runtime state.

Practical checks

  • Move type-only imports to import type when the dependency is only needed by TypeScript.
  • Prefer event callbacks or weak manager lookup when two managers need to observe each other.
  • Avoid top-level runtime work in modules that are imported by many systems.
  • Keep debug dumps bounded; do not serialize raw engine userdata.

Generate externals documentation

extern.json records the Lua-visible externs declared in TypeScript.

Regenerate

From the engine repository:

npm run cli -- build --include externs

This writes:

src/engine/declarations/extern.json
target/gamedata/extern.json

Check

npm run cli -- verify externs

The check compares the tracked JSON with current declarations and does not write files.

For optional HTML or XML references, use Extern exports.

Run custom scripts

The engine exposes two development console commands for running Lua from the game console: run_script and run_string. They are useful for short investigations on a throwaway save.

These commands are engine debug tools. Availability depends on the engine build.

Run a script file

Use run_script to run a .script file from the game scripts directory:

run_script my_debug_script

The engine rescans the scripts path before adding the script to the level script processor. Use this for repeatable debug snippets that are too large for a single console line.

Run one Lua expression

Use run_string for a single Lua command:

run_string level.set_weather("default_clear", true)
run_string db.actor:set_actor_position(vector():set(0, 0, 0))

The engine preserves argument casing for run_string, sends the string to the level script processor when one exists, and otherwise loads the buffer directly as @console_command.

XRF helper examples

Many XRF declarations are available through registered Lua modules after scripts are loaded. For example, debug notes in spawn configs use command shapes like:

run_string xr_effects.clear_smart_terrain(nil,nil,{"sim_smart_1"})
run_string alife():object(56):set_squad_position(patrol("tst"):point(0))

Prefer existing effects and helpers over hand-mutating unrelated manager state.

Safety

  • Test on a disposable save.
  • Keep commands small enough to read in logs.
  • Use logs or the debug panel to verify the effect after running the command.
  • Restart the session if you mutate state that the current manager lifecycle would normally own.

Performance statistics

Use engine stats for frame/render timing and script stats for Lua-side call patterns. They answer different questions, so enable the smallest view that matches the problem.

Rendering and frame statistics

Use these console commands while the game is running:

rs_stats on
rs_fps on
rs_fps_graph on

rs_stats enables the engine statistics overlay. rs_fps shows the FPS counter. rs_fps_graph shows the FPS graph. Use off to disable each toggle.

These commands are implemented by the engine, not by XRF scripts. Availability can depend on the engine build and renderer configuration.

AI statistics

For AI scheduler and planner timing:

ai_stats on

The AI stats overlay is useful when you need to see whether a cost is coming from engine AI updates rather than an XRF manager or scheme.

Lua profiling

The XRF ProfilingManager measures Lua call counts and manually marked profiling portions.

Open the XRF debug panel, switch to general, then use:

  • start/stop profiling to attach or clear the Lua debug hook;
  • log profiling stats to print hooked call counts;
  • log portions to print manually captured ProfilingPortion measurements;
  • refresh or collect memory to inspect Lua memory use.

For cleaner Lua hook stats, start the game with -nojit. LuaJIT can inline or alter execution in ways that make hook counts less representative.

Manual profiling portions

Use ProfilingPortion when you need to measure a specific code block:

const portion: ProfilingPortion = ProfilingPortion.mark("my_operation");

// Code to measure.

portion.commit();

The debug panel prints count, total duration, average duration, min, and max for captured portions.

Weather

XRF weather is managed by WeatherManager. It reads level weather settings, dynamic weather graphs, AtmosFear-style configuration, and weather FX state, then updates weather on actor spawn and on hourly game-time changes.

Runtime weather flow

On actor network spawn, the manager:

  1. reads the current level’s weathers setting from game.ltx;
  2. parses it as a condlist;
  3. initializes the current weather period and graph state;
  4. applies the first weather immediately.

During actor updates, it:

  • advances graph state when the game hour changes;
  • switches between good and bad weather periods;
  • marks transition and pre-blowout weather states;
  • updates depth-of-field settings for AtmosFear weather;
  • resumes weather FX from saved state when an FX is active.

Use the debug panel general section to dump Lua state when you need to inspect the live WeatherManager fields. The dump is written to _appdata_\\dumps\\lua_data.json.

Change weather from scripts

Script effects can force weather through xr_effects.set_weather, which calls level.set_weather:

on_info = %+some_info =set_weather(default_clear:true)%

The first argument is the weather section. The optional second argument controls whether the change is forced.

From Lua/TypeScript runtime code, the underlying engine call is:

level.set_weather(weatherName, isForced);

For weather effects, the engine exposes:

level.set_weather_fx("fx_surge_day_3");
level.start_weather_fx_from_time("fx_surge_day_3", time);

Weather console settings

WeatherManager applies commands from the weather_console_settings section in environment\\dynamic_weather_graphs.ltx during initialization. Use that section for console settings that must be applied with the dynamic weather system.

Debug weather editor issues

OpenXRay includes a weather editor project and editor documentation. Use it for visual tuning of weather sections and effects, then confirm the resulting section names and graph entries in XRF configs.

If a weather change does not appear:

  • verify the level weathers field resolves to the expected section or condlist branch;
  • verify the weather graph exists in dynamic_weather_graphs.ltx;
  • check whether a weather FX is currently playing;
  • check whether the level is treated as underground;
  • dump Lua state and inspect currentWeatherSection, nextWeatherSection, weatherFx, and weatherState.

References

XRF debug panel

The XRF debug panel is an in-game CUI dialog for development-only actions. It is available when forge.ltx has debug.enabled = true, which is the default project config.

Open the main menu and press F11. The panel hides the main menu while it is open. Press Esc or the cancel button to return to the main menu.

General

The general section shows Lua version, LuaJIT state, command-line arguments, and Lua memory usage.

It can also:

  • refresh memory usage;
  • force Lua garbage collection;
  • start or stop Lua profiling;
  • print profiling hook stats;
  • print manual profiling portion stats;
  • toggle simulation debug map overlays;
  • dump merged system.ini to _appdata_\\dumps\\system.ltx;
  • dump Lua manager state to _appdata_\\dumps\\lua_data.json.

Lua data dumping emits DUMP_LUA_DATA, so managers that registered a debug dump callback add their own state to the JSON payload.

Commands

The commands section exposes checkbox shortcuts for known console toggles.

Boolean commands are written as:

<command> on
<command> off

Numeric commands are written as:

<command> 1
<command> 0

The list includes AI debug toggles, HUD toggles, g_god, g_unlimitedammo, g_autopickup, and wpn_aim_toggle.

Object

The object section works with either the current target object or the nearest game object, depending on the section checkbox.

It can log:

  • scheme state and active section;
  • action planner and state planner details;
  • state manager details;
  • inventory contents;
  • relation data.

It can also set the actor relation to the object, kill the object, or set the object wounded. Use these actions on a throwaway save.

Items

The items section spawns inventory items for the actor. Categories are built from game config sections:

  • ammo;
  • artefacts;
  • consumables;
  • detectors;
  • helmets;
  • outfits;
  • weapons.

Ammo spawns as a stack of 30; other item categories spawn one item.

Spawn

The spawn section spawns creatures and simulation groups.

  • stalkers_list spawns the selected stalker section near the actor.
  • simulation_group_list spawns the selected squad section into the nearest smart terrain.

If there is no active game or no suitable smart terrain, the action is skipped and logged.

Teleport

The teleport section lists smart terrains. Selecting one moves the actor to that smart terrain.

If the target game vertex belongs to the current level, the panel sets the actor position directly. If it belongs to a different level, it calls game.jump_to_level. Double-click teleport also closes the main menu after moving the actor.

Registry and Tasks

The registry section lists ALife objects from the simulator, can filter online objects, and can print a registry summary with manager, scheme, object, smart terrain, event handler, and other collection counts.

The task section lists task config sections, can filter active tasks, and can give the selected task through TaskManager.

Treasures

The treasures section shows total, given, and found treasure counts. It can:

  • give all treasure coordinates;
  • give random treasure coordinates;
  • give one selected treasure coordinate;
  • teleport to the selected treasure restrictor when it exists.

The section also shows debug details for the selected treasure, including given/checked state, refresh flags, remaining items, and treasure type.

Current shell sections

player, position, and sound are present as panel sections, but their current TypeScript runtime classes only parse their form XML and do not bind interactive actions.

Game engine

XRF runs on top of the X-Ray engine used by S.T.A.L.K.E.R. Call of Pripyat style games. The engine owns the executable, renderer, file system, console, configuration loading, Lua VM, luabind exports, server objects, client objects, ALife simulation, and the low-level update loop.

XRF replaces the game script layer. TypeScript sources are compiled to Lua scripts, then loaded by the engine through the same script entry points that vanilla game logic uses.

What the engine owns

The engine starts from the executable. During startup it initializes core services, resolves the file system layout, loads configuration files, creates the console, initializes the Lua script engine, and opens engine bindings for Lua.

At runtime it also owns:

  • server-side ALife objects and the object factory;
  • client-side game objects and their object_binder instances;
  • save and load packets;
  • console command execution;
  • render, sound, input, and UI infrastructure;
  • frame updates and scheduled object processing.

The source references for these behaviors are the local xray-16 engine tree and the XRF X-Ray 16 SDK declarations.

What XRF adds

XRF provides the Lua scripts that the engine calls into:

  • _g.script preloads register, bind, and start, then registers global script externals.
  • register.script registers game classes, UI classes, server object classes, and callback functions.
  • start.script initializes managers, schemes, simulation helpers, extensions, and emits the XRF GAME_STARTED event.
  • bind.script maps engine object sections and script class names to XRF binder classes.

After that point, most game behavior goes through XRF managers, schemes, binders, and event callbacks.

Where to go next

  • Use Command line arguments when changing engine startup behavior.
  • Use Console commands for commands available after the console is initialized.
  • Use Execution flow to understand the order from executable startup to active gameplay.
  • Use Lifecycle when editing binders, managers, save/load code, or online/offline logic.
  • Use Luabind when a TypeScript class needs to be visible to the Lua engine runtime.

Command line arguments

Command line arguments are read by the engine before scripts are loaded. Use them for engine startup choices such as filesystem layout, renderer, logs, game mode, Lua JIT, and initial level loading.

npm run cli -- start_game starts the configured executable with -dump_bindings. It also supports --new, --load, --difficulty, and --no-intro for common test runs. To pass other engine arguments, run the executable directly, configure a launcher shortcut, or extend the local start command.

For example:

npm run cli -- start_game --new --difficulty gd_master --no-intro
npm run cli -- start_game --load quicksave

Common flags

FlagEffect
-fsltx <file>Use a specific filesystem configuration file before the engine core is initialized.
-ltx <file>Use a specific console/user configuration file instead of the default user.ltx.
-start <args>Execute a start ... console command after engine initialization.
-load <save>Execute a load ... console command after engine initialization.
-nointroSkip intro playback.
-nogameintroSkip the in-game intro sequence.
-nosplashDisable the startup splash screen.
-splashnotopShow the splash screen without forcing it on top.
-dedicatedStart in dedicated server mode.
-iDisable input capture used by the normal game window.
-overlaypath <path>Override the app data/logs root used by the engine locator.
-nologDo not create the main log file.
-unique_logsWrite logs with unique timestamped names.
-force_flushlogFlush log output aggressively. Useful when debugging crashes.
-nojitDisable LuaJIT JIT compilation. This also changes profiler behavior.
-dump_bindingsDump script binding information from the Lua script engine.

Renderer flags such as -r1, -r2, -r2a, -r2.5, -r3, -r4, and -rgl are engine-build dependent. Verify the selected executable before documenting a renderer as supported for a pack.

Game mode flags

OpenXRay-style builds can select a compatibility mode from the command line:

  • -cop for Call of Pripyat mode;
  • -cs for Clear Sky mode;
  • -shoc or -soc for Shadow of Chernobyl mode;
  • -unlock_game_mode to allow explicit game mode selection.

If no mode is selected, the engine can fall back to openxray.ltx compatibility settings.

Examples

Start with an explicit filesystem config and user config:

xrEngine.exe -fsltx fsgame.ltx -ltx user.ltx -nointro

Start a new local game through the engine console startup command:

xrEngine.exe -start "server(all/single/alife/new) client(localhost)"

Load a save after initialization:

xrEngine.exe -load quicksave

Quote values that contain spaces. Treat command line support as executable-specific: forks can add, remove, or rename flags.

Console commands

Console commands are available after the engine console is initialized. They can be typed in the in-game console, executed from config files, or triggered from scripts when the engine exposes command execution.

Most boolean commands use on/off. Some use 0/1, numeric ranges, enum values, or custom command strings.

Basic commands

CommandUse
helpPrint available console command help.
quitExit the game.
start ...Start a game session with server/client arguments.
disconnectDisconnect from the active session.
save <name>Save the current game.
load <name>Load a save.
load_last_saveLoad the most recent save.
main_menuReturn to the main menu.
cfg_save <file>Save console settings to a config file.
cfg_load <file>Load console settings from a config file.
flushFlush engine state where supported by the command implementation.
clear_logClear the current log output.

Script commands

run_script <name> reloads script paths and executes a script file through the engine script processor.

run_string <lua> executes a Lua string. In the inspected engine, the command preserves the original casing of the string payload instead of lowercasing it with the command name.

Use these commands for focused debugging. For repeatable development workflows, prefer tracked scripts and XRF externals instead of long console strings.

AI debug commands

The inspected OpenXRay-style engine registers these AI and ALife debugging commands:

  • ai_debug
  • ai_dbg_brain
  • ai_dbg_motion
  • ai_dbg_frustum
  • ai_dbg_funcs
  • ai_dbg_alife
  • ai_dbg_goap
  • ai_dbg_goap_script
  • ai_dbg_goap_object
  • ai_dbg_cover
  • ai_dbg_anim
  • ai_dbg_vision
  • ai_dbg_monster
  • ai_dbg_stalker
  • ai_stats
  • ai_dbg_destroy
  • ai_dbg_serialize
  • ai_dbg_dialogs
  • ai_dbg_infoportion
  • ai_dbg_node
  • ai_dbg_sight
  • ai_dbg_inactive_time
  • ai_draw_game_graph
  • ai_draw_game_graph_stalkers
  • ai_draw_visibility_rays
  • ai_animation_stats

Render, UI, sound, and gameplay commands

Useful command families include:

  • render toggles: rs_stats, rs_fps, rs_fps_graph, rs_vis_distance, rs_cam_pos, rs_wireframe;
  • video settings: vid_mode, vid_window_mode, vid_restart, renderer;
  • sound settings: snd_volume_eff, snd_volume_music, snd_restart, snd_device;
  • HUD settings: hud_draw, hud_info, hud_weapon, hud_crosshair, hud_crosshair_dist, hud_fov;
  • gameplay settings: g_game_difficulty, g_language, g_sleep_time, wpn_aim_toggle;
  • Lua debugging: lua_debug, lua_dump_depth;
  • ALife tuning: al_time_factor, al_switch_distance, al_process_time, al_objects_per_update, al_switch_factor.

Some commands are available only in debug builds or specific forks. Check the selected engine source before relying on a command in documentation or tooling.

XRF debug panel

XRF keeps a typed subset of console commands for the debug UI. If a command should be exposed from the panel, add it to the engine constants first and verify the selected engine accepts the same name and value type.

Execution flow

This page describes the normal flow from executable startup to active XRF gameplay. It is intentionally high-level: forks can move engine internals around, but XRF depends on the same script entry points.

1. Executable startup

The engine initializes core services, resolves filesystem paths, loads configuration files, initializes logging, creates the console, and applies startup command line arguments.

Important early choices include:

  • filesystem config from -fsltx;
  • console/user config from -ltx;
  • compatibility mode from -cop, -cs, -shoc, or -soc;
  • Lua JIT state from -nojit;
  • post-init start or load commands from -start and -load.

2. Lua script engine initialization

The script engine initializes Lua, opens luabind exports, opens the standard Lua libraries used by the selected build, adds game script paths to package.path, and loads script modules.

In XRF builds, _g.script is the root script entry point. It preloads:

  • register;
  • bind;
  • start.

It also registers global externals for conditions, effects, dialogs, tasks, and callbacks.

3. Class and callback registration

The engine calls into register.script to register script-visible game classes and resolve class identifiers.

XRF registers:

  • server object classes such as actors, stalkers, monsters, smart terrains, squads, items, weapons, anomalies, and physics objects;
  • UI classes such as the main menu;
  • engine callback functions exposed through global script paths.

The class registration step is what lets spawned engine objects construct the matching TypeScript-to-Lua classes.

4. XRF startup callback

The engine then calls start.callback(isNewGame).

XRF uses this callback to:

  • refresh class identifiers;
  • register the ALife simulator and ranks;
  • unlock system ini overriding;
  • initialize managers;
  • register scheme implementations;
  • register extensions;
  • emit GAME_STARTED.

Managers and schemes are available after this step.

5. Object creation and binding

The engine reads spawn data and creates server objects. When an object goes online on the client side, the engine asks bind.script for the binder class.

XRF binds engine objects to classes such as:

  • ActorBinder;
  • StalkerBinder;
  • MonsterBinder;
  • SmartTerrainBinder;
  • RestrictorBinder;
  • AnomalyZoneBinder;
  • WeaponBinder;
  • PhysicObjectBinder.

The binder receives lifecycle calls from the engine and becomes the bridge between low-level object state and XRF managers, schemes, and events.

6. Active gameplay loop

During gameplay, online binders receive updates, engine callbacks emit XRF events, managers react to those events, and save/load packets serialize state. Offline ALife objects continue to exist on the server side even when no client-side game object is active.

Known X-Ray engine forks

XRF targets the Call of Pripyat style X-Ray/OpenXRay API used by the local engine and type declarations. Forks are useful reference points, but they are not a compatibility guarantee. Verify bindings, console commands, save/load behavior, and script callbacks against the exact executable you ship.

Common references

Fork or baselineNotes
Original Call of Pripyat engine and gamedataCanonical behavior reference for vanilla script and resource behavior.
OpenXRay / xray-16Open-source X-Ray continuation used as the main local engine reference for XRF.
Call of Chernobyl engine familyUseful second opinion for evolved CoP-era behavior, but not the canonical baseline.
Anomaly / X-Ray Monolith familyHeavily modified fork family. Expect changed exports, fixes, callbacks, and engine-side assumptions.
OGSR EngineShadow of Chernobyl oriented fork with different compatibility expectations.
Oxygen and other experimental forksTreat as fork-specific until the script API is checked directly.

Compatibility checklist

Before moving XRF scripts to a fork, check:

  • luabind class names and exported functions;
  • object_binder method behavior;
  • game class identifiers and section-to-class mappings;
  • console command names and accepted value types;
  • command line flags used by your launcher;
  • save/load packet order and marker expectations;
  • availability of Lua libraries such as jit, ffi, marshal, and lfs;
  • callback names and callback argument order;
  • ALife online/offline switching behavior.

When a fork disagrees with vanilla resources and OpenXRay source, document the fork behavior as fork-specific instead of treating it as the default.

Lifecycle

The engine lifecycle is split between global startup, server-side ALife objects, client-side game objects, XRF binders, and XRF managers. Most script bugs come from mixing those scopes.

Global startup

Global startup happens once per game process and again at specific script reload points depending on the engine. In XRF, the important global scripts are:

  • _g.script, which preloads core entry points and registers externals;
  • register.script, which registers classes and callback globals;
  • start.script, which initializes managers, schemes, extensions, and emits GAME_STARTED;
  • bind.script, which returns binder classes for online objects.

Do not put per-save or per-object state in module globals unless it is intentionally reset during the relevant lifecycle step.

Binder lifecycle

Client-side game objects use engine object_binder lifecycle methods. XRF binders override the methods they need:

MethodWhen it is used
reinit()Reinitializes binder state and callbacks. Actor reinit also resets portable store state and schedules ALife update stabilization.
net_spawn(serverObject)Called when the object goes online and receives its server object. Return false to reject spawn after super.net_spawn(...) fails.
update(delta)Called while the client object is online. Use it for object-local work, not broad global polling.
net_destroy()Called when the object goes offline or is destroyed. Remove callbacks and unregister object state here.
save(packet)Write client-side state to the save packet. Preserve marker and write order.
load(reader)Read client-side state from the save packet in the same order it was written.

Actor, stalker, monster, restrictor, smart terrain, physic object, and item binders all follow this pattern.

Manager lifecycle

Managers extend AbstractManager. A manager can implement:

  • initialize() to register callbacks or allocate state;
  • destroy() to unregister callbacks and mark state as disposed;
  • update(delta) when it is driven by an update event;
  • save(packet) and load(reader) when it owns serialized state.

Managers should subscribe through EventsManager rather than being called from unrelated binders. This keeps object lifecycle code small and makes save/load ownership clearer.

Event lifecycle

EventsManager owns typed event subscriptions. Binders and game objects emit events such as:

  • ACTOR_GO_ONLINE, ACTOR_GO_OFFLINE, ACTOR_REINIT;
  • ACTOR_UPDATE, ACTOR_UPDATE_100, ACTOR_UPDATE_500, ACTOR_UPDATE_1000, ACTOR_UPDATE_5000, ACTOR_UPDATE_10000;
  • STALKER_DEATH, MONSTER_DEATH, HIT;
  • GAME_SAVE, GAME_SAVED, GAME_LOAD, GAME_LOADED;
  • BEFORE_LEVEL_CHANGE and GAME_STARTED.

Actor update drives the global timer manager tick and the throttled actor update events. Prefer those throttled events for recurring manager work that does not need every frame.

Save and load lifecycle

Save/load code must preserve packet order. XRF commonly wraps sections with save/load markers, calls the superclass method, then writes or reads owned state.

If a manager or binder adds state to a save packet, update the corresponding load code in the same change. Never insert a new read without matching old saves or version handling.

AI and logics

XRF AI logic is built on the engine GOAP planners, engine callbacks, XRF schemes, and the XRF stalker state manager. The engine still owns navigation, combat primitives, visibility, danger evaluation, animation execution, and the base planner runtime. XRF adds script-side actions, evaluators, scheme state, and event routing.

Use this page to understand the structure. Use the debugging AI page when you need runtime overlays, planner dumps, or log output.

Motivation planner

Each stalker has an engine motivation action planner. XRF modifies that planner when the stalker binder is reinitialized. The setup adds script evaluators and actions that coordinate engine behavior with XRF-controlled animation and logic state.

The motivation planner includes engine action ids for broad behaviors such as ALife, combat, anomaly, danger, gathering items, smart terrain tasks, and death. XRF adds custom action ids for script activities such as animpoint, walker, remark, sleeper, companion, smart cover, wounded, abuse, and state-to-idle transitions.

State planner

StalkerStateManager owns a separate Lua-side action planner for stalker state control. It manages the target state and drives sub-planners for:

  • weapon state;
  • movement;
  • look direction;
  • mental state;
  • body state;
  • animstate;
  • animation;
  • smart cover;
  • locked states.

The state planner goal is to reach its END state after the required weapon, movement, mental, body, direction, animstate, animation, and smart cover evaluators are satisfied.

Evaluators

Evaluators answer yes/no questions for the planner. Examples include whether the stalker is already standing, walking, in danger mental state, using a target weapon state, playing an animation, locked by animation, or inside a smart cover.

Evaluator ids are stable planner contracts. Changing ids can break planner graphs and debug output, even if TypeScript still compiles.

Actions

Actions change world state. XRF actions can strap or unstrap weapons, set movement, turn the stalker, switch mental and body state, start or stop animations, and enter or leave smart cover.

Actions and evaluators used by engine planners must be luabind-visible classes. Keep @LuabindClass() on planner classes that the engine action planner constructs or stores.

Schemes

Schemes are the script logic layer loaded from LTX logic sections. A scheme activates state for a specific object and section, subscribes handlers, and reacts to scheme events such as switching online/offline, death, hit, use, or extrapolation.

The stalker binder wires schemes into object lifecycle:

  • creates StalkerStateManager during reinit;
  • sets up the state planner and motivation planner;
  • initializes object logic on spawn;
  • updates the state manager while the object is online;
  • emits scheme events when the object switches offline, dies, is hit, or is used.

Keep scheme state in the object registry and clean it through the scheme lifecycle. Avoid storing per-object scheme state only in module globals.

Luabind

Luabind is the bridge between C++ engine code and Lua scripts. The engine exports C++ classes, functions, enums, and helpers into Lua, then game scripts create Lua-side classes that inherit from those bindings.

XRF TypeScript compiles to that Lua layer. A class that must be constructed or called by the engine needs to follow the same luabind-visible shape after compilation.

@LuabindClass()

Use @LuabindClass() on TypeScript classes that must be visible as Lua classes. Common examples include:

  • binders that extend object_binder;
  • action and evaluator classes used by GOAP planners;
  • UI classes that extend engine CUI classes;
  • server object classes registered through the factory.

The decorator preserves the class metadata expected by the TypeScript-to-Lua and luabind runtime path.

Class names

Many registrations use the class __name field. XRF passes those names to engine registration code for game classes, UI classes, and binder construction.

Changing a class name can therefore change runtime behavior even when TypeScript imports still compile. Treat class renames as compatibility changes.

Externals are separate from luabind

XRF also has an extern(...) helper. It writes values into _G or nested global tables so the engine and configs can find script callbacks such as conditions, effects, task functions, dialog functions, and startup callbacks.

That is not the same as binding a C++ class. Use luabind classes when the engine constructs or calls class instances. Use externals when a named global function or table entry must exist in Lua.

class, property, and super

OpenXRay luabind exposes helper globals such as class, property, and super. They come from the luabind runtime, not from XRF.

Modern XRF code usually does not call these helpers directly. TypeScript classes and @LuabindClass() generate the Lua shape that the engine expects.

Verification

Use the XRF X-Ray 16 SDK to check TypeScript-visible API shape. For ambiguous behavior, check the engine binding code in the selected xray-16 fork, because some binding setters and object methods have engine-specific semantics.

Lua extensions

The engine Lua environment is not plain standalone Lua. OpenXRay-style builds initialize LuaJIT, luabind, engine exports, standard libraries, and a few engine-specific libraries before game scripts run.

The inspected script engine opens the usual Lua libraries such as base, package, table, io, os, math, string, bit, and ffi. Debug builds can also expose the Lua debug library. LuaJIT is opened unless the executable starts with -nojit.

Runtime availability

XRF can have TypeScript declarations for a library even when a specific engine executable does not load that library. Always separate:

  • compile-time declarations in src/typedefs;
  • runtime modules actually opened by the engine;
  • modules shipped in the selected gamedata or Lua environment.

This matters for marshal and lfs: XRF has typings for them, but availability depends on the chosen engine/runtime package.

Script path

The engine appends gamedata script paths to package.path, allowing scripts to be required from the game script directory. Keep runtime require(...) names aligned with the emitted Lua script layout.

Practical checks

When adding a dependency on a Lua module:

  1. check the TypeScript declaration under src/typedefs;
  2. check whether the target executable opens or ships the module;
  3. run the game with the same executable that will ship to users;
  4. keep fallback behavior for optional modules.

For engine-bound code, prefer X-Ray APIs and XRF helpers over standalone Lua assumptions. The engine can change module availability, package paths, and debug library access depending on build flags.

Custom Lua

OpenXRay-style builds use a custom LuaJIT runtime rather than a stock standalone Lua executable. The engine initializes Lua, opens luabind, registers engine exports, configures script paths, and then loads game scripts.

This matters for XRF code because TypeScriptToLua output runs inside the game script engine, not inside a generic Lua CLI. Available globals, module search paths, JIT behavior, and engine bindings come from the selected executable.

Libraries opened by the engine

The inspected script engine opens standard Lua libraries and LuaJIT-related libraries used by game scripts:

  • package, table, io, os, math, and string;
  • bit and ffi;
  • LuaJIT support unless -nojit is passed;
  • debug in non-master/debug-capable builds;
  • engine-specific helpers such as xrluafix;
  • Tracy Lua integration when compiled into the engine.

Do not assume every fork opens the same set. Check the selected executable if a script depends on a non-standard module.

Useful flags

-nojit disables LuaJIT JIT compilation. This can make some debugging sessions easier, but it also changes profiler behavior.

-dump_bindings asks the script engine to dump binding information. Use it when comparing what the engine exported with what the TypeScript declarations say exists.

Notes for XRF scripts

  • Do not assume optional Lua modules such as LFS or marshal are loaded unless the runtime package opens or provides them.
  • Use engine APIs for game objects, packets, configs, and path resolution. Standalone Lua behavior is a weak reference when engine bindings are involved.

Marshal

marshal is a Lua serialization library. XRF has TypeScript declarations for the functions used by the runtime:

  • marshal.encode(value) converts a Lua value to an encoded representation;
  • marshal.decode(value) reads an encoded representation back;
  • marshal.clone(value) creates a cloned value through marshal semantics.

The declarations reference the upstream Lua marshal project: https://github.com/richardhundt/lua-marshal.

Availability

The inspected engine script initialization does not open marshal as one of the default Lua libraries. Treat it as an optional runtime dependency unless the selected executable or gamedata package explicitly provides it.

Guard code that depends on marshal, or keep usage in paths where the runtime package is known.

When to use it

Use marshal only when the runtime really needs Lua-level serialization or cloning. For game save data, prefer the engine save packet APIs and XRF save/load helpers so the data remains compatible with the engine lifecycle.

Good candidates are short-lived Lua tables in tooling, debug-only data capture, or controlled runtime features where the package is bundled with the executable. Avoid using it as an implicit dependency for core gameplay scripts.

Validation notes

  • Check that require("marshal") succeeds in the selected executable before using these declarations.
  • Keep encoded data versioned if it can persist outside the current process.
  • Prefer explicit table copies when the shape is small and known; marshal.clone is useful only when marshal semantics are the intended behavior.

LFS

LFS is LuaFileSystem. It provides filesystem operations such as directory iteration, attribute inspection, directory creation, links, locks, and current-directory changes.

XRF has TypeScript declarations for common LFS functions:

  • lfs.attributes(path);
  • lfs.dir(path);
  • lfs.currentdir();
  • lfs.chdir(path);
  • lfs.mkdir(path);
  • lfs.rmdir(path);
  • lfs.link(oldPath, newPath);
  • lfs.touch(path, atime, mtime);
  • lfs.lock(file, mode, start, length);
  • lfs.unlock(file, start, length);
  • lfs.symlinkattributes(path);
  • lfs.setmode(file, mode);
  • lfs.lock_dir(path, seconds).

The upstream library documentation is available at https://lunarmodules.github.io/luafilesystem/.

Availability

The inspected engine script initialization does not open LFS as one of the default Lua libraries. Treat it as optional unless your selected runtime package ships it.

For game paths, prefer engine filesystem helpers and configured path aliases. Use LFS for plain filesystem work only when the runtime dependency is verified.

Practical use

Use LFS for tools or controlled runtime packages where filesystem access is part of the environment contract. Avoid it inside portable gameplay code unless the target executable is known to expose the module.

For game data lookup, prefer engine path aliases and file-system helpers because they follow mounted game paths and mod layout rules. LFS works on process-visible filesystem paths; it does not know about engine virtual paths by itself.

Verification checklist

  • Check that require("lfs") succeeds in the exact runtime package you ship.
  • Check path separators and working directory assumptions on the target platform.
  • Keep save-game and game-state persistence on engine packet APIs instead of plain files unless the feature explicitly owns external files.

Online and offline

X-Ray keeps two related views of many objects:

  • a server-side ALife object, which can exist while the object is offline;
  • a client-side game object, which exists when the object is online and active on the current level.

Online/offline state is not the same as alive/dead. An offline object can still exist in simulation. An online object has a live game_object, a binder, callbacks, and client-side updates.

Offline objects

Offline objects live in the ALife simulation. They are represented by server objects and can be queried through simulator APIs. Smart terrains, squads, NPCs, monsters, items, and level changers all have server-side behavior in different ways.

Use server objects for simulation state, spawn data, story ids, smart terrain membership, and logic that must survive outside the active client bubble.

Online objects

When an object switches online, the engine creates or activates a client-side game_object and attaches an object_binder. XRF uses binders to register object state, set callbacks, emit events, and update active schemes.

Use online objects for:

  • direct game object methods;
  • visible object state;
  • callbacks such as hit, death, use, inventory, or task updates;
  • per-frame or throttled client-side updates.

Do not assume level.object_by_id(id) succeeds for an offline object. Use the simulator/server object path when the object may be offline.

Switching

The engine decides when objects switch online or offline based on ALife rules, level state, distance, and object type. Console variables such as al_switch_distance, al_objects_per_update, and related ALife settings affect this process.

XRF binders receive net_spawn(...) when an object goes online and net_destroy() when it goes offline or is destroyed. Put registration and callback setup in the online path, and cleanup in the offline path.

Updates and scheduling

The engine drives updates for online binders. XRF turns those updates into higher-level events and timers so managers do not need to poll unrelated objects directly.

Binder updates

An online binder can receive update(delta) from the engine. Use it for object-local behavior:

  • actor update orchestration;
  • stalker and monster state managers;
  • active restrictor, anomaly, smart terrain, and physic object logic;
  • sound manager updates tied to a specific object id.

Call super.update(delta) when overriding an engine binder method unless nearby code shows a deliberate reason not to.

Actor update events

ActorBinder.update(delta) is the central XRF update pump. It emits:

  • ACTOR_FIRST_UPDATE once after start or load;
  • ACTOR_UPDATE every actor update;
  • ACTOR_UPDATE_100;
  • ACTOR_UPDATE_500;
  • ACTOR_UPDATE_1000;
  • ACTOR_UPDATE_5000;
  • ACTOR_UPDATE_10000.

It also ticks the XRF timer manager and refreshes actor-related simulation object availability.

Use the throttled actor events for recurring manager work. For example, weather, input, psy, debug, and UI-related systems can subscribe to the event cadence they actually need.

Timers

EventsManager extends the timer manager and supports delayed and interval callbacks. Timers tick from the actor update path, so they require an active actor update loop.

Use timers for short delayed script work. Do not use them as durable save/load state unless the owning manager explicitly serializes enough data to restore the behavior.

ALife scheduling

Offline simulation is controlled by the engine and ALife settings. XRF can temporarily adjust object processing during startup; for example, actor reinit allows a broader ALife update window and then schedules a return to the stable configured value.

Keep online binder updates, manager event updates, and offline ALife scheduling separate. They run at different layers and have different save/load assumptions.

Script engine

The script engine chapter documents the XRF gameplay layer: TypeScript scripts, generated Lua, LTX/XML configs, forms, translations, schemes, managers, binders, and gameplay data.

The engine starts Lua and calls XRF entry points. XRF then owns most gameplay behavior above the native X-Ray runtime.

Source layout

Source areaPurpose
src/engine/scriptsLua script entry points and externally callable script declarations.
src/engine/coreRuntime managers, binders, schemes, objects, utils, and domain logic.
src/engine/configsStatic and generated LTX/XML game configuration source.
src/engine/formsJSX source for generated UI XML forms.
src/engine/translationsTranslation JSON and XML source.
src/resourcesStatic resource data copied into gamedata output.

Generated Lua, generated configs, copied resources, coverage, and packed outputs are written under target/. Do not edit target/ by hand.

Runtime entry points

The script layer starts from _g.script, then loads register, bind, and start.

  • register exposes game classes, UI classes, tasks, dialogs, conditions, effects, and callbacks.
  • bind selects object binder classes for online engine objects.
  • start initializes managers, schemes, extensions, simulation state, and emits GAME_STARTED.

Most gameplay work is then handled by managers, schemes, object binders, and event callbacks.

Config-driven behavior

XRF keeps vanilla-style config behavior where possible. LTX logic sections still drive object logic, condlists still call conditions and effects, and XML files still define dialogs, tasks, character descriptions, UI forms, and gameplay data.

When changing behavior, trace the full path:

  1. config field or XML entry;
  2. parser or build helper;
  3. runtime manager, scheme, binder, or extern;
  4. test or validation command.

For config changes, compare against original gamedata when baseline behavior matters.

Validation

Use focused validation first:

npm test -- <path-or-pattern>
npm run typecheck
npm run cli verify ltx
npm run cli -- build --include configs --filter <pattern>

Use broader npm run verify or npm run build when a change touches shared config, generated output, or multiple runtime systems.

Assets

Assets are static gamedata resources copied into the build output. Keep them separate from TypeScript, generated Lua, generated configs, and generated UI XML.

The main source directory is src/resources. Project asset override roots can also be included by the build when asset overrides are enabled for a language.

What gets copied

The resources build step copies files and folders into target/gamedata. It skips repository metadata and unpacked working folders such as textures_unpacked and particles_unpacked.

It also rejects resource folders that overlap with generated source areas such as core, configs, lib, and scripts. Those names are reserved for generated or source-owned content.

Filtering

Build filters are applied to resource paths. Use a filter when you only need to rebuild a focused asset group:

npm run cli build -- --filter textures
npm run cli build -- --filter sounds

The filter behavior is path based. Check the source path when a filtered build appears to skip an expected file.

Editing assets

Treat src/resources as base resource data. Avoid casual edits there unless the work is explicitly about resources. For new generated game data, prefer the appropriate source folder:

  • configs in src/engine/configs;
  • UI forms in src/engine/forms;
  • translations in src/engine/translations;
  • scripts in src/engine/scripts and src/engine/core.

Do not patch packed output or files under target/.

Validation workflow

After asset changes, run the narrowest build filter that covers the path you touched. Then inspect target/gamedata to confirm the copied path matches the engine-facing layout.

Use format-specific tools for packed or structured assets:

  • icon texture workflows through icons commands;
  • particles.xr workflows through particles commands;
  • spawn workflows through spawn commands;
  • archive inspection through the Tools CLI archive commands.

If an asset change depends on config references, validate the LTX/XML source that points at the asset as well.

Configs

Game configuration source lives under src/engine/configs. The build copies static .ltx and .xml files and generates additional .ltx or .xml files from TypeScript sources.

Configs are runtime behavior. They can switch schemes, call effects, check conditions, spawn objects, define smart terrain jobs, describe tasks, load dialogs, configure weapons, and drive weather.

Domain pages

The config chapter is split by the kind of runtime data being edited.

PageUse it for
SchemeHow $scheme/*.scheme.ltx files validate LTX sections.
CondlistsConditional expressions used by script configs and scheme switching.
DialogsDialog XML sources, phrase graphs, and dialog-related config data.
ScriptsStory script configs, scheme sections, effects, conditions, and logic activation.
CreaturesActor, stalker, monster, crow, and online/offline group configs.
Zones and anomaliesAnomaly fields, anomaly zones, restrictors, camp zones, and level changers.
Smart terrains and jobsSmart terrain population, jobs, respawn, and simulation behavior.
TreasuresTreasure descriptors and hidden stash behavior.
WeaponsWeapon section layout and generated weapon config helpers.
WeatherWeather cycles, graph sources, and weather config generation.

Source types

Source typeBuild behavior
.ltxCopied to target/gamedata/configs.
.xmlCopied to target/gamedata/configs.
.tsImported by the CLI and rendered to .ltx with renderJsonToLtx.
.tsxImported by the CLI and rendered to .xml with renderJsxToXmlText.
$scheme/*.scheme.ltxUsed by verify-ltx to validate config shape.

Dynamic .ts LTX sources must export create() or config. Dynamic .tsx XML sources must export create().

Includes and inheritance

LTX files use normal X-Ray include and inheritance syntax:

#include "items\weapons\base.ltx"

[wpn_example]:identity_immunities
class = WP_AK74

Generated LTX sources can express includes and section inheritance through the helper symbols used by renderJsonToLtx.

Validation

Run LTX validation after changing config shape, includes, inheritance, or $scheme coverage:

npm run cli verify ltx

For generated configs, also run a focused build:

npm run cli build -- --filter configs

Do not edit generated files under target/. Fix the source config or generator instead.

LTX scheme

src/engine/configs/$scheme contains validation schemas for LTX configs. The verify-ltx tool uses these files to check includes, inheritance, field names, and field types.

The root file is scheme.ltx. It includes category schemas such as base.scheme.ltx, script.scheme.ltx, environment.scheme.ltx, items.scheme.ltx, weapons.scheme.ltx, and zone.scheme.ltx.

Defining a schema section

A schema section starts with a section name. Use inheritance when several config sections share fields:

[$item_weapon]:$item,$item_weapon_sounds,$item_weapon_params
strict = true
ammo_class = ?string[]
ammo_mag_size = ?u32
weapon_class = ?enum:assault_rifle,shotgun,sniper_rifle,heavy_weapon,pistol,grenade,misc

Schema section names commonly start with $ to separate schemas from game config sections.

Field syntax

SyntaxMeaning
name = stringRequired string field.
name = ?stringOptional string field.
name = string[]Array of strings.
name = tuple:f32,f32,f32Tuple with fixed element types.
name = enum:on,offValue must be one of the listed enum values.
name = sectionValue should reference a section.
* = tuple:f32,f32,f32,f32Wildcard rule for arbitrary field names.

Optional marker ? can be combined with arrays, enums, tuples, and section references.

Available types

Common schema types:

  • string
  • section
  • tuple
  • condlist
  • f32
  • u32
  • i32
  • u16
  • i16
  • u8
  • i8
  • bool
  • vector
  • enum
  • unknown
  • any

Prefer the narrowest type that matches the engine behavior. Use unknown or any only when the field is intentionally untyped or still being investigated.

Arrays, enums, and tuples

Arrays use []:

levels = ?string[]
installed_upgrades = ?section[]

Enums list allowed values:

scope_status = ?enum:0,1,2
flares = enum:on,off

Tuples define ordered values:

fire_point = ?tuple:f32,f32,f32
hit_power = ?tuple:f32,f32,f32,f32

Strict sections

strict = true means fields not described by the schema are validation errors unless a wildcard rule covers them. Use strict schemas for stable config formats such as weapons, weather, and script definitions.

Leave strict mode off only when the config format is intentionally open-ended or not fully modeled yet.

Verification

Run:

npm run cli verify ltx

When a valid config fails validation, update the matching schema section instead of weakening unrelated schemas. When a schema accepts invalid data, add a narrower field type or enable strict mode for that schema if the format is stable.

Condlists

Condlists are conditional config expressions used throughout script configs, task configs, dialogs, and scheme switches. A condlist can check info portions, call conditions, apply side effects, and select a value.

Basic shape:

{conditions} value %effects%

Multiple entries are separated by commas and checked in order. The first matching entry wins:

on_info = {+quest_started} walker@active %=play_sound(quest_start)%, sr_idle

Conditions

SyntaxMeaning
+info_nameRequire an info portion.
-info_nameRequire a missing info portion.
=condition_nameCall xr_conditions.condition_name and require true.
!condition_nameCall xr_conditions.condition_name and require false.
~50Pass with a random chance from 1 to 100.

Function parameters use colon separators:

{=actor_has_item(af_oasis_heart)}
{!npc_in_actor_frustum =dist_to_actor_le(30)}

Effects

Effects live inside %...%:

%=give_inited_task(jup_b1_task) +jup_b1_started -jup_b1_waiting%

Inside effects:

  • =effect_name calls xr_effects.effect_name;
  • +info_name gives an info portion;
  • -info_name disables an info portion.

This means reading a config value can change game state. Check the caller before assuming a condlist is pure.

Generated condlists

Generated LTX sources should use helpers from cli/utils/ltx/condlist.ts:

  • checkCondition(...)
  • checkNoCondition(...)
  • checkChance(...)
  • checkHasInfo(...)
  • checkNoInfo(...)
  • callEffect(...)
  • addInfo(...)
  • removeInfo(...)
  • createCondlist(...)
  • joinCondlists(...)

These helpers keep generated syntax consistent with the runtime parser.

Parser limits

The parser is pattern-based. Avoid nested commas, nested parentheses, and quoted strings that require custom escaping. If new syntax is needed, update parser tests before relying on it in configs.

Debugging workflow

When a condlist does not behave as expected:

  1. identify the caller, such as a scheme switch, task field, dialog phrase, or smart terrain job;
  2. check whether the caller expects a returned section/value or only side effects;
  3. search for the condition under src/engine/scripts/declarations/conditions;
  4. search for the effect under src/engine/scripts/declarations/effects;
  5. add or update tests for parser helpers when generated syntax changes.

Keep side effects guarded with info portions when the same condlist can be evaluated repeatedly.

Dialog configs

Dialog configs define conversation XML and the script predicates/actions used by dialog phrases. XRF keeps dialog data under src/engine/configs/gameplay and dialog externs under src/engine/scripts/declarations/dialogs.

Use this page when you need to add a phrase, wire a phrase to script, or check why a dialog option is not visible.

Source files

Core dialog XML files:

  • src/engine/configs/gameplay/dialogs.xml
  • src/engine/configs/gameplay/dialogs_zaton.xml
  • src/engine/configs/gameplay/dialogs_jupiter.xml
  • src/engine/configs/gameplay/dialogs_pripyat.xml

Dialog text lives in translation files such as:

  • src/engine/translations/st_dialogs.json
  • src/engine/translations/st_dialogs_zaton.json
  • src/engine/translations/st_dialogs_jupiter.json
  • src/engine/translations/st_dialogs_pripyat.json

Script callbacks live in:

  • src/engine/scripts/declarations/dialogs/dialogs.ts
  • src/engine/scripts/declarations/dialogs/dialogs_zaton.ts
  • src/engine/scripts/declarations/dialogs/dialogs_jupiter.ts
  • src/engine/scripts/declarations/dialogs/dialogs_pripyat.ts
  • src/engine/scripts/declarations/dialogs/dialog_manager.ts

Generic dialog state is handled by src/engine/core/managers/dialogs/DialogManager.ts. The manager tracks phrase priority tables, disabled phrases, and generic phrase categories such as hello, job, anomalies, and information.

Runtime hooks

Dialog XML can call script functions through registered dialog externs. XRF registers these from src/engine/scripts/declarations/dialogs. The global script entry point loads externals_registrator, and the registrator exposes dialogs, dialogs_zaton, dialogs_jupiter, dialogs_pripyat, and dialog_manager.

dialog_manager.ts also exports callbacks used by generated generic dialogs. Examples include:

  • dialog_manager.init_new_dialog;
  • dialog_manager.fill_priority_hello_table;
  • dialog_manager.precondition_job_dialogs;
  • dialog_manager.action_information_dialogs;
  • dialog_manager.action_disable_phrase.

Keep XML callback names in sync with the extern name. A typo in XML does not create a TypeScript error at the call site.

When changing a dialog:

  1. update the XML phrase flow;
  2. update translation ids used by the phrases;
  3. update or add dialog predicate/action externs when XML calls script;
  4. test the dialog declaration code when behavior changes.

Editing workflow

Start from the dialog id and search across gameplay XML, translation JSON, and dialog declarations. Most quest dialogs touch at least two of those areas: XML controls phrase flow, translations hold the text, and declarations decide whether a phrase is visible or what action runs after selection.

For a script-backed phrase:

  1. add the XML phrase node and translation id;
  2. reference an existing callback or add a new extern(...) in the matching declaration file;
  3. update tests beside the declaration when the callback has logic;
  4. run a focused test for the declaration file, then run config verification if XML changed.

Use location-specific declaration files when the condition belongs to a level quest. Use dialog_manager.ts for generic dialog category behavior only.

Generated XML

Some gameplay XML is generated from .tsx sources. Dynamic XML sources export create() and are rendered with renderJsxToXmlText.

Do not edit generated XML under target/. Change the XML source or TSX generator.

Validation notes

  • externals_registrator.test.ts confirms the dialog extern namespaces are registered.
  • Dialog declaration tests check individual callback bindings and branch behavior.
  • Translation ids must exist in the matching st_dialogs*.json file, otherwise the game can show raw ids or missing text.
  • If a dialog changes quest state, check the related task, info portion, and effect declarations in the same pass.

Script configs

Script configs are LTX files that drive object logic. They live under src/engine/configs/scripts and are copied to target/gamedata/configs/scripts.

These files are active gameplay data. They select schemes, configure object binders, define section switches, call conditions and effects, and describe smart terrain jobs.

Common structure

Most logic files start from [logic]:

[logic]
active = sr_idle

[sr_idle]
on_info = {+some_info} sr_idle@done %=some_effect%

[sr_idle@done]

The active section name selects the scheme. Suffixes such as @done let one scheme have multiple named states.

Common fields

Common script logic fields include:

  • active in [logic], selecting the first section;
  • on_info, on_signal, on_timer, and related switch fields;
  • on_actor_inside, on_actor_outside, and zone-related switch fields;
  • on_death and on_hit for event-driven switches;
  • suitable and prior for smart terrain job selection;
  • spawn for item sections spawned on activation;
  • scheme-specific fields such as paths, animations, sounds, dialogs, and combat flags.

Field support depends on the active scheme. Check the scheme implementation before adding a field.

Extern calls

Condlists call short names, but the registered functions live under global namespaces:

  • {=actor_has_item(wpn_pm)} calls xr_conditions.actor_has_item;
  • %=give_inited_task(task_id)% calls xr_effects.give_inited_task.

Search both the short config name and the full extern name before renaming a condition or effect.

Validation

Use LTX validation after editing script configs:

npm run cli verify ltx

For behavior changes, test the scheme, condition, effect, or manager that reads the field.

Editing workflow

Start from the object, smart terrain job, or restrictor that owns the logic file. Follow [logic] active to the active section, then identify the scheme from the section prefix before @.

When a switch does not fire, check the pieces in this order:

  1. the active section name;
  2. the switch field supported by that scheme;
  3. the condlist conditions and effects;
  4. the target section existence;
  5. any runtime event needed to trigger the manager, such as hit, death, use, signal, or actor-zone update.

Creatures

Creature configs describe the actor, stalkers, monsters, crows, and online/offline squads. They live under src/engine/configs/creatures and are included through src/engine/configs/creatures/index.ltx.

These files are not only stats tables. A creature section also selects the engine class, script binder, physical settings, perception values, sounds, movement tuning, immunities, and condition sections used by runtime logic.

Source files

SourcePurpose
creatures/index.ltxIncludes all creature config files into the final game config tree.
creatures/base.ltxDefines shared monster defaults and common physical death-friction parameters.
creatures/actor.ltxDefines the playable actor section, condition sections, HUD link, hit sounds, movement, and immunities.
creatures/m_stalker*.ltxDefines stalker, monolith stalker, and zombied stalker creature sections.
creatures/m_*.ltxDefines monster species such as bloodsucker, boar, burer, chimera, controller, dog, flesh, gigant, poltergeist, pseudodog, snork, and tushkano.
$scheme/creatures.stalker.scheme.ltxValidates actor and stalker config fields.
$scheme/creatures.monster.scheme.ltxValidates monster config fields.
$scheme/creatures.misc.scheme.ltxValidates online/offline group sections used for squad descriptors.

Runtime binding

Creature sections attach TypeScript runtime code through script_binding.

BindingRuntime role
bind.actorAttaches actor lifecycle, callbacks, managers, actor state, and save/load behavior.
bind.stalkerAttaches NPC stalker lifecycle, logic activation, callbacks, smart terrain participation, and planner behavior.
bind.monsterAttaches monster lifecycle, monster scheme activation, smart terrain participation, and offline handling.
bind.crowAttaches crow-specific runtime behavior.

The engine class still matters. For example, actor sections use an actor class, stalker sections use stalker classes, and monster sections use species-specific monster classes. The script binder extends that engine object with XRF runtime logic.

Actor sections

The actor config starts at [actor] and uses $scheme = $actor. It links the runtime binder through script_binding = bind.actor.

Actor-related sections cover:

  • movement and inventory limits, such as sprint, crouch, jump, and carried mass values;
  • physical settings, collision damage, bones, and corpse handling;
  • hit probabilities, immunities, condition sections, and hit sounds;
  • actor HUD linkage through player_hud_section;
  • default quick item slots.

Use the actor schema when adding new actor fields. If validation rejects a real engine field, update the schema instead of bypassing validation in the config.

Stalker sections

Stalker configs use $scheme = $stalker and describe NPC-level behavior visible to both the X-Ray engine and XRF runtime.

Common stalker fields include:

  • script_binding, usually bind.stalker;
  • community, rank, terrain, and movement sections;
  • visual and sound configuration;
  • perception settings such as view distance, visibility, and sound perception;
  • condition, damage, immunities, and material sections;
  • spawn metadata and editor fields.

Scenario logic is usually not placed directly in the creature section. Stalker behavior for a specific story object is normally driven by script configs, smart terrain jobs, and schemes.

Monster sections

Monster configs inherit from shared monster bases and then specialize by species. The base sections provide common movement, perception, damage, sound, and physical behavior. Species files tune attacks, locomotion, particles, sounds, protections, and spawn sections for each monster family.

Monster runtime behavior is attached through bind.monster. During spawn, the binder registers the object, initializes scheme logic with the monster scheme type, and later handles section switching, online/offline transitions, sound cleanup, and smart terrain participation.

When editing monster configs, compare the closest existing species file first. Many fields are species-specific engine parameters, and nearby sections are usually the best source for valid value shape.

Online/offline groups

$scheme/creatures.misc.scheme.ltx validates online_offline_group sections. These sections describe squad-style spawn groups and can include fields such as faction, NPC section lists, random NPC pools, target smart terrain, behavior, death conditions, and invulnerability.

Use these sections when the config needs to describe a squad or group that moves through simulation, rather than a single creature section.

Editing checklist

When changing creature configs:

  • keep the $scheme marker on sections that validation should check;
  • keep script_binding aligned with the engine class and intended runtime binder;
  • update referenced condition, immunities, sound, terrain, movement, and damage sections together;
  • check script configs and smart terrain jobs before assuming behavior belongs in the creature base section;
  • run LTX validation after schema or config changes:
npm run cli verify ltx

Zones and anomalies

Zone configs describe anomaly fields, anomaly mines, campfires, teleport and no-gravity zones, space restrictors, level changers, and composite anomaly zones that can spawn artifacts. Static zone sections live under src/engine/configs/zones; script-level zone objects live in src/engine/configs/objects/zone_objects.ltx.

Use this page when you need to decide whether a behavior belongs in a base zone section, a script object section, or a scenario-specific anomaly config.

Source files

SourcePurpose
zones/index.ltxIncludes all zone config files into the final game config tree.
zones/zone_base.ltxDefines shared zone defaults. XRF sets the base zone script binding to bind.anomaly_field.
zones/zone_field_*.ltxDefines acidic, psychic, radioactive, and thermal field zones.
zones/zone_mine_*.ltx and zones/zone_minefield.ltxDefines electric, gravitational, acidic, thermal, and minefield anomaly variants.
zones/zone_campfire.ltxDefines campfire zone sections.
zones/zone_teleport.ltx and zones/zone_nogravity.ltxDefines special movement or transition zones.
zones/zone_burningfuzz.ltx and zones/zone_fireball.ltxDefines additional anomaly families.
objects/zone_objects.ltxDefines script-level zone objects such as space_restrictor, anomal_zone, camp_zone, level_changer, and script_zone.
$scheme/zone.scheme.ltxValidates zone fields and base zone fields.
scripts/**/anomaly/*.ltxDefines scenario-specific composite anomaly configs read by AnomalyZoneBinder.

Runtime binding

Zone-related sections attach runtime behavior through script_binding.

BindingUsed by
bind.anomaly_fieldRegular anomaly field or mine sections based on zone_base.
bind.anomaly_zoneComposite anomal_zone script objects that coordinate fields, artifact layers, and respawn rules.
bind.restrictorspace_restrictor sections.
bind.campcamp_zone sections.
bind.campfireCampfire zone sections.
bind.level_changerLevel changer sections.
bind.arena_zoneArena script zones when the section is present.

The binding tells XRF which TypeScript binder should manage the object. The engine class still controls the underlying X-Ray object type, collision behavior, and native zone behavior.

Base zone sections

Most static anomaly sections inherit from [zone_base] or a specialized zone parent and use $scheme = $zone.

Common zone fields include:

  • particle and sound names for idle, entrance, hit, and blowout states;
  • hit settings such as hit type, hit impulse, and power values;
  • light, wind, postprocess, and visual shape settings;
  • artifact handling flags such as ignore_artefacts;
  • spawn metadata and editor fields.

Use inheritance for variants such as weak, average, and strong anomaly sections. Keep the shared behavior on the parent section and override only the values that differ.

Script zone objects

objects/zone_objects.ltx defines script-level objects that are not ordinary anomaly damage sections.

Important sections include:

  • [space_restrictor], managed by bind.restrictor;
  • [anomal_zone], managed by bind.anomaly_zone;
  • [camp_zone], managed by bind.camp;
  • [level_changer], managed by bind.level_changer;
  • [script_zone], used for arena-style script zones.

Choose these sections when the object is meant to drive script logic, travel, restrictions, camps, or composite anomaly behavior rather than a standalone damage field.

Composite anomaly zones

Composite anomaly zones are managed by AnomalyZoneBinder. The binder reads the spawned object’s anomal_zone section and may also read an additional config file from the cfg field.

The binder reads fields such as:

  • layers_count;
  • respawn_tries;
  • max_artefacts;
  • applying_force_xz and applying_force_y;
  • artefacts;
  • start_artefact;
  • artefact_ways;
  • field_name;
  • coeff;
  • coeffs_section.

Each layer_N section can override artifact counts, respawn tries, maximum artifacts, forces, artifact lists, starting artifacts, and waypoint lists for that layer.

Use composite anomaly configs when the gameplay object needs to coordinate several anomaly fields and artifact spawning rules. Use a normal zone section when you only need an individual field, mine, or damage source.

Editing checklist

When changing zones or anomalies:

  • keep script_binding aligned with the section’s runtime role;
  • do not mix regular field sections with anomal_zone composite configs;
  • keep $scheme = $zone on sections validated by zone.scheme.ltx;
  • verify referenced particles, sounds, postprocess names, artifact sections, and waypoint names;
  • check story links and map placement when changing level changers or restrictors;
  • run LTX validation after config or schema changes:
npm run cli verify ltx

Smart terrains and jobs

Smart terrain configs describe places where squads and individual objects can work. Job configs decide which object can take which logic section and with what priority.

The source files live mostly under src/engine/configs/scripts/**/smart plus shared smart terrain configs such as sim_smart_base.ltx and sim_smart_resource.ltx.

Smart terrain role

A smart terrain is a server-side simulation object. It owns jobs, tracks assigned objects, and participates in offline ALife simulation. When an assigned object goes online, its binder and scheme logic run on the client side.

Use smart terrain configs for simulation placement and job selection. Use scheme sections for the actual object behavior once the job is active.

Job fields

Common job fields include:

  • logic, pointing to the script config and section used by the object;
  • prior, defining selection priority;
  • suitable, checking whether the object can take the job;
  • active, selecting the active logic section;
  • path_walk and path_look, used by walker-style schemes;
  • on_info and other condlists for switching job state.

The exact fields depend on the scheme used by the job.

Editing workflow

When changing a smart terrain job:

  1. find the smart terrain section and job section;
  2. follow logic or active to the scheme section;
  3. check suitable and prior if the NPC does not select the job;
  4. check scheme implementation and parser support for any new field;
  5. validate LTX includes and schema.

Run:

npm run cli verify ltx

Treasures

Treasure configs describe hidden stashes and the rewards assigned to them. XRF reads treasure data through TreasureManager and related treasure utilities.

Use this page when you need to add a stash, grant stash coordinates from a quest, or check why a marked stash does not produce the expected reward.

Source files

Treasure manager source files live under:

  • src/engine/configs/managers/treasure_manager.ltx
  • src/engine/configs/managers/treasures/treasures_zaton.ltx
  • src/engine/configs/managers/treasures/treasures_jupiter.ltx
  • src/engine/configs/managers/treasures/treasures_pripyat.ltx

Script configs can also reference treasure inventory boxes, for example src/engine/configs/scripts/treasure_inventory_box.ltx.

Quest and restrictor scripts grant stash coordinates with the give_treasure effect. Examples exist in task configs and script logic, including:

  • src/engine/configs/managers/tasks/tasks_zaton.ltx;
  • src/engine/configs/managers/tasks/tasks_jupiter.ltx;
  • src/engine/configs/scripts/jupiter/jup_b43_task_giver_restrictor.ltx;
  • src/engine/configs/scripts/pripyat/pri_b36_sr_ahi_place_pda.ltx.

Runtime behavior

Treasure data is loaded by the treasure manager. It tracks which treasures are available, found, or already looted, and coordinates map spot display through map utilities.

The script effect xr_effects.give_treasure calls TreasureManager.giveActorTreasureCoordinates(...) for each passed treasure id. This grants coordinates; the treasure definition still controls the stash metadata and reward contents.

Physical treasure containers enter the manager through object binders. ObjectPhysic and ObjectHangingLamp call TreasureManager.registerItem(this) when they are constructed, so the manager can connect spawned world objects to treasure data.

When changing treasure behavior, check both:

  • the manager config that defines treasure metadata and rewards;
  • the script config or object section that represents the stash in the world.

Example grant

This pattern appears in Jupiter quest logic. It grants one treasure once and then records that the reward was already given:

[sr_idle@reward]
on_info = {+jup_b43_contract_brought_first_artefact -jup_b43_once_treasure_give_1} %=give_treasure(jup_hiding_place_5) +jup_b43_once_treasure_give_1%

Use an info portion guard when the same logic section can be evaluated more than once.

Editing checklist

  • Keep treasure ids stable once saves can reference them.
  • Keep reward sections valid and included.
  • Check map spot behavior when a treasure should appear on the PDA.
  • Check the world object or inventory box section that represents the stash.
  • Check quest scripts that grant the treasure with give_treasure.
  • Test TreasureManager or treasure utility code when changing runtime behavior.
  • Run npm run cli verify ltx after config edits.

Weapons

Weapon configs live under src/engine/configs/items/weapons. They define base weapon sections, HUD sections, sounds, ballistics, upgrade links, add-ons, ammo classes, and mounted weapons.

Source layout

Important files and folders:

  • base.ltx for shared weapon definitions;
  • index.ltx for includes;
  • w_*.ltx for individual weapon sections;
  • upgrades/*.ltx for weapon upgrade trees;
  • weapon_upgrades.ltx and upgrades_properties.ltx for shared upgrade data;
  • $scheme/weapons.scheme.ltx for validation coverage.

Validation schema

Weapon schemas are mostly strict. They model common sections such as:

  • $item_weapon;
  • $item_weapon_hud;
  • $item_weapon_sounds;
  • $item_weapon_attachable;
  • $item_weapon_params;
  • $item_weapon_grenade;
  • $item_weapon_knife;
  • $weapon_mounted.

If a valid weapon field fails validation, update the narrow matching schema instead of disabling strict validation for the whole weapon category.

Weapon configs reference assets and other config sections:

  • visual, item_visual, HUD positions, and bones;
  • sound aliases such as snd_shoot and snd_reload;
  • particle aliases such as flame_particles and shell_particles;
  • ammo sections through ammo_class;
  • upgrade sections through upgrades, installed_upgrades, and upgrade_scheme;
  • add-on sections for scopes, silencers, and grenade launchers.

Check all referenced sections and assets when adding a weapon variant.

Editing checklist

  • Compare against a nearby weapon with the same weapon class.
  • Keep HUD and world model sections separate.
  • Validate upgrade section names and include order.
  • Run npm run cli verify ltx.
  • Test script-side weapon utilities only when changing runtime TypeScript behavior.

Common failure points

  • Missing include order can make a weapon section valid in isolation but unavailable from the final item index.
  • HUD section names must stay aligned with the weapon section fields that reference them.
  • Ammo, scope, silencer, and grenade-launcher section names must exist before the weapon can use them.
  • Texture, model, sound, and particle references are not fixed by LTX formatting; verify the referenced resource files separately.

Weather

Weather configs define environment cycles, ambient sounds, fog, suns, thunderbolts, weather effects, and manager-level weather selection. Runtime weather behavior is handled by WeatherManager.

Source layout

Important source areas:

  • src/engine/configs/environment/environment.ltx;
  • src/engine/configs/environment/weathers/*.ltx;
  • src/engine/configs/environment/weather_effects/*.ltx;
  • src/engine/configs/environment/ambients/*.ltx;
  • src/engine/configs/environment/ambient_channels/*.ltx;
  • src/engine/configs/environment/fog/*.ltx;
  • src/engine/configs/environment/dynamic_weather_graphs.ltx;
  • src/engine/configs/managers/weather_manager.ltx;
  • src/engine/configs/managers/weather/weather_manager_levels.ltx.

Weather sections

The $weather schema is strict and includes fields for sky, fog, rain, sun, clouds, wind, water, ambient, thunderbolt, and sun shafts. A weather cycle is built from time sections that point the engine at these values.

Weather effect sections describe temporary events such as surge, blowout, and psi storm effects. They reference particles, sound, wind, and lifetime fields.

Runtime manager

WeatherManager subscribes to actor update events and actor online events. It applies configured weather and exposes debug information through the XRF debug panel.

On actor network spawn, the manager reads the current level’s weathers value from game.ltx. If no level-specific value is configured, it uses the AtmosFear-style dynamic weather section. The selected value is parsed as a condition list and becomes the source for later weather section selection.

During actor updates, the manager:

  • checks hourly changes and advances weather graph state;
  • changes good/bad weather periods when the configured period boundary is reached;
  • marks pre-blowout weather when a surge or weather FX is close;
  • updates DOF every five game seconds for active AtmosFear weather;
  • saves and loads weather section, period, graph state, and active weather FX data.

When changing weather selection logic, update manager tests. When changing only LTX values, validate the configs and check the result in game.

Validation

Run:

npm run cli verify ltx

Use the debugging weather page for in-game inspection and debug panel workflow.

Editing notes

  • Edit weather cycle values under environment/weathers when changing sky, fog, rain, sun, or ambient output.
  • Edit dynamic_weather_graphs.ltx when changing transitions between clear, cloudy, rainy, or related graph states.
  • Edit weather_manager_levels.ltx and game.ltx links when changing which weather set a level uses.
  • Use a save/load check after manager logic changes because weather state is serialized.

Effects and conditions

Effects and conditions are Lua externals called from config condlists. They connect LTX logic to XRF TypeScript behavior.

Effects are registered under xr_effects. Conditions are registered under xr_conditions.

Source layout

Source areaPurpose
src/engine/scripts/declarations/effectsEffect functions called from %...% condlist actions.
src/engine/scripts/declarations/conditionsBoolean condition functions called from {...} condlist checks.
src/engine/scripts/register/externals_registrator.tsLoads declaration modules and prevents duplicate registration.
src/engine/core/utils/binding.tsImplements extern(...) and nested global registration.
src/engine/core/utils/iniRuntime condlist parsing and execution.

Config names

Configs call short names:

on_info = {=actor_has_item(af_oasis_heart)} %=give_inited_task(jup_b16_task)%

The registered globals include the namespace:

  • actor_has_item resolves to xr_conditions.actor_has_item;
  • give_inited_task resolves to xr_effects.give_inited_task.

Search both names before changing an effect or condition.

Function shape

Effect and condition declarations commonly receive the actor object, the current object, and a parameter array parsed from the condlist:

%=play_sound(story_sound_id)%
{=dist_to_actor_le(30)}

Parameters in configs are colon-separated. Keep parsing simple and update parser tests before adding syntax that needs nested values or escaping.

Side effects

Conditions should answer a question. Effects may mutate game state: give or remove info portions, start tasks, play sounds, set weather, spawn objects, save the game, or switch object state.

pickSectionFromCondList can run effects while choosing a section. A field that looks like a value read may still change state if its matching condlist entry contains %...%.

Testing

Effect and condition files have focused Jest tests beside the declarations. When changing behavior, update the matching test:

npm test -- src/engine/scripts/declarations/effects
npm test -- src/engine/scripts/declarations/conditions

For config changes that call the function, also run:

npm run cli verify ltx

Forms

Forms are UI XML sources consumed by engine CUI classes. XRF keeps most form source in TSX under src/engine/forms and builds it into XML.

Runtime UI classes live under src/engine/core/ui. They load XML, initialize controls, register callbacks, and update the UI at runtime.

Source types

Source typeBuild behavior
src/engine/forms/**/*.tsxImported by the UI build and rendered to .xml.
src/engine/forms/**/*.tsImported when it exports a valid create() form source.
src/engine/forms/**/*.xmlCopied as static UI XML.
src/engine/forms/textures_descr/*.xmlTexture atlas metadata copied as UI XML.

Dynamic forms must export create(). The UI build calls it and writes the result through renderJsxToXmlText.

Components

Shared JSX components live under src/engine/forms/components. Common base components include:

  • XrRoot;
  • XrElement;
  • XrStatic;
  • XrText;
  • Xr3tButton;
  • XrCheckBox;
  • XrEditBox;
  • XrScrollView;
  • XrTab;
  • XrTexture.

Prefer these helpers over manually assembling repeated XML structures.

Runtime loading

Runtime classes use engine UI helpers such as CScriptXmlInit, CUIScriptWnd, CUIStatic, CUI3tButton, CUIListBox, and related CUI bindings.

When changing a form:

  1. find the runtime class that loads it;
  2. keep XML node names stable unless the runtime lookup is updated;
  3. check paired 16:9 variants such as name.tsx and name_16.tsx;
  4. update tests for runtime UI classes when element names or callbacks change.

Validation

Run a focused UI build after form changes:

npm run cli build -- --filter ui

Do not edit generated XML under target/.

Debugging workflow

If a control does not appear or a callback does not fire, check the runtime class before changing the form. Most UI classes look up controls by XML node name, so a renamed node can break runtime initialization even when the XML builds.

For layout issues, compare the generated XML with the TSX source and any paired widescreen variant. For behavior issues, inspect the src/engine/core/ui class that loads the form and binds callbacks.

Patrols

Patrol paths are level-authored waypoints used by stalker schemes, monster movement, smart-cover targets, travel, spawn helpers, and simulation utilities.

For stalker logic, XRF routes most waypoint behavior through StalkerPatrolManager. Schemes such as walker, sleeper, patrol, and reach_task configure the manager with path_walk, optional path_look, team synchronization, suggested states, and waypoint callbacks.

path_walk

path_walk is the movement path. The scheme reads it from config, verifies the patrol path exists, parses waypoint metadata, and sends the object along the path.

Supported waypoint flags include:

FlagMeaning
a=stateUse a state condlist or state value while moving to or through the waypoint.
p=percentStop probability at the waypoint. If omitted, the manager uses the normal look-path behavior.
sig=nameSet an active scheme signal when the walk waypoint is reached.
ret=valuePass a numeric return value to a registered patrol callback before animation turn handling.

If no sig is provided on the last walk waypoint, the manager emits path_end.

path_look

path_look is an optional look/idle path paired with path_walk. The engine uses waypoint flags to choose a matching look point for a reached walk point.

Supported look flags include:

FlagMeaning
a=stateUse a state condlist or state value while standing and looking.
t=msecWait time. * means no timeout. Numeric values must be 0 or in the accepted millisecond range.
sig=nameSet a signal after turning to the look point. Defaults to turn_end when no signal is provided.
synWait for the patrol team before emitting the signal. Requires sig.
sigtm=nameSet a signal when the animation-time callback fires.
ret=valuePass a numeric return value to a registered patrol callback after turning.

syn is intended for terminal coordination. XRF asserts when it is used on a non-terminal waypoint.

Example

Button-style interaction that plays a press state, then switches when the timed animation signal is emitted:

path_look waypoint flags: a=press|t=0|sigtm=pressed
logic field: on_signal = pressed | next_scheme@section

The exact waypoint flag syntax is stored in level patrol data, not in the LTX file. LTX sections reference the patrol path names through fields such as path_walk and path_look.

Debugging

If a patrol does not work:

  • verify level.patrol_path_exists(path_name) would pass for path_walk and path_look;
  • check that path_look is not the same path as path_walk;
  • check waypoint flags when look points are not selected;
  • check on_signal when the movement reaches a point but the scheme does not switch;
  • use AI debug overlays and object dumps from the debug panel when the active state is unclear.

UI elements

UI elements are engine CUI bindings exposed to Lua. XRF uses them from runtime UI classes and initializes most controls from XML generated by src/engine/forms.

Use the XRF X-Ray 16 SDK as the API reference for exact methods. This page gives the practical map of common classes.

XML initialization

CScriptXmlInit loads XML and creates controls from node selectors.

Common methods include:

  • ParseFile(path);
  • ParseShTexInfo(path);
  • InitWindow(selector, index, window);
  • InitStatic(selector, parent);
  • InitTextWnd(selector, parent);
  • Init3tButton(selector, parent);
  • InitCheck(selector, parent);
  • InitComboBox(selector, parent);
  • InitEditBox(selector, parent);
  • InitListBox(selector, parent);
  • InitScrollView(selector, parent);
  • InitTab(selector, parent);
  • InitTrackBar(selector, parent).

Call ParseFile() before using Init* helpers. Controls created with a parent are attached to that parent by the engine binding.

Window base classes

ClassUse
CUIWindowBase window rectangle, visibility, enable state, child attachment, and positioning.
CUIDialogWndDialog window that can be shown, hidden, and attached to a dialog holder.
CUIScriptWndScript-driven dialog window with callbacks, keyboard handling, child registration, and typed lookup helpers.

Use CUIScriptWnd for custom script windows that need callbacks or registered child controls.

Static and text controls

ClassUse
CUIStaticStatic image, texture, animation, or simple visual element.
CUITextWndText window with text color, alignment, string-table text, and sizing helpers.
CUILinesText lines object used by text-capable controls.
CUISleepStaticSleep/static overlay variant used by engine UI.

Use SetTextST(...) when the text should come from translations.

Buttons and inputs

ClassUse
CUIButtonBase button class.
CUI3tButtonCommon three-state button used by menus and dialogs.
CUICheckButtonCheckbox-style button with checked state.
CUICustomEditBase edit control with text and focus capture.
CUIEditBoxEdit box with texture initialization.
CUICustomSpinBase spin control.
CUISpinFltFloat spin control.
CUISpinNumInteger spin control.
CUISpinTextText spin control.
CUITrackBarSlider/track bar with integer or float values.

For settings screens, prefer the existing option window patterns under src/engine/core/ui/menu/options.

Lists and tabs

ClassUse
CUIListBoxScrollable list box with list-box items.
CUIListBoxItemItem for CUIListBox.
CUIListBoxItemMsgChainMessage-chain list item variant.
CUIListWndEngine list window that owns added list items.
CUIListItemGeneric list item.
CUIScrollViewScroll view container.
CUITabControlTab container with id and index activation.
CUITabButtonButton used by tab controls.

Ids and visual indices are not the same for combo boxes, tabs, and lists. Use ids for stable logic and indices for visual order.

Frames, maps, and message windows

ClassUse
CUIFrameWindowFramed panel.
CUIFrameLineWndRepeating frame line.
CUIComboBoxDropdown list with item ids.
CUIMessageBoxMessage box static variant.
CUIMessageBoxExDialog-window message box variant.
CUIProgressBarProgress indicator.
CUIPropertiesBoxContext/properties menu.
CUIMapInfoMap metadata control.
CUIMapListMultiplayer map list control.
CUIMMShniagaMain-menu animated/menu control.
CServerListMultiplayer server list control.

Lifetime and callbacks

Parent-owned controls are adopted by the engine UI tree. After attaching a child or adding an item to a list, treat the parent/list as owning its lifetime.

For CUIScriptWnd, register child controls before adding callbacks or using typed lookup helpers. Callback names depend on the registered window name, so keep XML node names and registration names aligned.

Runtime lifecycle

The runtime lifecycle is the path from X-Ray loading Lua scripts to XRF managers, binders, schemes, and server objects owning live gameplay state.

Read this section before changing object registration, manager startup, save/load, online/offline transitions, or event emission order.

Startup flow

The main game-start callback is start.callback(isNewGame) in src/engine/scripts/start.ts. It runs for both new games and loaded games.

Startup order:

  1. update cached class ids from classIds;
  2. register the ALife simulator;
  3. register rank descriptors;
  4. unlock system ini overriding;
  5. register managers;
  6. register schemes;
  7. register extensions;
  8. emit GAME_STARTED.

Managers, schemes, and extensions should not depend on object binders already being online during this callback. Online objects arrive later through binder calls from the engine.

Object flow

The usual runtime path is:

  1. X-Ray loads script entry modules such as register, bind, and start.
  2. start.callback initializes shared runtime systems.
  3. X-Ray creates an online game object and calls a function from the bind extern module.
  4. The binder attaches a TypeScript object_binder subclass to the game object.
  5. net_spawn registers the object in the runtime registry and sets up callbacks.
  6. update runs per-frame or throttled behavior.
  7. net_destroy unregisters callbacks and registry state when the object goes offline.
  8. save and load persist binder and scheme state when the object is save-relevant.

Server-side objects use related ALife callbacks such as on_register, on_unregister, STATE_Write, and STATE_Read.

Runtime owners

OwnerSourceOwns
Entry modulessrc/engine/scriptsEngine-facing extern names and startup callbacks.
Binderssrc/engine/core/bindersClient object lifecycle and object-local glue.
Managerssrc/engine/core/managersCross-object systems and singleton runtime state.
Registrysrc/engine/core/databaseShared runtime tables and focused helper APIs.
Schemessrc/engine/core/schemesObject logic sections driven by configs.
Server objectssrc/engine/core/objectsALife-side registration, simulation, and save data.
Eventssrc/engine/core/managers/eventsInternal publish-subscribe events and game timers.

Keep state near the owner that controls its lifecycle. A binder should not become the permanent owner of a cross-object system. A manager should not store short-lived object state that belongs in registry.objects.

First places to check

  • For startup order, read src/engine/scripts/start.ts.
  • For binder factories, read src/engine/scripts/bind.ts.
  • For manager startup, read src/engine/scripts/register/managers_registrator.ts.
  • For manager registry helpers, read src/engine/core/database/managers.ts.
  • For shared state, read src/engine/core/database/registry.ts.
  • For save/load coordination, read src/engine/core/managers/save/SaveManager.ts.

Editing checklist

  • Identify whether the change belongs to a binder, manager, registry helper, scheme, or server object.
  • Check both online and offline paths.
  • Check save and load paths if state must survive reload.
  • Preserve event order unless the task is specifically about event behavior.
  • Add focused tests near the lifecycle owner.

Runtime binders

Binders attach TypeScript lifecycle code to online X-Ray game objects. They are registered through the bind extern module in src/engine/scripts/bind.ts.

Use binders for client-side lifecycle glue: registering an object, installing engine callbacks, initializing logic, and cleaning up when the object goes offline.

Binder families

FamilyBinder functions
Creaturesactor, stalker, monster, crow
Zonesrestrictor, anomaly_zone, anomaly_field, camp, arena_zone, level_changer
Physicalphysic_object, door, campfire, artefact, phantom, signal_light
Itemsweapon, helmet, outfit
Smart systemssmart_terrain, smart_cover
Helicopterhelicopter

Some binder factories are conditional:

  • arena_zone binds only when the spawn ini contains arena_zone;
  • helicopter binds only when the spawn ini contains logic;
  • physic_object binds only when the object has logic or is an inventory box;
  • smart_terrain binds only when the spawn ini contains smart_terrain.

Common lifecycle methods

Most binders implement some subset of:

  • reinit: reset local and registry state;
  • net_spawn: object came online;
  • update: object update tick;
  • net_destroy: object went offline;
  • net_save_relevant: whether binder state should be saved;
  • save: write binder and object logic state;
  • load: read binder and object logic state.

Use the same read/write order in save and load. When a binder persists object logic, it usually wraps the operation in save markers and calls saveObjectLogic / loadObjectLogic.

Actor binder

ActorBinder is the global update driver for many runtime systems.

On online switch it:

  • shows indicators;
  • registers actor references;
  • initializes actor portable store;
  • emits ACTOR_GO_ONLINE.

On reinit it registers the actor again, resets portable store, installs actor callbacks, enables unlimited ALife update for the initial spawn buffer, schedules stable ALife updates, and emits ACTOR_REINIT.

On update it emits ACTOR_UPDATE, throttled actor update events, processes EventsManager timers, and updates actor simulation availability.

Stalker binder

StalkerBinder owns online stalker setup. It creates the stalker state manager and patrol manager, sets up planners, registers the stalker in the registry, installs callbacks, initializes sound themes, initializes object logic, and sets up post-combat idle behavior.

On offline switch it stops sounds, emits scheme offline events, applies on_offline overrides, stores offline state, and unregisters the stalker.

Restrictor binder

RestrictorBinder is a compact example for zone lifecycle:

  • reinit resets object registry state;
  • net_spawn registers the zone and starts looped sounds;
  • first update initializes restrictor scheme logic;
  • later update tracks visited state, emits scheme updates, and updates sounds;
  • net_destroy emits scheme offline behavior, stops sounds, and unregisters the zone;
  • save and load persist object logic and visited state.

Guidelines

  • Keep binders focused on object lifecycle.
  • Use managers for cross-object systems.
  • Use registry helpers instead of mutating registry tables directly.
  • Reset engine callbacks when an object goes offline.
  • Check net_save_relevant before assuming a binder’s save/load methods are used.

Runtime managers

Managers are singleton runtime services stored in the registry. They own cross-object systems such as events, save/load, sound, simulation, trade, tasks, weather, upgrades, UI state, and debugging.

Managers extend AbstractManager from src/engine/core/managers/abstract/AbstractManager.ts.

Manager access

Use the registry helper that matches the lifecycle you need:

getManager(SoundManager);
getWeakManager(SoundManager);
getManagerByName("SoundManager");

getManager(ManagerClass) is the normal path. It returns the existing singleton or initializes one.

getWeakManager(ManagerClass) returns null if the manager is not initialized.

getManagerByName(name) is mainly for circular-reference cases where the class reference is not available. It cannot initialize a missing manager.

Startup managers

registerManagers() initializes the startup manager list during start.callback.

The current startup list is:

  • ActorInputManager;
  • ActorInventoryMenuManager;
  • DatabaseManager;
  • DebugManager;
  • DialogManager;
  • EventsManager;
  • GameSettingsManager;
  • LoadScreenManager;
  • LoadoutManager;
  • MapDisplayManager;
  • MusicManager;
  • NotificationManager;
  • PdaManager;
  • PhantomManager;
  • ReleaseBodyManager;
  • SaveManager;
  • SimulationManager;
  • SleepManager;
  • SoundManager;
  • StatisticsManager;
  • TaskManager;
  • TradeManager;
  • TravelManager;
  • TreasureManager;
  • UpgradesManager;
  • WeatherManager.

Other managers can still be initialized lazily with getManager. For example, SaveManager initializes SurgeManager after ACTOR_REINIT.

Lifecycle methods

AbstractManager defines:

  • initialize();
  • destroy();
  • update(delta);
  • save(packet);
  • load(reader).

The base update, save, and load methods abort. Implement only the methods the manager actually supports.

disposeManager calls destroy(), marks the manager as destroyed, and removes it from both registry maps.

Common patterns

Managers that listen to events usually subscribe in initialize() and unsubscribe in destroy().

Managers with persistent state write to net packets through SaveManager or to dynamic save data through helpers. Keep save and load order synchronized.

Managers with delayed work should check isDestroyed before doing work after disposal.

Where to add behavior

  • Use a manager for shared behavior across many objects.
  • Use a binder when the behavior belongs to one online object.
  • Use a scheme manager when the behavior belongs to one config scheme.
  • Use a database helper when the behavior is narrow registry access.

Do not construct managers directly in runtime code. Use getManager unless a test is intentionally isolating a manager class.

Runtime events and timers

EventsManager is XRF’s internal publish-subscribe layer for runtime lifecycle changes. It also owns global-time intervals and timeouts through AbstractTimersManager.

Use events when a binder, manager, scheme, or server object needs to announce a lifecycle change without directly depending on every listener.

Event dispatch

Events are declared in EGameEvent under src/engine/core/managers/events/events_types.ts.

Register callbacks through the manager:

getManager(EventsManager).registerCallback(EGameEvent.ACTOR_UPDATE, this.onActorUpdate, this);

Emit events through the manager or the static helper:

EventsManager.emitEvent(EGameEvent.GAME_STARTED, isNewGame);

Callbacks can be registered with a context. When context is provided, the callback is called with that context.

Event groups

EGameEvent covers:

  • actor registration, online/offline, reinit, death, item, trade, sleep, and update ticks;
  • stalker and monster registration, hit, death, and interaction;
  • helicopter, squad, smart terrain, smart cover, zone, and item lifecycle;
  • task, treasure, surge, notification, hit, and UI menu events;
  • save/load and level-change events;
  • debug dump requests.

Prefer adding a specific event over overloading an unrelated existing one. Listeners should be able to infer why they were called from the event name.

Timers

EventsManager extends AbstractTimersManager.

Use:

const [cancel] = EventsManager.registerGameTimeout(callback, 1000);
const [stop] = EventsManager.registerGameInterval(callback, 500);

Intervals assert that the period is at least 50 milliseconds. Both intervals and timeouts receive the actual elapsed offset when they run.

Timers are processed by ActorBinder.update() through eventsManager.tick(). They advance on actor updates, not as independent operating-system timers.

Cleanup

Unregister callbacks in the lifecycle owner that registered them:

  • managers should unregister in destroy();
  • binders should reset object callbacks in offline cleanup;
  • one-shot timers remove themselves after running;
  • long-lived intervals should keep and call the cancel function when the owner is destroyed.

High-frequency events such as ACTOR_UPDATE should stay light. Use throttled actor update events or an interval when work does not need to run every actor tick.

Runtime save and load

XRF has two save paths:

  • engine net-packet save/load for binders, server objects, and selected managers;
  • dynamic save data stored beside the game save through marshal-backed files.

Use net-packet save/load for compact state that is part of the engine lifecycle. Use dynamic save data for flexible extension or event state that should not be constrained by packet layout.

Save manager

SaveManager coordinates core manager save/load and engine save callbacks.

Client manager state is saved and loaded through:

  • WeatherManager;
  • ReleaseBodyManager;
  • SurgeManager;
  • PsyAntennaManager;
  • SoundManager;
  • StatisticsManager;
  • TreasureManager;
  • TaskManager;
  • ActorInputManager;
  • GameSettingsManager;
  • DeimosManager.

Server manager state currently goes through SimulationManager.

SaveManager also handles alife_storage_manager callbacks exposed from src/engine/scripts/declarations/callbacks/game.ts.

Dynamic save data

Before a game save, SaveManager.onBeforeGameSave(saveName):

  1. emits GAME_SAVE;
  2. saves extension state;
  3. writes registry.dynamicData with saveDynamicGameSave.

When loading starts, SaveManager.onGameLoad(saveName):

  1. loads registry.dynamicData with loadDynamicGameSave;
  2. loads extension state;
  3. emits GAME_LOAD.

After the engine reports successful load, SaveManager.onAfterGameLoad(saveName) emits GAME_LOADED.

Binder save and load

Save-relevant binders write their own state from save(packet) and read it in load(reader).

Common binder pattern:

openSaveMarker(packet, BinderClass.__name);
super.save(packet);
saveObjectLogic(this.object, packet);
closeSaveMarker(packet, BinderClass.__name);

The load path must read fields in the same order:

openLoadMarker(reader, BinderClass.__name);
super.load(reader);
loadObjectLogic(this.object, reader);
closeLoadMarker(reader, BinderClass.__name);

saveObjectLogic persists logic file names, active section, smart terrain name, activation time, active scheme save event, and portable store data. loadObjectLogic restores the matching loaded fields and portable store.

Save markers

Save markers protect net-packet layout.

  • openSaveMarker records the current packet offset.
  • closeSaveMarker writes the saved block size.
  • openLoadMarker records the current reader offset.
  • closeLoadMarker checks that the loaded block size matches the saved block size.

The marker helpers assert when the read/write sizes drift. If a save format changes, update save and load together and adjust tests for the saved data list.

Guidelines

  • Keep net-packet data compact.
  • Never reorder saved fields without updating the load path.
  • Save manager state through SaveManager when it is part of global runtime state.
  • Save object logic through binder save/load when it belongs to one online object.
  • Use dynamic save data for extension data or flexible event state.

Schemes

A scheme is the behavior selected by an object’s logic configuration. [logic] active names the initial section; the part before @ selects the implementation.

[logic]
active = walker@guard

[walker@guard]
path_walk = guard_walk
path_look = guard_look
on_info = {+alarm_started} walker@alarm

[walker@alarm]
path_walk = alarm_walk
def_state_moving = run

The scheme name is the part before the suffix. For example, walker@guard uses the walker implementation, and sr_idle@wait_for_actor uses the sr_idle implementation.

Digits are ignored while resolving a scheme name, so numbered variants share the same implementation.

Switching sections

Active sections can define switch rules. The engine evaluates rule groups in this fixed order: actor distance, signals, info portions, real-time timers, game-time timers, actor-zone rules, then NPC-zone rules. Within a group, rules follow their order in the section. Numbered variants such as on_info1 add another rule of the same type.

on_info, on_info1, …

Value shape: condlist.

When the condlist picks a target section.

on_signal, on_signal1, …

Value shape: signal | condlist.

When a waypoint or manager sets the named signal.

on_timer, on_timer1, …

Value shape: milliseconds | condlist.

After the section has been active for the given real-time duration.

on_game_timer, on_game_timer1, …

Value shape: seconds | condlist.

After the section has been active for the given game-time duration.

on_actor_inside

Value shape: condlist.

When the actor is inside the current restrictor object.

on_actor_outside

Value shape: condlist.

When the actor is outside the current restrictor object.

on_actor_in_zone

Value shape: zone | condlist.

When the actor is inside the named zone.

on_actor_not_in_zone

Value shape: zone | condlist.

When the actor is outside the named zone.

on_npc_in_zone

Value shape: story_id | zone | condlist.

When the NPC resolved by story id is inside the named zone.

on_npc_not_in_zone

Value shape: story_id | zone | condlist.

When the NPC resolved by story id is outside the named zone.

on_actor_dist_le

Value shape: distance | condlist.

When the object sees the actor and actor distance is less than or equal to the value.

on_actor_dist_le_nvis

Value shape: distance | condlist.

Same distance check, without requiring actor visibility.

on_actor_dist_ge

Value shape: distance | condlist.

When the object sees the actor and actor distance is greater than the value.

on_actor_dist_ge_nvis

Value shape: distance | condlist.

Same distance check, without requiring actor visibility.

A condlist can also set info portions or run effects while selecting the next section:

on_info = {+actor_has_key} ph_door@open %=play_sound(door_unlock)%

An empty target, nil, or the current section does not switch. Timer baselines reset after a successful switch.

Scheme families

  • Stalker: NPC movement, animations, combat, and interaction (walker, remark, animpoint, smartcover).
  • Monster: monster movement, territory, animations, and combat (mob_walker, mob_home, mob_remark, mob_combat).
  • Restrictor: zone triggers, timers, visual effects, and actor events (sr_idle, sr_timer, sr_teleport, sr_particle).
  • Physical: usable and reactive world objects (ph_idle, ph_button, ph_door, ph_code, ph_on_hit).
  • Helicopter: scripted flight and weapons (heli_move).
  • Generic: behavior attached alongside an active scheme (combat, danger, death, hit, meet, post_combat_idle, wounded).

Patrol names

Several schemes read patrol path fields such as path_walk and path_look. When an object is running under a smart terrain, relative path names are resolved against the smart terrain name. For example, path_walk = guard_walk in smart terrain zat_b40_smart_terrain resolves to zat_b40_smart_terrain_guard_walk.

Use full path names when the path does not belong to the active smart terrain.

Before testing a section

  • Supply every required field for the selected scheme.
  • Keep path_walk and path_look distinct where both are used.
  • Use sr_idle for a state that only waits for switch rules.

abuse

abuse is a generic stalker scheme that makes an NPC react when the actor abuses it repeatedly. The current action reaction is a punch animation aimed at the actor.

Parameters

abuse has no scheme-specific LTX fields in the current TypeScript implementation.

Runtime values are stored in AbuseManager:

isEnabled

Default: true.

Enables or disables abuse accumulation.

abuseRate

Default: 2.

Multiplier used when abuse is added.

abuseThreshold

Default: 5.

Threshold at which the evaluator reports abuse.

abuseValue

Default: 0.

Current accumulated abuse value. It decays over time.

Behavior

The scheme adds an IS_ABUSED evaluator and an abuse action to the stalker planner. The action can run only while the NPC is alive, not in danger, and not wounded. When selected, it clears desired position and direction and sets the NPC state to punch, looking at the actor.

AbuseManager.update() decays accumulated abuse over time, clamps it near the threshold, and returns whether the value is currently above the threshold.

Example

[logic]
active = walker@idle

[walker@idle]
path_walk = guard_walk
path_look = guard_look

abuse is installed as a generic stalker scheme. It is not normally selected as the active section in [logic].

Notes

  • The public manager API exposes addAbuse, clearAbuse, enableAbuse, disableAbuse, and setAbuseRate.
  • The page documents the current engine behavior. It does not define a separate config field for changing the abuse threshold or rate from LTX.

animpoint

animpoint moves a stalker to a registered smart cover point and plays an idle animation there. Use it for traders, quest NPCs, camp idles, and fixed scene poses where the NPC should stand, sit, or perform an ambient animation at a known point.

The scheme uses a smart cover record as its anchor. The cover position gives the animation position, and the cover angle gives the look direction.

Parameters

cover_name

Type: string. Optional. Default: $script_id$_cover.

Registered smart cover name used as the animation anchor.

use_camp

Type: boolean. Optional. Default: true.

Allows camp manager integration when the animpoint position is inside a camp zone.

reach_movement

Type: stalker state. Optional. Default: walk.

Movement state used while walking to the animpoint.

reach_distance

Type: number. Optional. Default: 0.75.

Distance threshold for reaching the animpoint. The engine stores it as squared distance.

avail_animations

Type: comma-separated strings. Optional. Default: null.

Explicit animation states to choose from. When absent, animations are selected from predicates for the smart cover description.

The section also supports common switch fields such as on_info, on_timer, and on_signal.

Behavior

Use animpoint when the map has a smart cover that represents the desired pose location. The smart cover must be registered before the scheme starts, otherwise activation aborts when the manager calculates the position.

If avail_animations is not set, the engine uses the smart cover description to find compatible animation predicates. If the description has no registered predicate list, avail_animations is required.

With use_camp = true, the animpoint can register with a camp manager. Camp roles can choose director or listener animations from the approved action list.

Example

[logic]
active = animpoint@trader

[animpoint@trader]
cover_name = zat_trader_cover
use_camp = false
reach_movement = walk
reach_distance = 1.0
avail_animations = wait, wait_trade
on_info = {+zat_trader_alarm} walker@alarm

Notes

  • The smart cover named by cover_name must exist in the smart cover registry.
  • avail_animations is parsed as a comma-separated list.
  • The planner uses one action to reach the point and another action to play the selected animation.
  • The scheme is interrupted by enemy, anomaly, wounded, abuse, corpse, item, and meet states through common planner preconditions.

camper

camper makes a stalker hold a combat position, scan look points, and fire from cover. Use it for ambushes, snipers, defensive posts, and scripted combat positions.

The scheme owns a combat-camping planner action. It can block regular ALife, item gathering, corpse search, and wounded helping until close-combat camping is finished.

Parameters

path_walk

Type: string. Required. Default: none.

Patrol path used for movement between camp points. Relative names are resolved against the active smart terrain.

path_look

Type: string. Required. Default: none.

Patrol path used for look and scan points. It must not equal path_walk.

sniper

Type: boolean. Optional. Default: false.

Enables sniper scan behavior and sniper update rate.

no_retreat

Type: boolean. Optional. Default: false.

Stored in scheme state. Invalid together with sniper = true.

shoot

Type: always, none, or terminal. Optional. Default: always.

Controls when the NPC may fire at the visible enemy.

sniper_anim

Type: stalker state. Optional. Default: hide_na.

Sniper animation state stored by the scheme.

radius

Type: number. Optional. Default: 20.

Close-combat radius used by the close-combat evaluator.

def_state_moving

Type: stalker state. Optional. Default: null.

Suggested movement state.

def_state_moving_fire

Type: stalker state. Optional. Default: null.

Suggested movement-with-fire state.

def_state_campering

Type: stalker state. Optional. Default: null.

Suggested cover/scanning state.

def_state_standing

Type: stalker state. Optional. Default: def_state_campering.

Suggested standing state.

def_state_campering_fire

Type: stalker state. Optional. Default: null.

Suggested cover firing state.

scantime_free

Type: number. Optional. Default: 60000.

Time to keep scanning without enemy contact before resuming patrol movement.

attack_sound

Type: string or false. Optional. Default: fight_attack.

Sound played when firing. false disables it.

enemy_idle

Type: number. Optional. Default: 60000.

Enemy memory timeout before the action stops treating the remembered enemy as active.

The section also supports common switch fields such as on_info, on_timer, and on_signal.

Firing

always

Fire whenever the enemy is visible and the action can shoot.

none

Never fire from this camper action.

terminal

Fire only from the terminal waypoint of path_walk.

Any other value aborts with a config error.

Sniper mode

With sniper = true, the action builds a scan table from flags on path_look, enables the object’s sniper update rate, and scans look points while the NPC is on a camp patrol walk point.

sniper = true cannot be combined with no_retreat = true.

Example

[logic]
active = camper@ambush

[camper@ambush]
path_walk = ambush_walk
path_look = ambush_look
sniper = true
shoot = terminal
def_state_campering = hide_na
def_state_campering_fire = hide_sniper_fire
attack_sound = fight_attack
on_info = {+ambush_done} walker@after_ambush

Notes

  • path_walk and path_look are both required.
  • path_look cannot be the same as path_walk.
  • Danger handling can temporarily override scanning with danger-facing states.
  • The implementation uses fixed internal scan constants for enemy dispersion, scan delta, and scan time delta.

combat

combat configures scripted combat style for stalker NPCs. It selects a combat type through a condlist and installs the planner hooks used by camper and zombied combat helpers.

Parameters

combat_type

Type: condlist. Optional. Default: null.

Resolves to a scripted combat type. Supported enum values in the TypeScript source are camper, zombied, and monolith.

The section also supports common switch fields. They are parsed into state.logic, although the main combat behavior is driven through planner evaluators and actions.

Behavior

On activation, the scheme:

  1. marks combat overrides as enabled;
  2. reads combat_type;
  3. defaults zombied-community NPCs to combat_type = zombied when no field is provided;
  4. resolves the selected combat type with pickSectionFromCondList;
  5. stores it on the object registry state as scriptCombatType.

The add method registers IS_SCRIPTED_COMBAT, changes the base combat action precondition, and installs helper actions for combat_camper and combat_zombied.

Example

[logic]
active = walker@guard

[walker@guard]
path_walk = guard_walk
path_look = guard_look
combat_type = {+ambush_started} camper, nil

Notes

  • combat is usually combined with another active movement or state scheme. It changes combat planner behavior rather than replacing movement by itself.
  • The monolith value exists in the enum, but the inspected implementation only wires specific helper actions for camper and zombied combat.
  • Use the combat_camper and combat_zombied pages when debugging the helper actions installed by this scheme.

combat_camper

combat_camper is an internal helper installed by the combat scheme. It makes an NPC hide, remember the last seen enemy position, and shoot only when the scripted combat type is camper.

Parameters

combat_camper has no standalone LTX fields in the current TypeScript implementation.

Enable it through combat_type on a section handled by combat:

[walker@ambush]
path_walk = ambush_walk
path_look = ambush_look
combat_type = camper

Behavior

The helper adds two evaluators:

IS_COMBAT_CAMPING_ENABLED

Returns true when the object registry scriptCombatType is camper.

SEE_BEST_ENEMY

Returns true when the NPC sees its best enemy and stores the enemy position.

It also adds two actions:

SHOOT

Sets stalker state to hide_fire and looks at the best enemy.

LOOK_AROUND

Sets stalker state to hide, looks near the last seen enemy position, and periodically changes search direction.

Where it is installed

combat_camper is installed by SchemeCombat. It relies on registry.objects[object.id()].scriptCombatType being camper, which is set from combat_type = camper on the active combat-capable section.

Use it for ambush NPCs that should hold a position, hide, and shoot when they see the best enemy. Do not use it as a movement scheme; path and base activity still come from the section that activates combat behavior.

Notes

  • LOOK_AROUND forgets the last seen position after 30_000 ms.
  • The search direction changes after 10_000 ms initially, then every random 2_000 to 4_000 ms.
  • Hit callbacks while looking around can refresh the last seen enemy position if the hit came from the best enemy.
  • If an NPC never enters camper combat, check the active section’s combat_type and whether normal combat activation ran for the object.

combat_ignore

combat_ignore controls whether a stalker should accept a potential enemy while scripted logic is active. It is used for base behavior and quest scenes where combat can be ignored until conditions change.

Parameters

combat_ignore has no scheme-specific section fields in SchemeCombatIgnore.

The runtime state can receive logic overrides from the object registry. In particular, combatIgnoreKeepWhenAttacked keeps combat ignore enabled when the actor hits the NPC.

Behavior

On reset, the scheme:

  1. installs an enemy callback on the object;
  2. subscribes CombatProcessEnemyManager;
  3. enables the scheme state;
  4. copies current logic overrides into the scheme state.

The enemy callback asks canObjectSelectAsEnemy. If an enemy can be selected and the NPC is assigned to a smart terrain, the manager starts the smart terrain alarm. If the actor attacked a controlled smart terrain, the terrain control is notified.

When the NPC is hit by the actor, the manager disables combat ignore unless overrides keep it active.

Example

[logic]
active = walker@base
combat_ignore = true

[walker@base]
path_walk = base_walk
path_look = base_look

Notes

  • The exact combat_ignore override syntax is parsed outside SchemeCombatIgnore; this page documents the scheme implementation that consumes the resolved override state.
  • Enemy selection is rejected when the source and enemy are farther apart than the configured attack distance.
  • disable clears the enemy callback and unsubscribes the stored manager action.

combat_zombied

combat_zombied is an internal helper installed by the combat scheme. It provides simplified combat actions for zombied-community stalkers: advance toward the enemy, shoot, and move toward danger sources.

Parameters

combat_zombied has no standalone LTX fields in the current TypeScript implementation.

Zombied-community NPCs receive combat_type = zombied by default when combat is activated without an explicit combat_type.

Behavior

The helper adds IS_COMBAT_ZOMBIED_ENABLED. The current evaluator returns true when the object’s community is zombied.

It also adds two actions:

ZOMBIED_SHOOT

Moves toward the current enemy’s last seen position and uses raid/threat fire states depending on distance and visibility.

ZOMBIED_GO_TO_DANGER

Moves toward the best danger source, ignoring grenade movement targets and reacting to hits.

Where it is installed

combat_zombied is added by the broader combat scheme; it is not normally used as an active section in LTX. Use combat_type = zombied for intent, but also keep the NPC community as zombied because the evaluator checks community.

This helper is useful for simple zombie-style combat where the NPC should keep pressure on the enemy and react to danger without cover or camper behavior.

Example

[logic]
active = walker@zombie

[walker@zombie]
path_walk = zombie_walk
path_look = zombie_look
combat_type = zombied

Notes

  • SchemeCombat parses the zombied combat type, but the inspected combat_zombied evaluator itself checks the NPC community. Use zombied community data when relying on this behavior.
  • ZOMBIED_SHOOT may play fight_attack on activation with a 25 percent chance.
  • Use combat_camper or the normal combat flow when the NPC should use cover-like hide/look behavior instead.

companion

companion makes a stalker follow and assist the actor. The current implementation uses a simple walking behavior and planner action rather than a large LTX parameter set.

Parameters

companion has no scheme-specific LTX fields.

The section supports common switch fields. They are parsed into state.logic.

Behavior

On activation, the scheme sets behavior = 0, which corresponds to the simple walk behavior in ActionCompanionActivity.

The scheme adds EActionId.COMPANION_ACTIVITY to the state planner. It is a stalker scheme, so it expects a stalker object with a planner and normal movement access.

The action runs when the NPC is alive, has no enemy, and the companion section is active. It:

  • clears desired position and direction;
  • enables talk;
  • picks an accessible assist point near the actor;
  • moves to that point using level path movement;
  • chooses raid, rush, or assault state based on distance;
  • switches to threat while standing near the assist point and looking at the actor.

Example

[logic]
active = companion@follow

[companion@follow]
on_info = {+companion_stop} walker@wait

Notes

  • The source defines additional behavior constants for near/ignore/wait modes, but the current activation code always sets simple walking behavior.
  • The assist position is chosen to the side of the actor and must be accessible to the NPC.
  • Combat or death interrupts the action because the evaluator requires the NPC to be alive and without an enemy.
  • Use common switch fields on the companion section to exit follow behavior when quest state changes.

corpse_detection

corpse_detection is a generic stalker scheme for finding nearby lootable corpses. It adds planner logic that sends an NPC to a corpse within the configured search radius and plays the corpse-search state.

Parameters

corpse_detection_enabled

Type: boolean. Optional. Default: true.

Enables or disables corpse detection for the current reset section.

Behavior

The scheme adds IS_CORPSE_EXISTING and a SEARCH_CORPSE action. The evaluator returns true only when the NPC is alive, has no enemy, is not in danger, is not zombied, is not wounded, is not the cinematic actor visual, and a nearby lootable corpse exists within 20 meters.

When a corpse is selected, the evaluator stores the selected corpse id, vertex id, and position in scheme state and marks the corpse in portable storage so another NPC does not select the same corpse. The action sends the NPC to the corpse, switches to search_corpse near the target, and plays corpse_loot_begin once.

The finishCorpseLooting helper transfers items from the selected corpse to the looting NPC when the corpse object is online. It then plays either corpse_loot_good or corpse_loot_bad; empty transfers use the bad-loot sound.

Example

[logic]
active = walker@camp

[walker@camp]
path_walk = camp_walk
path_look = camp_look
corpse_detection_enabled = false

Notes

  • corpse_detection is generic stalker behavior. It is usually controlled from the active logic section, not selected as [logic] active.
  • Finalizing the search action frees the selected corpse marker.

cover

cover sends a stalker to a nearby cover point around a smart terrain and plays an animation while looking toward a generated reference position.

Parameters

smart

Type: string. Required. Default: none.

Smart terrain name used as the center for cover selection.

anim

Type: condlist. Optional. Default: hide.

Stalker state condlist used after the NPC reaches cover.

sound_idle

Type: string. Optional. Default: null.

Sound alias played while the NPC is in cover.

use_attack_direction

Type: boolean. Optional. Default: true.

Parsed into state. The inspected action does not currently read it.

radius_min

Type: number. Optional. Default: 3.

Minimum random distance from the smart terrain for selecting a cover search point.

radius_max

Type: number. Optional. Default: 5.

Maximum random distance from the smart terrain for selecting a cover search point.

The section also supports common switch fields.

Behavior

On activation, the cover action picks a random direction from the smart terrain level vertex and chooses a point between radius_min and radius_max. That point is also stored as the look reference. The action asks the engine for the best cover near it. If no cover is found, it uses the random point itself. If the chosen point is not accessible, it asks the object for the nearest accessible point.

While moving to cover, the NPC uses the assault state. After reaching the cover position, the action resolves anim and sets that stalker state, looking toward the generated enemy-facing position. If sound_idle is set, it plays that sound through the sound manager.

Example

[logic]
active = cover@base

[cover@base]
smart = esc_smart_terrain_1
anim = {+under_attack} hide_fire, hide
sound_idle = state
radius_min = 3
radius_max = 6
on_info = {+leave_cover} walker@guard

Notes

  • smart is required. Activation aborts when it is missing.
  • The action blocks normal alife while cover activity is needed.

danger

danger replaces the default danger evaluator for stalkers and updates danger state from heard hostile sounds. It is a generic planner scheme rather than a normal active movement section.

Parameters

danger has no scheme-specific LTX fields in the current TypeScript implementation.

Runtime constants:

INERTIA_TIME

Value: 15000.

Time in milliseconds to keep danger true after the object stops facing a current danger.

BULLET_REACT_DISTANCE_SQR

Value: 2 * 2.

Distance check for nearby bullet-hit sounds.

ALLIES_SHOOTING_ASSIST_DISTANCE_SQR

Value: 40 * 40.

Distance check for helping allies or reacting to enemy weapon sounds.

Behavior

The scheme replaces DANGER evaluators in the main planner and the nested danger action planner with EvaluatorDanger, then stores a DangerManager on state.

The evaluator returns true when the object is facing a danger. If the planner is already running the danger action, it stores the danger time. When no current danger is faced, it can keep returning true while the last danger time is within INERTIA_TIME.

The manager handles heard sounds. It can set danger time and destination vertex when the NPC hears nearby hostile bullet-hit sounds, enemy weapon sounds, or ally weapon sounds aimed at an enemy.

Example

[logic]
active = walker@guard

[walker@guard]
path_walk = guard_walk
path_look = guard_look

danger is installed generically for stalkers and does not need to be selected as the active section.

Notes

  • If a stalker is in a smart terrain and faces danger, the evaluator starts the smart terrain alarm.
  • The manager ignores heard sounds when the NPC already has a best enemy or cannot select the sound source as an enemy.

death

death executes configured condlists when a stalker dies and stores the killer id in the death scheme state.

Parameters

death reads its configuration indirectly:

on_death

Location: active logic section. Type: section name. Optional.

Names the death configuration section.

on_info

Location: death configuration section. Type: condlist. Optional.

First condlist executed on death.

on_info2

Location: death configuration section. Type: condlist. Optional.

Second condlist executed on death.

Behavior

On reset, the scheme reads on_death from the current logic section. If it is set, the named section must exist. The scheme then parses on_info and on_info2 from that section.

On death, the manager stores the killer object id, or -1 when no killer is provided. It then evaluates both parsed condlists with the actor and the dead object as context. The return value is not used for section switching; use effects inside the condlist for death side effects.

Example

[logic]
active = walker@guard
on_death = death@guard

[walker@guard]
path_walk = guard_walk
path_look = guard_look

[death@guard]
on_info = %+guard_dead%
on_info2 = {=killed_by_actor} %+actor_killed_guard%

Notes

  • Missing on_death is allowed.
  • If on_death names a section that does not exist, reset aborts.
  • Use mob_death for monster death callbacks. death is the stalker scheme.

gather_items

gather_items controls whether a stalker can use the base item-pickup evaluator. It is generic stalker behavior, not an active movement section.

Use it on the section that becomes active for the NPC. The scheme is reset with that section and reads gather_items_enabled from it.

Parameters

gather_items_enabled

Type: boolean. Optional. Default: true.

Enables item gathering for the current reset section.

Behavior

The scheme replaces the planner ITEMS evaluator with EvaluatorGatherItems. The evaluator returns true when gather_items_enabled resolved to true and the engine reports that there are items to pick up through object.is_there_items_to_pickup().

If the field is omitted, canLootItems is set to true during reset. Setting it to false blocks this evaluator for that active section only; it does not remove inventory logic from the engine or change item selection rules.

Example

[logic]
active = walker@post

[walker@post]
path_walk = post_walk
path_look = post_look
gather_items_enabled = false

Use this on story NPCs that must stay in position, wounded or dialog scenes where looting would break staging, and escort sections where the NPC should keep moving instead of reacting to nearby loot.

Notes

  • Use this field on the active section that is passed to scheme reset.
  • The scheme does not choose which items to pick up. It only controls whether the engine item evaluator is allowed to report available pickup work.
  • If a later section should allow looting again, omit the field or set gather_items_enabled = true there.

hear

hear parses on_sound rules from the current section and switches schemes when the object hears a matching sound. It is shared by stalker and monster binders.

Parameters

Add one or more on_sound lines to the active section:

on_sound, on_sound1, …

Type: pipe-separated parameter list. Optional.

story_id | sound_type | distance | power | condlist

Parameter meanings:

story_id

Position: 1.

Story id of the sound source. The runtime uses any when the source object has no story id.

sound_type

Position: 2.

Mapped sound type, such as WPN_shoot, WPN_hit, ITM_drop, or MST_attack.

distance

Position: 3.

Maximum distance from the heard sound position to the listening object.

power

Position: 4.

Minimum heard sound power.

condlist

Position: 5.

Section condlist evaluated when the sound matches.

Behavior

On reset, the scheme scans every line in the section and stores entries whose field name matches on_sound with an optional numeric suffix.

On a hear callback, the scheme first forwards the event to the object’s danger manager when present. It then resolves the source story id, maps the engine sound mask to a sound type enum, checks configured distance and power, and evaluates the stored condlist. A non-empty selected section switches the object to that section. An empty selected section removes that hear rule.

Example

[walker@guard]
path_walk = guard_walk
path_look = guard_look
on_sound = any|WPN_shoot|40|0.2|walker@alert

Notes

  • Supported mapped sound names are defined by the TypeScript ESoundType enum: weapon, item, monster, and NIL variants.
  • Rules are keyed by source story id and sound type. Multiple rules for the same pair overwrite the same stored slot.

heli_move

heli_move moves a helicopter along a patrol path and configures its targeting, weapon ranges, engine sound, fire trail, and optional combat health UI.

Parameters

path_move

Type: string. Required. Default: none.

Patrol path used for helicopter movement. Must exist.

path_look

Type: string. Optional. Default: null.

Patrol path used as a look point, or actor to keep looking at the actor.

enemy

Type: string. Optional. Default: null.

Enemy preference passed to the fire manager. Runtime handles actor, all, nil, or a story id.

fire_point

Type: string. Optional. Default: null.

Patrol path whose first point is used as a fallback fire point.

max_velocity

Type: number. Required. Default: none.

Maximum helicopter movement velocity.

max_mgun_attack_dist

Type: number. Optional. Default: null.

Overrides helicopter max minigun attack distance.

min_mgun_attack_dist

Type: number. Optional. Default: null.

Overrides helicopter min minigun attack distance.

max_rocket_attack_dist

Type: number. Optional. Default: null.

Overrides helicopter max rocket attack distance.

min_rocket_attack_dist

Type: number. Optional. Default: null.

Overrides helicopter min rocket attack distance.

upd_vis

Type: number. Optional. Default: 10.

Visibility refresh interval passed to the fire manager, in seconds.

use_rocket

Type: boolean. Optional. Default: true.

Enables rocket use during attack.

use_mgun

Type: boolean. Optional. Default: true.

Enables minigun use during attack.

engine_sound

Type: boolean. Optional. Default: true.

Enables helicopter engine sound.

stop_fire

Type: boolean. Optional. Default: false.

With path_look = actor, holds the helicopter position while the actor is visible.

show_health

Type: boolean. Optional. Default: false.

Shows the helicopter combat health UI while active.

fire_trail

Type: boolean. Optional. Default: false.

Enables the helicopter fire trail effect.

invulnerable

Type: boolean. Optional. Default: false.

Sets object registry invulnerable state.

immortal

Type: boolean. Optional. Default: false.

Sets object registry immortal state.

mute

Type: boolean. Optional. Default: false.

Sets object registry mute state.

The section also supports common switch fields. They are checked before movement updates.

Behavior

On activation, the manager asserts that path_move exists, parses waypoint data, creates the movement patrol, and sets linear acceleration and max velocity from max_velocity. It loops through patrol points and records waypoint signals from parsed waypoint data into state.signals.

If path_look is set to actor, the look point is refreshed from the actor position on update. If it names a patrol path, the first point of that path is used as the look point. The manager blocks free look and applies the look target through the helicopter fly manager.

Weapon distance fields are written to the engine helicopter object when present. use_mgun and use_rocket update the engine attack flags. The fire manager uses enemy, fire_point, and upd_vis to select or refresh enemies.

Example

[logic]
active = heli_move@patrol

[heli_move@patrol]
path_move = esc_heli_move
path_look = actor
max_velocity = 30
enemy = actor
use_mgun = true
use_rocket = false
upd_vis = 5
show_health = true
on_signal = patrol_done | heli_move@return

Notes

  • path_move is required and must exist.
  • When path_look names a patrol path, that patrol path must exist.
  • fire_point is read as a patrol path name and the first point is used. The current activation code does not assert that the path exists before constructing the patrol.
  • On save/load, the manager stores movement state, last and next waypoint indices, and whether a waypoint callback was pending.

help_wounded

help_wounded is generic stalker behavior for helping nearby wounded friendly stalkers. It sends a suitable NPC to the wounded target and plays the medkit-help animation.

Parameters

help_wounded_enabled

Type: boolean. Optional. Default: true.

Enables or disables wounded-helper behavior for the current reset section.

Runtime constants:

DISTANCE_TO_HELP

Value: 30.

Maximum search distance for wounded targets.

HELPING_WOUNDED_OBJECT_KEY

Value: helping_wounded_object.

Portable-store key used to reserve a wounded target for one helper.

Behavior

The scheme adds IS_WOUNDED_EXISTING and a HELP_WOUNDED action. The evaluator only allows helping when the NPC is alive, has no enemy, is not zombied, is not wounded, is not the cinematic actor visual, and a wounded target is nearby.

When a target is selected, the evaluator stores the wounded object id, vertex id, and position in scheme state, and marks the target in portable storage so another helper does not claim it. The action runs to the target, then switches to help_wounded_with_medkit, looks at the wounded position, and plays wounded_medkit once.

The finishObjectHelpWounded helper gives the selected wounded object a medkit and unlocks its wounded manager medkit use.

Example

[logic]
active = walker@camp

[walker@camp]
path_walk = camp_walk
path_look = camp_look
help_wounded_enabled = false

Notes

  • The helper action blocks item gathering and alife actions while a wounded target is selected.
  • Finalizing the action frees the wounded target reservation.

hit

hit switches a stalker section when the NPC receives a hit callback. It also records hit metadata that conditions can use indirectly through the scheme state.

Parameters

hit has no scheme-specific fields.

The section supports common switch fields. They are evaluated from the hit callback.

Behavior

On activation, hit verifies that the configured section exists and parses common switch conditions.

When the NPC is hit, the manager stores the hit bone index and attacker id. Missing attackers are stored as -1. A zero-damage hit is ignored when the object is not invulnerable. If the object has an active scheme, the manager marks isDeadlyHit when the hit amount is greater than or equal to current health times 100, tries to switch section, then clears the flag.

Runtime sequence

  1. SchemeHit.activate aborts if the referenced section does not exist.
  2. SchemeHit.add subscribes a HitManager action.
  3. HitManager.onHit stores boneIndex in the hit scheme state.
  4. Zero-damage hits are ignored unless the object is invulnerable.
  5. The manager stores who.id() or -1.
  6. While switching, isDeadlyHit is true only for hits whose amount is at least health * 100.
  7. The flag is cleared after the switch attempt.

Example

[logic]
active = hit@guard

[hit@guard]
on_info = walker@angry %=give_info(guard_was_hit)%

Notes

  • This scheme is event-driven. Its switch fields are checked when the object is hit.
  • Disabling the scheme unsubscribes the stored hit manager.
  • Use condition/effect code that reads the scheme state if behavior depends on attacker, bone, or deadly-hit status.

meet

meet controls how a stalker reacts to the actor at interaction distance: greeting sounds, idle animations, use permission, dialog start, trade availability, abuse state, and interaction text.

Unlike active movement schemes, meet is a generic stalker scheme. The current active section points to a meet section with a meet = ... field, and the meet scheme is reset whenever the active logic section changes.

Parameters

Most fields are condlists. They can return values such as distances, animation states, sound ids, dialog ids, true, false, or nil.

close_distance

Type: condlist number. Default source: relation defaults.

Distance for close meet state.

close_anim

Type: condlist state. Default source: relation defaults.

Animation state used during close contact.

close_snd_distance

Type: condlist number. Default source: relation defaults.

Distance for hello and bye sound checks.

close_snd_hello

Type: condlist sound. Default source: relation defaults.

Sound played when the actor first enters close sound distance.

close_snd_bye

Type: condlist sound. Default source: relation defaults.

Sound played after hello when the actor is still inside far sound distance.

close_victim

Type: condlist story id. Default source: relation defaults.

Object story id used as look victim for close animation.

far_distance

Type: condlist number. Default source: relation defaults.

Distance for far meet state.

far_anim

Type: condlist state. Default source: relation defaults.

Animation state used during far contact.

far_snd_distance

Type: condlist number. Default source: relation defaults.

Distance for far sound checks.

far_snd

Type: condlist sound. Default source: relation defaults.

Sound played while executing far meet state.

far_victim

Type: condlist story id. Default source: relation defaults.

Object story id used as look victim for far animation.

snd_on_use

Type: condlist sound. Default source: relation defaults.

Use sound condlist stored by the scheme.

use

Type: condlist value. Default source: relation defaults.

Controls actor use behavior. self starts talk directly.

meet_dialog

Type: condlist dialog id. Default source: relation defaults.

Overrides the object’s start dialog. nil restores the default start dialog.

abuse

Type: condlist boolean. Default source: relation defaults.

Enables or disables abuse state on the object.

trade_enable

Type: condlist boolean. Default source: relation defaults.

Enables or disables trading, unless the NPC is wounded.

allow_break

Type: condlist boolean. Default source: relation defaults.

Allows or blocks breaking the talk dialog.

meet_on_talking

Type: boolean string. Default source: relation defaults.

Treats current talking as close meet contact when enabled.

use_text

Type: condlist string. Default source: relation defaults.

Overrides the interaction tip text. nil restores default use text behavior.

The engine uses enemy defaults for hostile NPCs and neutral defaults for other relations. If the section is no_meet, interaction is disabled by setting distances to 0, animations and sounds to nil, and use to false.

Usage

Reference a meet section from the active logic section:

[logic]
active = walker@guard

[walker@guard]
path_walk = guard_walk
meet = meet@guard

[meet@guard]
close_distance = 3
close_anim = talk_default
close_snd_hello = meet_hello
use = {=dist_to_actor_le(3)} true, false
meet_dialog = guard_dialog
trade_enable = false

To disable meeting for a section, point meet to the no-meet section used by the project config.

Behavior

The meet manager checks actor distance and visibility each update. It tracks close, far, and reset states. When the actor moves beyond the reset distance, hello and bye flags are cleared.

When meet_dialog changes, the manager updates the object’s start dialog. If the object is already talking, it can run the actor talk dialog with the current break setting.

use = self starts the talk dialog directly when the actor can use the object and the screen is not black.

Notes

  • The reset distance is fixed in config at 30.
  • Wounded NPCs have trading disabled regardless of trade_enable.
  • The meet action blocks ALife and idle reset while the actor is in meet contact.

mob_combat

mob_combat is the generic monster combat switch scheme. It listens for monster combat events and uses common switch conditions to move the object to another section.

Parameters

mob_combat has no scheme-specific config fields. It reads common switch fields from the section.

on_info, on_info1, …

Type: condlist.

Switch when the condlist selects another section.

on_timer, on_timer1, …

Type: milliseconds | condlist.

Switch after the section has been active for the duration.

on_signal, on_signal1, …

Type: signal | condlist.

Switch when a signal is set.

Behavior

The manager only acts on the combat event. If the scheme is enabled, the monster has an enemy, and the object has an active scheme, it calls the common section switcher.

The scheme can be disabled through SchemeMobCombat.disable, which sets its runtime enabled flag to false.

Example

[logic]
active = mob_home@idle
on_combat = mob_combat@combat

[mob_combat@combat]
on_info = mob_walker@attack

Notes

  • This is normally referenced from a monster logic section through on_combat.
  • It does not perform combat movement by itself.
  • Put movement behavior in the target monster section, such as mob_home, mob_walker, or another monster scheme.

mob_death

mob_death handles monster death callbacks. It records the killer id and then evaluates common switch logic.

Use it from monster logic through on_death when a death should set info portions, trigger quest effects, or switch to a cleanup section.

Parameters

mob_death has no scheme-specific config fields. It reads common switch fields from the section.

on_info, on_info1, …

Type: condlist.

Switch when the condlist selects another section.

on_signal, on_signal1, …

Type: signal | condlist.

Switch when a signal is set.

on_timer, on_timer1, …

Type: milliseconds | condlist.

Switch after the section has been active for the duration.

Behavior

On death, the manager stores the killer id in the object’s death state:

  • killer.id() when a killer exists;
  • -1 when the killer is nil.

After that, it calls the common switcher for the mob_death state.

The stored killer id is available in the death state for code that needs to inspect who killed the monster. The scheme does not decide rewards by itself; put reward effects in the condlist.

Example

[logic]
active = mob_home@idle
on_death = mob_death@dead

[mob_death@dead]
on_info = nil %+bloodsucker_dead%

Notes

  • This is normally referenced from a monster logic section through on_death.
  • Use condlist effects to set info portions, spawn rewards, or advance quest state.
  • Use killer = -1 as the nil-killer case when reading the stored state in script code.

mob_home

mob_home assigns a monster to a home area and radius range. Use it to keep monsters near a lair, patrol center, smart terrain point, or scripted territory.

Parameters

state

Type: monster state. Optional. Default: null.

Base monster state applied on activation.

path_home

Type: string. Optional. Default: null.

Patrol path used as home reference. Relative names are resolved against the active smart terrain.

gulag_point

Type: boolean. Optional. Default: false.

Uses the monster’s smart terrain level vertex as the home point.

aggressive

Type: boolean. Optional. Default: false.

Passed to object.set_home.

home_min_radius

Type: number. Optional. Default: default config.

Minimum home radius.

home_mid_radius

Type: number. Optional. Default: midpoint.

Middle home radius. Clamped to the range between min and max.

home_max_radius

Type: number. Optional. Default: default config.

Maximum home radius.

The section also supports common switch fields.

Behavior

On activation, the manager applies the configured monster state, resolves home parameters, and calls object.set_home(home, min, max, aggressive, mid).

If path_home is set, waypoint data on its first point can provide minr and maxr values. Explicit home_min_radius and home_max_radius override waypoint values.

On deactivation, the manager calls object.remove_home().

Example

[logic]
active = mob_home@lair

[mob_home@lair]
path_home = bloodsucker_home
home_min_radius = 5
home_max_radius = 35
aggressive = true
on_info = {+actor_entered_lair} mob_walker@attack

Notes

  • home_max_radius must be greater than home_min_radius.
  • With gulag_point = true, the home is based on the monster’s current smart terrain.

mob_jump

mob_jump captures a monster, turns it toward a point, and forces a jump. After the jump, it releases the monster from script control.

Use it for scripted scare jumps and ambush starts.

Parameters

path_jump

Type: string. Optional. Default: null.

Patrol path whose first point is the jump target base. Relative names are resolved against the active smart terrain.

offset

Type: x,y,z string. Required. Default: none.

Offset added to path_jump point 0 to form the final jump target.

ph_jump_factor

Type: number. Optional. Default: 1.8.

Jump factor passed to object.jump.

on_signal

Type: switch field. Required. Default: none.

Required by the scheme parser. Usually listens for jumped.

The section also supports other common switch fields.

Behavior

On activation, the manager captures the monster and resolves the jump point. It commands the monster to look at the target, waits for the look action to finish, then calls object.jump(point, ph_jump_factor).

After jumping, it sets signal jumped and releases the monster.

Runtime sequence

  1. SchemeMobJump.activate requires on_signal, reads path_jump, offset, and ph_jump_factor.
  2. MobJumpManager.activate captures the monster for scripted control.
  3. The manager resolves patrol point 0, adds offset, and stores the final jump point.
  4. On update, the monster is commanded to look at the jump point.
  5. After the look action ends, the manager calls object.jump(...).
  6. The manager sets the jumped signal and releases the monster.

Example

[logic]
active = mob_jump@scare

[mob_jump@scare]
path_jump = bloodsucker_jump
offset = 0, 1, 0
ph_jump_factor = 1.8
on_signal = jumped | mob_home@after_jump

Notes

  • on_signal must exist in the section.
  • path_jump must resolve to an existing patrol path.
  • offset must contain three numeric components.
  • If path_jump is omitted, the active smart terrain name is used as the path name.

mob_remark

mob_remark plays scripted monster animations and optional interaction state. Use it for monster idles, scripted threat displays, scene beats, and temporary talk/tip control.

Parameters

state

Type: monster state. Optional. Default: null.

Monster state applied on activation.

dialog_cond

Type: condlist. Optional. Default: null.

Enables or disables talking based on condlist result.

anim

Type: comma-separated strings. Optional. Default: null.

Animation sequence to command.

anim_movement

Type: boolean. Optional. Default: false.

Uses movement animation command form when true.

anim_head

Type: string. Optional. Default: null.

Parsed and stored; current manager does not use it directly.

tip

Type: string. Optional. Default: null.

Tip notification sent once after activation.

snd

Type: string. Optional. Default: null.

Parsed and stored; current manager does not play it directly.

time

Type: comma-separated numbers. Optional. Default: null.

Per-animation timeout list. Missing values use animation-end condition.

The section also supports common switch fields.

Behavior

On activation, the manager disables talk, applies monster state, captures the monster, and queues each animation from anim. If a matching time value exists, the animation uses a time-end condition. Otherwise it waits for animation end.

On update, it:

  • toggles talk based on dialog_cond;
  • sends tip once through the notification manager;
  • sets signal action_end once the scripted action is finished.

Example

[logic]
active = mob_remark@threat

[mob_remark@threat]
state = threat
anim = stand_idle, attack_prepare
time = 2000, 1000
tip = st_monster_warning
on_signal = action_end | mob_walker@attack

Notes

  • anim and time are parsed as comma-separated lists.
  • action_end is the usual signal for switching after the scripted remark.
  • noReset is always set to true by the scheme.

mob_walker

mob_walker makes a monster follow a patrol path and optionally stop at look points for scripted animations, sounds, and state changes.

Use it for non-combat monster patrols, scripted monster movement, lair idles, and simple ambush staging.

Parameters

path_walk

Type: string. Required. Default: none.

Patrol path used for monster movement. Relative names are resolved against the active smart terrain.

path_look

Type: string. Optional. Default: null.

Patrol path used for look or idle points. It must not equal path_walk.

state

Type: monster state. Optional. Default: null.

Base monster state read by the shared monster-state parser.

no_reset

Type: boolean. Optional. Default: false.

Stored in scheme state for compatibility with monster logic.

The section also supports common switch fields such as on_info, on_signal, on_timer, and actor-zone checks.

Waypoints

When activated, the manager captures the monster for scripted commands and sends it along path_walk.

Movement waypoints can provide extra data parsed from patrol point flags:

sig

Sets a signal on the active scheme state. Use it with on_signal.

s

Schedules a sound to play with the next movement or standing command.

c

Uses crouch or steal movement when set to true.

r

Uses run movement when set to true.

b

Overrides the monster state at that waypoint.

flags

Selects a matching path_look point.

Look waypoints can provide:

t

Standing time in milliseconds.

a

Animation condlist. The selected value is resolved through the engine anim table.

If a movement waypoint has look flags, the manager chooses a matching point from path_look, turns the monster toward it, plays the selected animation, waits, and then resumes movement.

Example

[logic]
active = mob_walker@lair

[mob_walker@lair]
path_walk = bloodsucker_walk
path_look = bloodsucker_look
state = nvis
on_signal = attack_ready | mob_home@attack
on_info = {+actor_entered_lair} mob_home@attack

[mob_home@attack]
path_home = bloodsucker_home

Use waypoint sig data on bloodsucker_walk to raise attack_ready when the monster reaches the intended patrol point.

Notes

  • path_walk is required.
  • path_look cannot be the same as path_walk.
  • The manager reactivates itself if the monster is no longer script-captured.
  • If a waypoint requests a look point but no matching look point exists, the scheme aborts with a config error.

patrol

patrol coordinates a group of stalkers around a commander. Use it when several NPCs should move as one patrol instead of each running an independent walker path.

The scheme is a stalker scheme. It registers each participating object in a shared patrol manager. The first registered object becomes commander unless a section has commander = true.

Parameters

path_walk

Type: string. Required. Default: none.

Commander movement path. Relative names are resolved against the active smart terrain.

path_look

Type: string. Optional. Default: null.

Optional look path for the commander. It must not equal path_walk.

formation

Type: string. Optional. Default: back.

Formation used by followers. Supported values are defined by the patrol formation config.

silent

Type: boolean. Optional. Default: false.

Disables automatic patrol movement sounds.

move_type

Type: string. Optional. Default: patrol.

Stored movement type for patrol logic compatibility.

commander

Type: boolean. Optional. Default: false.

Marks this NPC as the patrol commander when registering in the patrol manager.

def_state_standing

Type: string. Optional. Default: null.

Suggested standing animation state.

def_state_moving

Type: string. Optional. Default: def_state_moving1.

Suggested commander moving animation state.

def_state_moving1

Type: string. Optional. Default: null.

Compatibility fallback for def_state_moving.

The section also supports common switch fields such as on_info, on_signal, on_timer, and actor-distance checks.

Behavior

Give all members of the same patrol the same path_walk. If the NPCs belong to a squad, the engine keys the shared patrol manager by path name plus squad id, so separate squads can use the same route without sharing one runtime manager.

Followers update their target roughly once per second. They follow the commander’s current path, direction, movement state, and formation offset. If a follower falls too far behind, the patrol manager can return an accelerated movement state based on the commander’s current state.

The commander can change formation from waypoint callback return values:

0

line

1

around

2

back

Example

[logic]
active = patrol@route

[patrol@route]
path_walk = squad_patrol_walk
path_look = squad_patrol_look
formation = back
commander = true
def_state_moving = patrol
on_info = {+base_alarm} patrol@alarm

[patrol@alarm]
path_walk = squad_alarm_walk
formation = line
silent = true
def_state_moving = rush

Use the same section on the intended patrol members. Set commander = true on the NPC that should drive the formation.

Notes

  • path_walk is required.
  • path_look cannot be the same as path_walk.
  • A patrol manager rejects attempts to register more than seven objects.
  • Objects unregister from the patrol manager on scheme deactivation, death, or offline switch.

ph_button

ph_button plays a button animation and switches sections when the object is used. Use it for physical buttons, levers, and scripted controls.

The scheme is for physical objects. On activation it sets the object tooltip, subscribes PhysicalButtonManager, and plays the configured animation cycle.

Parameters

anim

Type: string. Required. Default: none.

Animation cycle played on activation.

anim_blend

Type: boolean. Optional. Default: true.

Passed as the blending flag to object.play_cycle.

on_press

Type: condlist. Optional. Default: null.

Switch condlist evaluated when the active button is used.

tooltip

Type: string. Optional. Default: null.

Tip text shown on the object. Empty text is used when absent.

The section also supports common switch fields such as on_info and on_timer.

Example

[logic]
active = ph_button@off

[ph_button@off]
anim = idle_off
tooltip = st_press_button
on_press = ph_button@on %+button_pressed%

[ph_button@on]
anim = idle_on
anim_blend = false

Notes

  • anim is required.
  • anim_blend defaults to true.
  • on_press only runs when the object is still in the active button section.
  • Common switch conditions are checked during update.
  • Use on_press for use-triggered transitions and common switch fields for background transitions.

ph_code

ph_code opens a numeric input window for a physical object and evaluates condlists for entered codes. It is mainly used for code locks.

Parameters

tips

Type: string. Optional. Default: st_codelock.

Tip text assigned to the object.

code

Type: number. Optional. Default: null.

Single accepted numeric code.

on_code

Type: condlist. Optional. Default: null.

Condlist evaluated when entered text equals code.

on_check_code1, on_check_code2, …

Type: string | condlist. Optional. Default: empty.

Per-code condlists used when code is not set.

Behavior

There are two modes:

Single code

Config: code plus on_code.

Entering the matching number evaluates on_code when it is configured. If code is set and on_code is absent, the matching input is accepted by the check but has no documented side effect.

Multiple codes

Config: numbered on_check_codeN fields.

Entering a matching text key evaluates that key’s condlist.

Current implementation evaluates the selected condlist for effects and info portion changes. It does not explicitly switch to the returned section from the code manager.

Example

[logic]
active = ph_code@lock

[ph_code@lock]
tips = st_enter_code
code = 1234
on_code = nil %+door_code_entered =play_sound(code_ok)%

Multiple code form:

[ph_code@multi]
on_check_code1 = 1111 | nil %+first_code_entered%
on_check_code2 = 2222 | nil %+second_code_entered%

Notes

  • Activation makes the object script-usable by setting nonscript_usable to false.
  • Deactivation clears the tip text.
  • Use ph_door or another section’s switch logic when a successful code should open or change an object state.

ph_door

ph_door controls a physical door object: initial open or closed state, lock state, NPC locking, tips, sounds, use handling, and hit-on-bone section switches.

Parameters

closed

Type: boolean. Optional. Default: true.

Activates the door as closed when true, open when false.

locked

Type: boolean. Optional. Default: false.

Locks the door and locks it for NPCs.

no_force

Type: boolean. Optional. Default: false.

Uses zero joint force instead of applying opening or closing force.

not_for_npc

Type: boolean. Optional. Default: false.

Locks the door for NPCs without necessarily locking actor use.

show_tips

Type: boolean. Optional. Default: true.

Enables door tip text updates.

tip_open

Type: string. Optional. Default: tip_door_open.

Tip shown when the closed door can be opened.

tip_close

Type: string. Optional. Default: tip_door_close.

Tip shown when the opened door can be closed.

slider

Type: boolean. Optional. Default: false.

Uses slider-style joint angle checks.

snd_open_start

Type: string. Optional. Default: trader_door_open_start.

Sound played when opening starts, and when a locked door is used.

snd_close_start

Type: string. Optional. Default: trader_door_close_start.

Sound played when closing starts.

snd_close_stop

Type: string. Optional. Default: trader_door_close_stop.

Sound played when closing finishes.

on_use

Type: condlist. Optional. Default: null.

Switch condlist evaluated when the door is used.

hit_on_bone

Type: bone descriptor list. Optional. Default: empty.

Maps hit bone indexes to section switch condlists.

The implementation currently reads the locked-door tip from tip_open with default tip_door_locked.

Behavior

Using the door evaluates on_use and switches to the selected section. Hitting a configured bone evaluates the matching hit_on_bone condlist and switches to the selected section.

hit_on_bone uses repeated bone_index|condlist descriptors:

hit_on_bone = 1|ph_door@open %+door_forced%|2|ph_door@broken

Example

[logic]
active = ph_door@closed

[ph_door@closed]
closed = true
locked = false
on_use = ph_door@open
tip_open = st_open_door

[ph_door@open]
closed = false
on_use = ph_door@closed
tip_close = st_close_door

Notes

  • The object is registered as a door for NPCs when the scheme is added.
  • The manager expects a physics shell with a door joint.
  • Deactivation clears the tip text.
  • Common switch conditions are checked during update.

ph_force

ph_force applies a constant force to a physical object toward a patrol point. Use it for scripted pushes, moving props, and one-shot physical impulses that should last for a configured duration.

Parameters

force

Type: number. Required. Default: none.

Force magnitude. Must be greater than 0.

time

Type: number. Required. Default: none.

Duration passed to object.set_const_force. Must be greater than 0.

delay

Type: number. Optional. Default: 0.

Delay in milliseconds before applying the force.

point

Type: string. Required. Default: none.

Patrol path used to choose the target point.

point_index

Type: number. Optional. Default: 0.

Patrol point index used as the force target.

The section also supports common switch fields. They are checked before force application.

Behavior

On activation the scheme validates force, time, point, and point_index, then stores the selected patrol point in state. On manager activation, a non-zero delay schedules the first force attempt for a later game time.

Each update first checks common switch conditions. If a switch happens, no force is applied. If processing already finished, the manager returns. Otherwise it waits for the delay, computes the direction from the physical object to the stored patrol point, normalizes it, and calls:

object.set_const_force(direction, force, time);

After the call, the manager marks processing complete.

Example

[logic]
active = ph_force@push

[ph_force@push]
force = 500
time = 2000
delay = 500
point = push_target
point_index = 0
on_timer = 3000 | ph_idle@done

Notes

  • force, time, and point are required.
  • force and time must be positive.
  • point_index must be inside the patrol path point count.
  • The force is applied once. After that, the manager marks processing complete.
  • Common switch fields run before force application, so a matching switch can prevent the force entirely.

ph_hit

ph_hit applies a scripted hit to a physical object when the section activates. Use it for one-shot impacts, breaking props, kicking an object, or driving door and physics reactions through the normal hit API.

The hit is created by PhysicalHitManager during activation. The manager does not wait for actor interaction.

Parameters

power

Type: number. Optional. Default: 0.

Hit power.

impulse

Type: number. Optional. Default: 1000.

Hit impulse.

bone

Type: string. Required. Default: none.

Bone name passed to the hit object.

dir_path

Type: string. Required. Default: none.

Patrol path whose first point defines the hit direction.

The section also supports common switch fields such as on_info and on_timer.

Example

[logic]
active = ph_hit@kick

[ph_hit@kick]
power = 0.5
impulse = 1200
bone = door
dir_path = kick_direction
on_timer = 100 | ph_idle@after_hit

Notes

  • bone and dir_path are required. Missing values fail during scheme activation.
  • The hit direction is calculated from the object position toward point 0 of dir_path.
  • The hit type is strike.
  • The hit is applied on activation. Common switches are checked during later updates.
  • Use an on_timer or another common switch when the object should move to an idle section after the impact.

ph_idle

ph_idle is the neutral physical-object scheme. It keeps an object usable or non-usable, shows an optional tip, and can switch sections when the object is used or hit on configured bones.

Use it for switches, props, doors, breakable objects, and scene objects that should wait for actor use or damage.

Parameters

hit_on_bone

Type: bone descriptor list. Optional. Default: empty.

Maps hit bone indexes to condlists.

nonscript_usable

Type: boolean. Optional. Default: false.

Passed to object.set_nonscript_usable on activation.

on_use

Type: condlist. Optional. Default: null.

Switch condlist evaluated when the object is used.

tips

Type: string. Optional. Default: empty string.

Tip text assigned to the object.

The section also supports common switch fields such as on_info and on_timer.

Bone hit descriptors

hit_on_bone uses repeated bone_index|condlist descriptors:

hit_on_bone = 1|ph_idle@hit %+box_was_hit%|2|ph_idle@hit

When a matching bone is hit, the manager evaluates the condlist and switches to the selected section.

Behavior

  1. Activation parses common switch fields, hit_on_bone, nonscript_usable, on_use, and tips.
  2. The object tip text is set immediately.
  3. Manager activation calls object.set_nonscript_usable(...).
  4. Each update checks common switch fields.
  5. onUse evaluates on_use and switches to the selected section.
  6. onHit checks the hit bone index against hit_on_bone and switches through that condlist when a match exists.

Example

[logic]
active = ph_idle@locked

[ph_idle@locked]
tips = st_locked_box
on_use = {+actor_has_key} ph_idle@open %=play_sound(box_open)%
hit_on_bone = 1|ph_idle@broken %+box_broken%

[ph_idle@open]
nonscript_usable = true

Notes

  • The manager clears the tip text on deactivation.
  • on_use and hit_on_bone use explicit section switching.
  • Common switch conditions are checked during update.
  • Bone descriptors use engine bone indexes, not bone names.

ph_minigun

ph_minigun controls a physical minigun object. It can aim at a patrol point, the actor, or a story object, and it can switch sections when a watched target becomes visible or hidden.

Parameters

path_fire

Type: string. Optional. Default: null.

Patrol path used as the fire point when target = points. Smart terrain prefixing is applied.

auto_fire

Type: boolean. Optional. Default: false.

Enables automatic fire for enemy targets when the current target can be hit. Point-target firing uses its own firing path in the manager and is not gated the same way.

fire_time

Type: number. Optional. Default: 1.0.

Fire phase duration in seconds.

fire_repeat

Type: number. Optional. Default: 0.5.

Pause duration in seconds between fire phases. -1 disables the fire/pause timer update.

fire_range

Type: number. Optional. Default: 50.

Maximum distance to an enemy target.

target

Type: string. Optional. Default: points.

Fire target. Supported runtime values are points, actor, or a story object id. Smart terrain prefixing is applied.

track_target

Type: boolean. Optional. Default: false.

Keeps aiming at the enemy target even when the minigun cannot fire at it.

fire_angle

Type: number. Optional. Default: 120.

Horizontal firing arc used by the manager when checking whether the target can be aimed at.

shoot_only_on_visible

Type: boolean. Optional. Default: true.

Requires engine visibility before firing at an enemy target.

on_target_vis

Type: condlist. Optional. Default: null.

story_id | condlist pair. Switches section when that story object is alive and visible to the minigun.

on_target_nvis

Type: condlist. Optional. Default: null.

story_id | condlist pair. Switches section when that story object is alive and not visible to the minigun.

The section also supports common switch fields. They are checked on manager update before minigun-specific processing.

Behavior

On activation, the manager gets the object’s car interface, disables normal script use, clears the tip text, and activates the mounted weapon if the car has one.

When target = points, path_fire must point to an existing patrol path. The minigun aims at the first point of that path and toggles fire according to the firing timer. When target = actor, the actor is used if alive. Any other non-null value is resolved as a story object id.

Firing only starts when the target is inside fire_range, inside the configured firing arc, and visible unless shoot_only_on_visible = false. Enemy target aim height is adjusted for actor, crouching NPCs, wounded NPCs, and normal standing NPCs.

Example

[logic]
active = ph_minigun@post

[ph_minigun@post]
target = actor
auto_fire = true
fire_range = 60
fire_time = 2
fire_repeat = 1
fire_angle = 90
shoot_only_on_visible = true
on_target_nvis = esc_actor_story | ph_idle@quiet

Notes

  • on_target_vis and on_target_nvis use a story object id before the |, not a section name.
  • If path_fire is configured and the patrol path does not exist, activation aborts.
  • If the minigun car health reaches zero, the manager stops firing, releases script capture, optionally grants onDeathInfo from state, and switches the object to nil on the next update.

ph_on_death

ph_on_death switches a physical object when it receives a death callback. Use it for scripted reactions to destroyed physics objects.

Parameters

ph_on_death has no scheme-specific fields.

The section supports common switch fields. They are evaluated from the death callback.

Behavior

The manager subscribes to physical object death events. When the object dies and the object still has an active scheme, it calls the common section-switching logic for the current ph_on_death state.

The death callback receives the dead object and optional killer object. The current manager does not inspect the killer; conditions and effects in the switch fields define the response.

Runtime sequence

  1. SchemePhysicalOnDeath.activate parses common switch conditions with getConfigSwitchConditions.
  2. SchemePhysicalOnDeath.add stores a PhysicalDeathManager action on the state and subscribes it.
  3. PhysicalDeathManager.onDeath checks that the physical object still has an active scheme.
  4. The manager calls trySwitchToAnotherSection for the current state.

The scheme is event-driven. It does not run a regular update loop and does not evaluate the killer object.

Example

[logic]
active = ph_on_death@barrel

[ph_on_death@barrel]
on_info = ph_idle@dead %=give_info(barrel_destroyed)%

Notes

  • The implementation comments note that disable does not unsubscribe from the death callback because death is expected to happen once.
  • The scheme does not apply damage, spawn particles, or play sounds by itself. Put those effects in the switch condlist.
  • Use another physical scheme, such as ph_idle, for the section that should exist after the destroyed-state switch.

ph_on_hit

ph_on_hit switches a physical object when it receives a hit callback. Use it for breakable or reactive props where the next section should be chosen only after damage is applied.

Parameters

ph_on_hit has no scheme-specific fields.

The section supports common switch fields such as on_info, on_timer, and zone or distance checks. They are evaluated from the hit callback, not from a normal per-frame update.

Behavior

The manager subscribes to physical object hit events. When the object is hit and the object still has an active scheme, it calls the common section-switching logic for the current ph_on_hit state.

Hit amount, direction, attacker, and bone index are received by the callback, but the current implementation only logs the object name, bone index, and hit amount. The switch conditions decide what happens next.

Runtime sequence

  1. SchemePhysicalOnHit.activate parses common switch conditions.
  2. SchemePhysicalOnHit.add stores a PhysicalOnHitManager action and subscribes it.
  3. PhysicalOnHitManager.onHit logs object name, bone index, and hit amount.
  4. If the object still has an active scheme, the manager calls trySwitchToAnotherSection.
  5. SchemePhysicalOnHit.disable unsubscribes the stored action when the state exists.

The callback receives hit direction and attacker, but those values are not written to scheme state by the current implementation.

Example

[logic]
active = ph_on_hit@crate

[ph_on_hit@crate]
on_info = ph_idle@damaged %=give_info(crate_was_hit)%

Notes

  • The scheme is event-driven. Without a hit callback, its switch fields are not checked by this manager.
  • disable unsubscribes the stored manager action when the scheme state exists.
  • Use ph_idle bone-hit condlists when the response depends on a specific physical bone; ph_on_hit treats all hits the same.

ph_oscillate

ph_oscillate applies alternating constant force to a physical object joint. Use it for objects that should sway or rock around a physics bone.

Parameters

joint

Type: string. Required. Default: none.

Physics joint bone name. Smart terrain prefixing is applied by the parser.

period

Type: number. Required. Default: none.

Time interval used by the oscillation manager.

force

Type: number. Required. Default: none.

Force magnitude used to calculate force growth during the active part of the period.

correct_angle

Type: number. Optional. Default: 0.

Rotation angle applied to the next force direction when the oscillation flips.

The section also supports common switch fields, parsed into state with the rest of the section.

Behavior

On activation, the manager:

  1. stores the current game time;
  2. chooses a random horizontal direction;
  3. calculates force / period;
  4. finds the physics joint by joint;
  5. starts unpaused.

During update, the manager applies object.set_const_force(direction, elapsed * force / period, 2) until period passes. It then flips the horizontal direction, rotates it by correct_angle, pauses for half of period, and repeats.

Example

[logic]
active = ph_oscillate@swing

[ph_oscillate@swing]
joint = door_hinge
period = 1000
force = 20
correct_angle = 15

Notes

  • period is used directly against time_global() deltas, so configure it in the same time units used by engine time.
  • The manager looks up the joint on activation. The object must have a physics shell and a matching bone joint.

post_combat_idle

post_combat_idle makes a non-zombied stalker wait briefly after combat before returning to alife, looting, or helper behavior. It is installed by setup code and does not have a hand-authored active section.

Parameters

post_combat_idle has no scheme-specific LTX fields in the current TypeScript implementation.

The wait duration can be affected by resolved logic overrides stored on the object registry:

minPostCombatTime

Default: 5.

Minimum randomized wait time in seconds after a non-actor enemy disappears.

maxPostCombatTime

Default: 10.

Maximum randomized wait time in seconds after a non-actor enemy disappears.

These overrides are consumed by the evaluator. Their parsing is handled outside SchemePostCombatIdle.

Behavior

SchemePostCombatIdle.setup() skips zombied-community stalkers. For other stalkers it:

  1. creates post_combat_idle state in the object registry;
  2. replaces ENEMY evaluators in the main planner and nested combat planner;
  3. adds the POST_COMBAT_WAIT action to the combat planner.

The evaluator returns true while a selectable best enemy exists. When the enemy disappears, it starts a timer. Actor targets reset the timer to the current time. Other enemies use a randomized delay between minPostCombatTime and maxPostCombatTime, or the default 5 to 10 seconds.

The wait action equips the best weapon, sets danger/crouch/stand posture, uses danger sight, starts the hide animation when possible, and plays post_combat_wait. On finalize, it plays post_combat_relax and clears the animation state.

Example

[logic]
active = walker@guard

[walker@guard]
path_walk = guard_walk
path_look = guard_look

post_combat_idle is installed by the stalker scheme setup path. It is not selected with [logic] active.

Notes

  • The action does not start the hide animation while the NPC is in a smart cover or its weapon is locked.
  • If an animation is still clearing after the timer expires, the evaluator can keep returning true until the animation marker is gone.

reach_task

reach_task drives squad members toward their assigned simulation target. It is part of generic stalker alife behavior, not a hand-authored active section with LTX parameters.

Parameters

reach_task has no scheme-specific LTX fields in the current TypeScript implementation.

Runtime constants:

PATROL_UPDATE_PERIOD

Value: 1000.

Milliseconds between movement-order updates.

FORMATIONS.back

Value: built-in formation list.

Default follower offsets behind the squad commander.

Behavior

SchemeReachTask.setup() installs the SMART_TERRAIN_TASK evaluator and action inside the nested alife planner. The evaluator returns true when the NPC’s squad has a REACH_TARGET action and the assigned simulation target is not yet reached.

The action initializes movement toward the squad assigned target. The squad commander moves toward the target game vertex and level vertex. Other squad members follow orders from ReachTaskPatrolManager, which keeps them in formation behind the commander and accelerates members that fall behind.

Objects are removed from the patrol manager when they die or switch offline.

Runtime sequence

  1. SchemeReachTask.setup replaces the nested ALife planner’s SMART_TERRAIN_TASK evaluator and action.
  2. EvaluatorReachedTaskLocation returns true only when the NPC’s squad is doing REACH_TARGET and the assigned simulation target still reports not reached.
  3. SchemeReachTask.add subscribes the existing nested SMART_TERRAIN_TASK action for scheme events.
  4. The action sends the commander toward the assigned target and keeps followers in formation.

Example

[logic]
active = walker@idle

[walker@idle]
path_walk = idle_walk
path_look = idle_look

reach_task is driven by squad simulation state. It is not normally configured with a dedicated [reach_task] section.

Notes

  • Movement switches between game-path and level-path movement depending on whether the commander is on the target game vertex.
  • During surge, reach-task movement uses running with free mental state.
  • Debug this through squad simulation state first; there is normally no [reach_task] LTX section to inspect.

remark

remark plays a short scripted stalker animation, optionally aimed at a target, with optional sound and completion signals. Use it for scenario beats, one-off gestures, directed looks, and transitions between scripted sections.

Parameters

anim

Type: condlist. Optional. Default: wait.

Animation state selected by condlist when the remark starts.

snd

Type: string. Optional. Default: null.

Sound played by the sound manager after the animation when sound is scheduled.

snd_anim_sync

Type: boolean. Optional. Default: false.

Controls whether sound is scheduled independently from the animation.

target

Type: string. Optional. Default: nil.

Optional look target descriptor.

tips

Type: string. Optional. Default: null.

Tip id stored by the scheme.

tips_sender

Type: string. Optional. Default: null.

Sender id read only when tips is set.

The section also supports common switch fields. remark is commonly paired with on_signal = action_end | ... or on_signal = anim_end | ....

Target behavior

target supports three descriptor forms:

story | actor or story | story_id

Looks at the object resolved by story id.

path | patrol_path,point_id

Looks at the selected patrol point.

job | job_section,smart_name

Looks at the object assigned to a smart terrain job.

When target = nil, the animation runs without a target descriptor.

Signals

The remark action sets signals on the active scheme state:

anim_end

The animation callback reaches the sound stage.

action_end

Both animation end and sound end were observed.

The action also observes sound_end and theme_end signals. Those can be set by sound handling code to allow action_end.

Example

[logic]
active = remark@look_actor

[remark@look_actor]
anim = threat_na
target = story|actor
snd = meet_hide_weapon
on_signal = anim_end | walker@guard

Notes

  • Invalid target descriptors abort with a config error.
  • anim is a condlist, so it can select different animation states by info portions or conditions.
  • The planner blocks normal ALife while the remark section is active.

sleeper

sleeper moves a stalker to a sleeping patrol point and then puts the stalker into a sleeping or sitting state. Use it for beds, camp sleep spots, and scripted rest positions.

Parameters

path_main

Type: string. Required. Default: none.

Patrol path used to derive both walking and look data. Relative names are resolved against the active smart terrain.

wakeable

Type: boolean. Optional. Default: false.

Uses the sitting state instead of the sleeping state when the NPC reaches the final point.

The section also supports common switch fields such as on_info, on_timer, and on_signal.

Behavior

SchemeSleeper.add installs a planner evaluator and SLEEP_ACTIVITY action. The action runs only when the stalker is alive and not in danger, combat, or anomaly handling.

On action initialization, desired position and direction are cleared, then the action builds walk/look data from path_main and starts the patrol manager. Reaching the final patrol point switches the stalker to sleep or sit.

Patrol shape

path_main must contain either one or two waypoints.

1

The NPC walks to the single point and then sleeps there.

2

The NPC walks using the main path and looks toward the second point when entering the final state.

Any other waypoint count aborts with a config error.

For a two-point path, the second point is also used as the look position while entering the final state.

Example

[logic]
active = sleeper@bed

[sleeper@bed]
path_main = sleep_place
wakeable = false
on_info = {+alarm_started} walker@wake_up

[walker@wake_up]
path_walk = wake_up_walk

Notes

  • path_main is required and must exist as a patrol path.
  • wakeable = true currently maps to sit; wakeable = false maps to sleep.
  • The action builds internal path_walk and path_look data from path_main; those are not user-facing fields.
  • Use common switch fields such as on_info to wake or redirect the NPC when an alarm or quest state changes.

smartcover

smartcover makes a stalker use a registered smart cover and update the cover target state while the section is active. Use it for scripted cover positions, lookout points, and controlled firing from cover.

Parameters

cover_name

Type: string. Optional. Default: $script_id$_cover.

Registered smart cover name.

loophole_name

Type: string. Optional. Default: null.

Loophole name stored by the scheme.

cover_state

Type: condlist string. Optional. Default: default_behaviour.

Smart cover state selected each update.

target_enemy

Type: story id. Optional. Default: null.

Story id of the enemy object to target.

target_path

Type: condlist string. Optional. Default: nil.

Condlist selecting a patrol path whose first point becomes the fire target.

idle_min_time

Type: number. Optional. Default: 6.

Minimum idle time passed to the game object.

idle_max_time

Type: number. Optional. Default: 10.

Maximum idle time passed to the game object.

lookout_min_time

Type: number. Optional. Default: 6.

Minimum lookout time passed to the game object.

lookout_max_time

Type: number. Optional. Default: 10.

Maximum lookout time passed to the game object.

exit_body_state

Type: string. Optional. Default: stand.

Exit body state stored by the scheme.

use_precalc_cover

Type: boolean. Optional. Default: false.

Stored by the scheme for cover selection compatibility.

use_in_combat

Type: boolean. Optional. Default: false.

Allows the combat evaluator to permit smart cover use in combat.

weapon_type

Type: string. Optional. Default: null.

Weapon type stored by the scheme.

def_state_moving

Type: stalker state. Optional. Default: sneak.

Movement state stored by the scheme.

sound_idle

Type: string. Optional. Default: null.

Sound played while the smart cover action executes.

The section also supports common switch fields such as on_info, on_timer, and on_signal.

Cover state

cover_state is parsed as a condlist. The selected value is used to choose smart cover target behavior.

idle_target

Calls idle target mode.

lookout_target

Updates target and calls lookout target mode.

fire_target

Calls fire target mode.

fire_no_lookout_target

Updates target and calls fire-without-lookout mode.

default_behaviour or other values

Updates target and uses default target mode.

nil

Clears the target selector.

When target_path selects a patrol path, the first point of that path becomes the smart cover target. If no path is selected, the action can target target_enemy by story id. A stored target position is also supported by the action, but the current scheme parser does not read a config field for it.

Signals

When target_enemy is set and the stalker is in smart cover, the action updates:

enemy_in_fov

Target enemy is in the current loophole field of view.

enemy_not_in_fov

Target enemy is not in the current loophole field of view.

Example

[logic]
active = smartcover@post

[smartcover@post]
cover_name = esc_guard_cover
cover_state = {+alarm_started} fire_target, lookout_target
target_path = esc_guard_fire_point
idle_min_time = 4
idle_max_time = 8
sound_idle = state
on_signal = enemy_in_fov | camper@fire

Notes

  • cover_name must exist in the smart cover registry when the action initializes.
  • target_path must resolve to an existing patrol path when it is selected.
  • The planner blocks normal ALife while smart cover is needed.

sr_crow_spawner

sr_crow_spawner periodically spawns crow server objects at configured patrol paths while the total crow count on the level is below a limit.

Use one active crow spawner per level unless the level intentionally needs multiple independent spawn sets.

Parameters

max_crows_on_level

Type: number. Optional. Default: 16.

Maximum allowed registry.crows.count before spawning is throttled.

spawn_path

Type: comma-separated strings. Optional. Default: empty string.

Patrol paths considered as crow spawn points.

The section also supports common switch fields.

Behavior

On activation, the manager initializes a cooldown entry for each spawn path. On update, if enough time has passed and the current crow count is below the configured maximum, it tries the paths in random order.

A path can spawn a crow when:

  • its cooldown has elapsed;
  • its first patrol point is farther than 100 units from the actor.

The spawned server object section is m_crow. After a spawn, the selected path is put on a 10-second cooldown.

Runtime sequence

  1. Activation reads common switch fields, max_crows_on_level, and spawn_path.
  2. Manager activation initializes each path cooldown to 0.
  3. On update, the manager checks the global crow count and update throttle.
  4. Paths are copied and tried in random order.
  5. A valid path creates m_crow at patrol point 0 with that point’s level and game vertex ids.
  6. Common switch fields are checked after the spawn attempt.

Example

[logic]
active = sr_crow_spawner

[sr_crow_spawner]
max_crows_on_level = 7
spawn_path = zat_crow_spawn_1, zat_crow_spawn_2, zat_crow_spawn_3

Notes

  • spawn_path is parsed as a comma-separated list.
  • If the crow count is already at the limit, the manager waits for the crow update throttle.
  • Each path uses point 0 as the spawn position.
  • Keep spawn points away from the actor; paths within 100 units are skipped.

sr_cutscene

sr_cutscene teleports the actor to a point/look pair, disables game UI, and plays one or more camera effectors. Use it for scripted first-person scenes and controlled transitions where player input should be temporarily blocked.

Parameters

point

Type: string. Required. Default: none.

Patrol path used by the teleport effect as actor position.

look

Type: string. Required. Default: none.

Patrol path used by the teleport effect as actor look target.

global_cameffect

Type: boolean. Optional. Default: false.

Marks generated camera effects as global.

pp_effector

Type: string. Optional. Default: nil.

Postprocess effector name without .ppe. The parser appends .ppe.

cam_effector

Type: comma-separated strings. Required. Default: none.

Camera effectors or named effector sets played in order.

fov

Type: number. Optional. Default: null.

Field of view stored in the scheme state.

enable_ui_on_end

Type: boolean. Optional. Default: true.

Re-enables game UI/input at the end when possible.

outdoor

Type: boolean. Optional. Default: false.

Adds a brighten complex effector for outdoor night cutscenes.

The section also supports common switch fields.

Behavior

On activation, the manager:

  1. teleports the actor using xr_effects.teleport_actor(point, look);
  2. starts the configured postprocess when it is not nil;
  3. disables game UI;
  4. optionally starts a brighten effector for outdoor night scenes;
  5. starts the first configured camera effector or effector set.

Camera progression uses scheme signals. When cam_effector_stop is present, the current motion stops and the manager advances. After the final motion, the manager sets cameff_end.

Example

[logic]
active = sr_cutscene@intro

[sr_cutscene@intro]
point = intro_actor_point
look = intro_actor_look
cam_effector = intro_camera_1, intro_camera_2
pp_effector = fade_in
enable_ui_on_end = true
on_signal = cameff_end | sr_idle@done

Notes

  • cam_effector is parsed as a comma-separated list.
  • pp_effector = nil becomes the nil postprocess constant and is skipped.
  • The manager stores the active cutscene object and state in cutscene config while running.

sr_deimos

sr_deimos drives a disorientation effect based on actor movement speed. It ramps intensity, starts postprocess and looped sounds, can play repeated camera effects, and drains actor health when intensity is high.

Parameters

movement_speed

Type: number. Optional. Default: 100.

Target movement speed used to calculate intensity delta.

growing_rate

Type: number. Optional. Default: 0.1.

Multiplier used when intensity is increasing.

lowering_rate

Type: number. Optional. Default: growing_rate.

Multiplier used when intensity is decreasing.

pp_effector

Type: string. Required. Default: none.

Primary postprocess effector name without .ppe.

pp_effector2

Type: string. Required. Default: none.

Secondary postprocess effector name without .ppe.

cam_effector

Type: string. Required. Default: none.

Camera effector animation name without path or extension.

cam_effector_repeating_time

Type: number. Optional. Default: 10.

Seconds between repeated camera effects. Stored as milliseconds.

noise_sound

Type: string. Required. Default: none.

Looped noise sound id.

heartbeat_sound

Type: string. Required. Default: none.

Looped heartbeat sound id.

health_lost

Type: number. Optional. Default: 0.01.

Health amount subtracted when the high-intensity camera effect triggers.

disable_bound

Type: number. Optional. Default: 0.1.

Intensity below which phase effects are stopped.

switch_lower_bound

Type: number. Optional. Default: 0.5.

Intensity where heartbeat phase starts or stops.

switch_upper_bound

Type: number. Optional. Default: 0.75.

Intensity where camera and secondary postprocess can trigger.

The section also supports common switch fields.

Behavior

The manager compares movement_speed with the actor’s current movement speed and adjusts intensity between 0 and 1. As thresholds are crossed it starts or stops:

  • primary postprocess and noise sound;
  • heartbeat sound;
  • camera effector and secondary postprocess.

When intensity rises above switch_upper_bound, the manager may replay the camera effect after cam_effector_repeating_time and subtract health_lost from actor health.

When the section switches away, the manager resets related effectors and looped sounds.

Example

[logic]
active = sr_deimos@horror

[sr_deimos@horror]
pp_effector = deimos
pp_effector2 = deimos_flash
cam_effector = deimos_camera
noise_sound = deimos_noise
heartbeat_sound = deimos_heartbeat
movement_speed = 80
switch_upper_bound = 0.75
on_info = {+scene_finished} sr_idle@done

Notes

  • cam_effector_repeating_time is configured in seconds and converted to milliseconds.
  • The manager skips updates while the screen is black.
  • The core DeimosManager persists the active intensity and provides it to the next active Deimos controller after a load.

sr_idle

sr_idle is a restrictor scheme that waits and checks switch conditions. It does not run its own effect, movement, UI, or actor interaction behavior.

Use it as a neutral trigger state when a space restrictor should wait for info portions, timers, actor entry, actor exit, or other common switch conditions.

Parameters

sr_idle has no scheme-specific parameters. It reads only common switch fields from the section.

on_info, on_info1, …

Type: condlist.

Switch when the condlist selects another section.

on_timer, on_timer1, …

Type: milliseconds | condlist.

Switch after the section has been active for the duration.

on_game_timer, on_game_timer1, …

Type: seconds | condlist.

Switch after the section has been active for the game-time duration.

on_actor_inside

Type: condlist.

Switch while the actor is inside the current restrictor.

on_actor_outside

Type: condlist.

Switch while the actor is outside the current restrictor.

on_actor_in_zone

Type: zone | condlist.

Switch while the actor is inside another named zone.

on_actor_not_in_zone

Type: zone | condlist.

Switch while the actor is outside another named zone.

on_npc_in_zone

Type: story_id | zone | condlist.

Switch while the named NPC is inside the named zone.

on_npc_not_in_zone

Type: story_id | zone | condlist.

Switch while the named NPC is outside the named zone.

Behavior

Use sr_idle when the restrictor is only a condition gate:

  • wait until the actor enters a volume;
  • wait until an info portion is set;
  • call an effect through a condlist and then switch;
  • hold a trigger in a disabled state until another section enables it.

Because sr_idle checks conditions every update, avoid condlists that repeatedly return the same active section while running effects. Effects in a matching condlist can run every update if the switch itself does not move to another section.

Example

[logic]
active = sr_idle@wait

[sr_idle@wait]
on_actor_inside = sr_idle@inside %=play_sound(alarm_start)%

[sr_idle@inside]
on_actor_outside = sr_idle@wait
on_timer = 10000 | sr_idle@done %+actor_stayed_in_zone%

[sr_idle@done]

The first section waits for the actor to enter the restrictor. The second section waits for either actor exit or a 10-second timer.

sr_light

sr_light registers a restrictor as a light-control zone for stalkers. Other systems can query active light zones to decide whether a stalker’s torch should be on or off while the stalker is inside the zone.

Parameters

light_on

Type: boolean. Optional. Default: false.

Light flag returned when a stalker is inside the active zone.

The section also supports common switch fields such as on_info, on_timer, and actor-zone checks.

Behavior

On activation, the manager registers itself in registry.lightZones. On update, it checks common switch conditions. If a switch happens, the manager marks itself inactive and removes the zone from the registry. Otherwise it remains active.

The manager’s checkStalker helper returns two booleans:

  • the configured light_on value;
  • whether the checked stalker is inside this active restrictor zone.

Runtime sequence

  1. SchemeLight.activate reads common switch fields and light_on.
  2. SchemeLight.add subscribes LightManager.
  3. LightManager.activate registers the manager in registry.lightZones by restrictor object id.
  4. Each update tries common section switching.
  5. If switching happens, the manager marks itself inactive and removes the zone from registry.lightZones.
  6. Otherwise it marks itself active and can answer checkStalker(...).

Example

[logic]
active = sr_light@underground

[sr_light@underground]
light_on = true
on_info = {+lab_power_restored} sr_light@off

[sr_light@off]
light_on = false

Notes

  • The scheme reset clears all registered light zones.
  • Deactivation does not currently unregister the zone directly; updates and reset handle registry cleanup.
  • checkStalker returns (false, false) when the manager is inactive or the stalker is outside the restrictor.

sr_monster

sr_monster stages a monster ambush from a restrictor. While the actor is inside the zone, it moves a warning sound source along a patrol path. When the path wraps, it spawns a monster and commands it to run to the path endpoint.

Parameters

snd

Type: string. Optional. Default: null.

Sound id played as the moving warning sound source.

delay

Type: number. Optional. Default: 0.

Parsed and stored; current manager does not use it directly.

idle

Type: number. Optional. Default: 30.

Idle duration after the ambush finishes. The parser multiplies this value by 10000, so one configured unit becomes ten seconds of game-time delay.

sound_path

Type: string list. Optional. Default: null.

Patrol paths used by the moving warning sound. One path is selected at a time.

monster_section

Type: string. Optional. Default: null.

Server object section spawned when the path wraps.

slide_velocity

Type: number. Optional. Default: 7.

Speed for sliding the warning sound position along the path.

The section also supports common switch fields.

Behavior

When the actor enters the restrictor, the manager selects a path from sound_path and starts sliding a sound position from point to point. When the selected path wraps back to its start:

  1. it spawns monster_section at the current sound position;
  2. it plays the hard-coded appear sound monsters\boar\boar_swamp_appear_1;
  3. it captures the spawned monster when it comes online;
  4. it commands the monster to run to the final point of the current path;
  5. after the monster reaches the final point, it releases and removes the server object;
  6. it enters idle state until idleEnd.

Example

[logic]
active = sr_monster@ambush

[sr_monster@ambush]
snd = monsters_boar_boar_swamp_appear_1
sound_path = ambush_sound_path_1, ambush_sound_path_2
monster_section = boar_normal
slide_velocity = 7
idle = 30

Notes

  • sound_path should contain patrol paths with enough points for the sound slide and final run target.
  • With multiple paths, the manager avoids immediately selecting the same path again.
  • The implementation currently stores delay but does not apply it in MonsterManager.

sr_no_weapon

sr_no_weapon tracks whether the actor is inside a restrictor where weapons should be disabled. It emits enter and leave events and records the zone in registry.noWeaponZones.

Use it for safe areas, story spaces, and bases where the actor should not be able to keep a weapon raised.

Parameters

sr_no_weapon has no scheme-specific fields. It reads common switch fields from the section.

on_info, on_info1, …

Type: condlist.

Switch when the condlist selects another section.

on_timer, on_timer1, …

Type: milliseconds | condlist.

Switch after the section has been active for the duration.

on_actor_inside

Type: condlist.

Switch while the actor is inside the current restrictor.

on_actor_outside

Type: condlist.

Switch while the actor is outside the current restrictor.

Behavior

On activation, the manager removes the zone’s previous registry entry, resets its local actor state, and immediately checks whether the actor is inside the restrictor.

When the actor enters the zone, it:

  • sets registry.noWeaponZones[zone_id] = true;
  • emits ACTOR_ENTER_NO_WEAPON_ZONE.

When the actor leaves the zone, it:

  • sets registry.noWeaponZones[zone_id] = false;
  • emits ACTOR_LEAVE_NO_WEAPON_ZONE.

If a section switch happens while the actor is inside, the manager emits the leave path before switching away.

Example

[logic]
active = sr_no_weapon@base

[sr_no_weapon@base]
on_info = {+base_alarm} sr_idle@disabled

Notes

  • The actual weapon hiding/UI behavior is handled by systems listening to the registry/events, not by this manager.
  • Use sr_idle or another section when the no-weapon zone should be disabled.

sr_particle

sr_particle plays particle effects from a restrictor section. It supports a simple path-following particle and a complex mode that plays one particle instance per patrol point.

Parameters

name

Type: string. Required. Default: none.

Particle effect name passed to particles_object.

path

Type: string. Required. Default: none.

Patrol path used by the particle effect. Must not be empty.

mode

Type: 1 or 2. Required. Default: none.

Particle behavior mode. 1 is simple, 2 is complex.

looped

Type: boolean. Optional. Default: false.

Restarts playback when the particle is not playing.

The section also supports common switch fields such as on_info, on_timer, and on_signal.

Particle modes

1

Creates one particle object, loads path, starts path playback, and plays it.

2

Creates one particle object per patrol point and plays each particle at its point after the waypoint delay.

Complex mode reads waypoint key d as delay in milliseconds. Waypoint sound key s is currently a development trap and aborts if present.

Signals

For non-looped particles, the manager sets particle_end after playback has started and all particle objects have stopped.

[logic]
active = sr_particle@steam

[sr_particle@steam]
name = anomaly2\steam
path = steam_path
mode = 1
looped = false
on_signal = particle_end | sr_idle@done

Notes

  • mode accepts only 1 and 2.
  • Deactivation stops all playing particle objects.
  • Updates are throttled by the particle scheme update period.

sr_postprocess

sr_postprocess applies a gray/noise postprocess effect while the actor is inside a restrictor and applies periodic radiation and shock hits.

Use it for hazardous visual zones where the actor should see an effect and take damage while inside.

Parameters

intensity

Type: number. Required. Default: none.

Target postprocess intensity. The value is multiplied by 0.01.

intensity_speed

Type: number. Required. Default: none.

Ramp speed for entering and leaving the zone. The value is multiplied by 0.01.

hit_intensity

Type: number. Required. Default: none.

Damage accumulation rate while the actor is inside.

The section also supports common switch fields such as on_info, on_timer, and actor-zone checks.

Behavior

On activation, the manager starts a postprocess effector with id object.id() + 2000. Each update:

  • checks common switch conditions first;
  • tests whether the actor is inside the restrictor;
  • ramps intensity toward the target when inside and back toward zero when outside;
  • updates gray color and noise parameters;
  • accumulates hit power while inside;
  • once per second, applies radiation and shock hits to the actor.

intensity and intensity_speed are converted from percent-style values by multiplying by 0.01. hit_intensity is used directly as the per-second accumulation rate.

Example

[logic]
active = sr_postprocess@hazard

[sr_postprocess@hazard]
intensity = 40
intensity_speed = 8
hit_intensity = 0.02
on_actor_outside = sr_idle@cooldown

Notes

  • intensity and intensity_speed are percent-style config values.
  • Deactivation is not implemented in the current manager and aborts if called.
  • The hit direction is zero and impulse is 0.
  • Use a switch to a non-postprocess section when the restrictor should stop controlling the effect.

sr_psy_antenna

sr_psy_antenna applies psy-zone effects while the actor is inside a restrictor. It adjusts the shared PsyAntennaManager, enables fake HUD indicators, optionally starts a postprocess effector, and restores the manager values when the actor leaves.

Parameters

eff_intensity

Type: number. Required. Default: none.

Sound/postprocess intensity. Multiplied by 0.01.

postprocess

Type: string. Optional. Default: psy_antenna.

Postprocess effector name. Use nil to skip adding an effector.

hit_intensity

Type: number. Required. Default: none.

Hit intensity added to the psy antenna manager. Multiplied by 0.01.

phantom_prob

Type: number. Optional. Default: 0.

Phantom spawn probability. Multiplied by 0.01.

mute_sound_threshold

Type: number. Optional. Default: 0.

Added to the manager mute threshold while inside.

no_static

Type: boolean. Optional. Default: false.

Sets the manager noStatic flag.

no_mumble

Type: boolean. Optional. Default: false.

Sets the manager noMumble flag.

hit_type

Type: string. Optional. Default: wound.

Hit type used by the manager.

hit_freq

Type: number. Optional. Default: 5000.

Hit frequency used by the manager.

The section also supports common switch fields.

Behavior

When the actor enters the zone, the manager:

  • enables fake HUD indicators;
  • adds intensity, hit intensity, mute threshold, and phantom probability to the shared psy manager;
  • copies no_static, no_mumble, hit_type, and hit_freq to the shared manager;
  • starts the configured postprocess if it is not nil.

When the actor leaves or the scheme deactivates, those additive values are subtracted and fake indicators are disabled.

The manager saves its inside/outside state in portable storage under key inside.

Example

[logic]
active = sr_psy_antenna@lab

[sr_psy_antenna@lab]
eff_intensity = 60
hit_intensity = 10
phantom_prob = 5
mute_sound_threshold = 0.2
postprocess = psy_antenna
hit_freq = 3000
on_actor_outside = sr_idle@outside

Notes

  • Percent-style fields are multiplied by 0.01.
  • Multiple active psy antenna zones add to the shared manager values.
  • postprocess = nil disables postprocess creation for this zone.

sr_silence

sr_silence marks a restrictor as a silence zone by registering it in registry.silenceZones. It is used by other systems to suppress dynamic music, usually in safe places.

Use it for restrictor volumes around bases or scripted quiet areas. It marks the zone; the music behavior comes from systems that read the registry entry.

Parameters

sr_silence has no scheme-specific fields. It reads common switch fields from the section.

on_info, on_info1, …

Type: condlist.

Switch when the condlist selects another section.

on_timer, on_timer1, …

Type: milliseconds | condlist.

Switch after the section has been active for the duration.

Behavior

On activation, the scheme stores the restrictor id and name in registry.silenceZones.

The manager itself is empty in the current implementation. The source notes that deactivation behavior may be missing. Use another section to control logic flow, but do not rely on this manager to unregister itself.

Because registration happens during activation, keep the active section stable for areas that should remain quiet. If a scenario needs temporary silence, verify the consumer of registry.silenceZones before relying on a section switch to remove the effect.

Example

[logic]
active = sr_silence@base

[sr_silence@base]
on_info = {+base_alarm} sr_idle@disabled

Notes

  • This scheme does not currently implement update or deactivation behavior.
  • Music suppression is handled by systems that read registry.silenceZones.
  • The stored value is the restrictor name keyed by object id.

sr_teleport

sr_teleport teleports the actor after the actor enters a restrictor and a timeout elapses. It can choose from up to ten weighted destination/look pairs.

Parameters

timeout

Type: number. Optional. Default: 900.

Delay in milliseconds between actor entry and teleport.

point1point10

Type: string. Required: at least one pair. Default: none.

Patrol path whose first point is the teleport position.

look1look10

Type: string. Required: at least one pair. Default: none.

Patrol path whose first point defines look direction after teleport.

prob1prob10

Type: number. Optional. Default: 100.

Weight for the matching point/look pair.

The section also supports common switch fields. They are checked after teleport processing when the manager is idle.

Behavior

When the actor enters the restrictor, the manager:

  1. switches from idle to activated state;
  2. starts the teleport postprocess effector;
  3. waits timeout milliseconds;
  4. chooses a destination by subtracting weights from a random value in the total probability range;
  5. teleports the actor to pointN[0] and looks toward lookN[0] - pointN[0];
  6. returns to idle state.

The parser stops reading destination pairs when it finds pointN = none or lookN = none.

Example

[logic]
active = sr_teleport@burnt_farm

[sr_teleport@burnt_farm]
timeout = 1000
point1 = teleport_walk_a
look1 = teleport_look_a
prob1 = 25
point2 = teleport_walk_b
look2 = teleport_look_b
prob2 = 75

Notes

  • At least one complete pointN and lookN pair is required.
  • probN is a weight, not a normalized percentage.
  • The teleport triggers again if the actor remains or re-enters after the manager returns to idle.

sr_timer

sr_timer shows a HUD timer and switches sections when the timer reaches a configured value. Use it for visible countdowns, elapsed-time displays, evacuation limits, laboratory timers, or mission windows where the player should see time passing.

The scheme is a restrictor scheme. When activated, it adds the configured HUD static. When deactivated, it removes the timer static and optional label static.

Parameters

type

Type: inc or dec. Optional. Default: inc.

Timer mode. inc counts up from start_value; dec counts down from start_value.

start_value

Type: number. Required: required for dec. Default: 0 for inc.

Starting time in milliseconds.

on_value

Type: number | condlist. Optional. Default: null.

Switch when the timer reaches the value. For dec, the switch happens at or below the value. For inc, it happens at or above the value.

timer_id

Type: string. Optional. Default: hud_timer.

HUD custom static id used for the timer text.

string

Type: string id. Optional. Default: null.

Optional text string shown in hud_timer_text.

The section also checks common switch fields before updating the timer. If a common switch succeeds, the timer update for that tick is skipped.

Behavior

Use type = dec for deadlines and visible countdowns. It requires start_value.

Use type = inc for elapsed-time displays. start_value is optional and defaults to 0.

on_value is separate from on_timer. on_timer switches after section activation time; on_value switches when the displayed timer value crosses the configured threshold.

Example

[logic]
active = sr_timer@countdown

[sr_timer@countdown]
type = dec
start_value = 60000
timer_id = hud_timer
string = st_lab_countdown
on_value = 0 | sr_idle@failed %+lab_timer_failed%
on_info = {+lab_shutdown_complete} sr_idle@done

[sr_idle@done]

[sr_idle@failed]

This section starts a 60-second countdown. It switches to sr_idle@failed when the displayed value reaches zero, unless lab_shutdown_complete switches it to sr_idle@done first.

Notes

  • Timer values are milliseconds.
  • The displayed value is clamped at zero.
  • type accepts only inc and dec.
  • Decrement timers without start_value are invalid.

walker

walker makes a stalker follow a patrol path while no higher-priority planner state is active. Use it for guards, ambient movement, scripted walks, and simple station-keeping behavior.

The scheme is a stalker scheme. It adds a walker planner action that runs only while the NPC is alive and not in danger, combat, anomaly handling, wounded handling, corpse search, item gathering, or abuse reactions.

Parameters

path_walk

Type: string. Required. Default: none.

Patrol path used for movement. Relative names are resolved against the active smart terrain.

path_look

Type: string. Optional. Default: null.

Patrol path used for look points. It must not equal path_walk.

team

Type: string. Optional. Default: null.

Patrol team name passed to the stalker patrol manager. Relative names are resolved against the active smart terrain.

sound_idle

Type: string. Optional. Default: null.

Sound played by the sound manager while the NPC is not in a camp zone.

use_camp

Type: boolean. Optional. Default: false.

Allows the NPC to register in a camp story manager when standing inside a camp zone.

def_state_standing

Type: string. Optional. Default: null.

Suggested standing animation state.

def_state_moving

Type: string. Optional. Default: def_state_moving1.

Suggested moving animation state.

def_state_moving1

Type: string. Optional. Default: null.

Compatibility fallback for def_state_moving.

The section also supports common switch fields such as on_info, on_signal, on_timer, and actor-distance checks.

Behavior

Use walker when one NPC owns its own patrol. Use patrol instead when several squad members should share a commander and follow a formation.

The movement path is parsed the first time the action runs. If path_look is present, look waypoints are parsed too. Waypoint flags and signals are handled by the shared stalker patrol manager.

If use_camp = true, the walker action checks whether the NPC is inside a camp zone each update. Inside a camp, the NPC registers with the camp manager; outside a camp, sound_idle can play.

Example

[logic]
active = walker@guard

[walker@guard]
path_walk = guard_walk
path_look = guard_look
sound_idle = state
def_state_standing = guard
def_state_moving = walk
on_info = {+zat_b40_alarm} walker@alarm

[walker@alarm]
path_walk = alarm_walk
def_state_moving = run
on_timer = 15000 | walker@guard

In a smart terrain named zat_b40_smart_terrain, the first section resolves guard_walk to zat_b40_smart_terrain_guard_walk.

Notes

  • path_walk must exist as a level patrol path.
  • path_look cannot be the same as path_walk.
  • The scheme does not force combat behavior. Combat, danger, wounded, and other generic schemes can interrupt it.

wounded

wounded captures a stalker when health or psy-health reaches configured breakpoints. The NPC falls into a wounded state, calls for help, can be helped by another stalker, and can auto-heal after a timeout.

Parameters

The active logic section can point to a wounded configuration section:

wounded

Location: active section or [logic] fallback. Type: section name. Optional.

Section used to configure wounded behavior. nil uses community defaults.

The wounded configuration section supports:

hp_state

Type: wounded data. Optional. Default: community default.

State/sound descriptor for HP wounds when the actor is not seen.

hp_state_see

Type: wounded data. Optional. Default: community default.

State/sound descriptor for HP wounds when the actor is seen.

psy_state

Type: wounded data. Optional. Default: community default.

State/sound descriptor for psy-health wounds.

hp_victim

Type: wounded data. Optional. Default: community default.

Victim descriptor stored in portable state.

hp_cover

Type: wounded data. Optional. Default: community default.

Parsed into state. The inspected manager does not currently store its processed result.

hp_fight

Type: wounded data. Optional. Default: community default.

Controls whether the NPC can keep fighting while wounded.

help_dialog

Type: string. Optional. Default: community default.

Dialog used for wounded help interaction.

help_start_dialog

Type: string. Optional. Default: null.

Start dialog set when the NPC becomes wounded.

use_medkit

Type: boolean. Optional. Default: community default.

Allows medkit use after help or auto-heal unlocks it.

autoheal

Type: boolean. Optional. Default: true.

Allows automatic medkit unlock after wounded timeout.

enable_talk

Type: boolean. Optional. Default: true.

Stores whether talking is enabled while wounded.

not_for_help

Type: boolean. Optional. Default: community default.

Marks the wounded object as not suitable for helper NPCs.

Wounded Data Syntax

Wounded data is parsed as repeated descriptors:

hp|state_condlist@sound_condlist|hp|state_condlist@sound_condlist

Examples:

hp_state = 20|wounded_heavy_2@help_heavy
hp_fight = 20|false
psy_state = 20|{=best_pistol}psy_armed,psy_pain@wounded_psy

Each descriptor has:

hp

Breakpoint compared with current HP or psy-health in the 0..100 range.

state_condlist

Condlist resolved to the stalker state or special value.

sound_condlist

Optional condlist after @, resolved to the sound name.

The parser selects the last descriptor whose breakpoint is greater than or equal to the current value before a higher unmatched breakpoint stops the scan.

Behavior

On reset, the scheme resolves the wounded config section and parses the descriptor fields. Defaults differ for normal, monolith, and zombied communities. Normal stalkers default to a wounded-heavy state with help sounds and medkit use. Monolith and zombied defaults disable outside help.

The wound manager recalculates state on update and hit. Psy wounds are checked first. If no psy wound applies, HP wound state and sound are selected from hp_state or hp_state_see depending on whether the NPC sees the actor. Fight and victim results are stored in portable state.

When the wounded action starts, the NPC stops moving, disables trade, sets wounded state, registers as a wounded object, and begins calling for help after the configured delay. If auto-heal is enabled and no helper unlocks medkit use, the manager unlocks medkit use after the wounded timeout.

Example

[logic]
active = walker@guard

[walker@guard]
path_walk = guard_walk
path_look = guard_look
wounded = wounded@guard

[wounded@guard]
hp_state = 25|wounded_heavy_2@help_heavy
hp_state_see = 25|wounded_heavy_3@help_heavy
hp_fight = 25|false
hp_victim = 25|nil
help_dialog = dm_help_wounded_medkit_dialog
use_medkit = true
autoheal = true
not_for_help = false

Notes

  • Wounded timing defaults are loaded from schemes\wounded.ltx: call delay 5000, call period 5000, wounded timeout 60000 if the config file does not override them.
  • If the resolved wounded state is nil while the action is executing, the implementation aborts with a wrong wounded animation error.

Scripts

The script engine is the TypeScript runtime layer that is compiled to Lua and loaded by the X-Ray engine. It owns the Lua extern modules, object binders, scheme registry, global managers, server object classes, and shared helpers used by configs and gameplay logic.

For object lifecycle, manager startup, events, and save/load, start with Runtime lifecycle. This page is the lower-level source map for script modules.

The main source roots are:

AreaSource
Lua entry pointssrc/engine/scripts
Runtime binderssrc/engine/core/binders
Global managerssrc/engine/core/managers
Runtime registrysrc/engine/core/database
Scheme implementationssrc/engine/core/schemes
Server object classessrc/engine/core/objects
Shared helperssrc/engine/core/utils
UI classessrc/engine/core/ui
Animation tablessrc/engine/core/animation

Entry points

The engine-facing entry points are registered with extern(...).

_g

Loads global runtime declarations. This is the global bridge used before other modules are available.

register

Exposes class-id and class registration helpers:

  • register.registerGameClasses
  • register.getGameClassId
  • register.getUiClassId

The X-Ray engine calls these functions while linking script classes to engine class ids.

bind

Exposes binder factories such as bind.actor, bind.stalker, bind.restrictor, bind.weapon, and bind.smart_terrain. Each function receives a game object and attaches the matching object_binder implementation.

Some binders are conditional. For example, helicopters and physical objects are bound only when their spawn ini or object kind needs script logic.

start

Runs the game-start callback. It updates class ids, registers the simulator and ranks, unlocks system ini overriding, registers managers, registers schemes, registers extensions, and emits GAME_STARTED.

Runtime shape

Most gameplay code is not called directly from config files. The usual path is:

  1. X-Ray loads script entry modules.
  2. start.callback initializes shared runtime systems.
  3. X-Ray creates online objects and calls a bind.* function.
  4. The binder registers the object in registry.
  5. The binder initializes object logic from spawn ini or generated config.
  6. Schemes, managers, and events update behavior on object or actor ticks.

Server-side classes such as squads and smart terrains participate through on_register, on_unregister, STATE_Write, and STATE_Read.

What to edit

  • Add new engine callbacks under src/engine/scripts/declarations.
  • Add new object lifecycle behavior under src/engine/core/binders.
  • Add cross-object systems under src/engine/core/managers.
  • Add runtime object registries under src/engine/core/database only when the state is shared across modules.
  • Add shared helpers under src/engine/core/utils when they are stateless or narrowly scoped.

Validation

For script runtime changes, start with focused tests near the changed module. Use broader checks when touching shared runtime contracts:

npm test -- src/engine/scripts
npm test -- src/engine/core/database
npm run typecheck

Animations

Animation scripts define named stalker states, animation sequences, smart-cover descriptors, and helper functions used by schemes such as walker, remark, animpoint, camper, corpse_detection, and help_wounded.

Source layout

SourcePurpose
src/engine/core/animation/typesState, animation, and patrol descriptor types
src/engine/core/animation/statesStalker state descriptors such as movement, mental state, weapon mode, body state
src/engine/core/animation/animationsAnimation sequence tables and callbacks
src/engine/core/animation/animstatesAdditional animation-state mappings
src/engine/core/animation/smart_coversSmart-cover and loophole animation descriptors
src/engine/core/animation/predicatesPredicate lists used to select animpoint-compatible animations
src/engine/core/utils/animation.tsHelpers for sequence construction

State descriptors

State descriptors map a script state name to engine animation inputs. The base table defines states such as walk, run, patrol, assault, threat, hide, search_corpse, help_wounded, and wounded states.

A descriptor can set:

  • weapon animation mode, such as strapped, unstrapped, fire, drop, or none;
  • movement mode, such as stand, walk, or run;
  • mental state, such as free, danger, or panic;
  • body state, such as standing or crouch;
  • animation state or concrete animation name;
  • sight direction behavior;
  • force flags used by state managers.

Schemes usually pass a state name to setStalkerState. The state manager resolves that name through these tables and applies the engine-level settings.

Animation sequences

Animation descriptors define into, idle, rnd, and out sequences. The createSequence(...) helper stores the sequence as a Lua table using zero-based indexes, which matches the runtime animation code.

Sequence entries can be:

  • animation names;
  • arrays of candidate animation names;
  • action descriptors, such as attaching or detaching an item;
  • function callbacks, such as finishing corpse looting or wounded help.

Examples in the base animation table include:

  • play_guitar and play_harmonica, which attach camp instruments;
  • punch, which calls the abuse punch helper and then clears abuse state;
  • search_corpse, which calls corpse-loot finalization;
  • help_wounded_with_medkit, which attaches the scripted medkit and finishes wounded help.

Smart-cover animations

Smart-cover animation files define cover descriptions and loopholes consumed by smartcover and animpoint. The registered smart_covers.descriptions extern exposes the list to X-Ray.

Animpoint schemes can also use predicate lists to choose compatible animations from a smart-cover description when avail_animations is not explicitly set.

Common pitfalls

  • State names are runtime API. Changing a name can break LTX sections that refer to it.
  • Some sequence callbacks have gameplay side effects, such as transferring loot or healing wounded NPCs.
  • Smart-cover descriptors are used both by scripts and engine cover logic. Keep cover and loophole names stable.

Callbacks and events

Callbacks are the bridge from X-Ray and Lua config scripts into the TypeScript runtime. Events are XRF’s internal publish-subscribe layer for sharing lifecycle changes between managers, binders, and schemes.

External callbacks

External callbacks live under src/engine/scripts/declarations/callbacks and are loaded by registerExternals().

ModuleExamples
actor.tsactor condition notifications, travel dialog callbacks
game.tssave/load hooks, level input, visual memory, trade, loadout, outro, class unregister
interface.tsload-screen tips, inventory upgrades, actor menu, PDA, weapon UI parameters
custom.tssleep, surge, achievement, task, and cutscene callbacks under engine.*

The declarations use extern(name, value) to register global functions or modules. Config files and engine code call those names from Lua.

Binder callbacks

Object binders register engine callbacks on online objects.

For example, ActorBinder registers callbacks for inventory info, item take/drop, trade, task state, use object, and HUD animation end. It converts those callbacks to EGameEvent emissions.

StalkerBinder registers hit, death, use, sound, and patrol extrapolate callbacks. These callbacks forward work to scheme events, managers, and global events such as STALKER_HIT or STALKER_DEATH.

EventsManager

EventsManager is the internal event dispatcher. It stores a Lua table of subscribers for every declared EGameEvent.

Use:

getManager(EventsManager).registerCallback(EGameEvent.ACTOR_UPDATE, this.onActorUpdate, this);
EventsManager.emitEvent(EGameEvent.GAME_STARTED, isNewGame);

Callbacks can be registered with or without an explicit context. When a context is provided, the manager calls the callback with that context.

Timers

EventsManager extends AbstractTimersManager, so it also owns game-time intervals and timeouts.

  • registerGameInterval(callback, period) repeats after at least period milliseconds.
  • registerGameTimeout(callback, delay) runs once after delay milliseconds.
  • intervals assert that the period is at least 50.
  • timers are processed from ActorBinder.update() through eventsManager.tick().

Event groups

EGameEvent includes events for:

  • actor lifecycle and throttled actor update ticks;
  • stalker, monster, helicopter, item, zone, smart terrain, and squad lifecycle;
  • task, treasure, surge, notification, and hit events;
  • save/load and level-change events;
  • UI events such as main menu on/off;
  • debug dump requests.

Guidelines

  • Use external callbacks only for names the engine or config files call directly.
  • Use EventsManager for internal cross-system notifications.
  • Unregister callbacks in destroy() or binder cleanup paths when the owner can be disposed.
  • Do not put long-running work inside high-frequency actor update events unless it is explicitly throttled.

Managers

Managers are singleton runtime services stored in registry.managers. They own cross-object systems such as events, save/load, sound, simulation, trade, tasks, map spots, weather, upgrades, and debug state.

Registration

registerManagers() initializes the manager list during start.callback.

The current startup list includes:

  • actor input and actor inventory menu;
  • database, debug, and events;
  • dialogs, load screen, loadout, map display, music, notifications, PDA;
  • phantom, save, simulation, sleep, sound, statistics;
  • tasks, trade, travel, treasures, upgrades, weather;
  • body release handling.

Each manager is initialized through initializeManager(ManagerClass).

Other managers can be initialized lazily through getManager. For the startup list and lifecycle rules, see the Runtime managers page.

Manager registry

Manager instances are stored in two maps:

  • registry.managers, keyed by class reference;
  • registry.managersByName, keyed by class name.

Use getManager(ManagerClass) for normal access. It initializes the manager if needed.

Use getWeakManager(ManagerClass) only when missing manager state is acceptable.

Use getManagerByName(name) only for circular-reference cases where the class reference is not available.

Lifecycle

Managers extend AbstractManager. The base class defines:

  • initialize();
  • destroy();
  • update(delta);
  • save(packet);
  • load(reader).

update, save, and load abort by default. A manager should implement only the lifecycle methods it actually uses.

disposeManager calls destroy(), marks the instance as destroyed, and removes it from both registry maps.

Common patterns

Managers often subscribe to EventsManager in initialize() and unsubscribe in destroy().

Examples:

  • SoundManager listens for actor update/offline and debug dump events.
  • SimulationManager listens for actor registration and actor offline events.
  • SaveManager coordinates client/server save callbacks exposed through alife_storage_manager.

Managers with persistent state write to net packets or dynamic save data. When editing save/load logic, keep marker order and read/write order synchronized.

Guidelines

  • Put shared system state in a manager, not in a binder.
  • Keep object-local state in the object’s registry state or binder.
  • Register manager callbacks in initialize() and unregister them in destroy().
  • Avoid constructing managers directly; use getManager unless a test is isolating the class.

Object binders

Object binders attach TypeScript lifecycle code to online X-Ray game objects. They are client-side wrappers around object_binder and are registered through the bind extern module.

Binder registration

src/engine/scripts/bind.ts exposes one function per bindable object category:

  • creatures: actor, stalker, monster, crow;
  • zones: restrictor, anomaly_zone, anomaly_field, camp, arena_zone, level_changer;
  • physical objects: physic_object, door, campfire, artefact, phantom, signal_light;
  • items: weapon, helmet, outfit;
  • simulation objects: smart_terrain, smart_cover;
  • helicopter: helicopter.

Some binders are conditional. For example, physic_object binds only when the object has a [logic] section or is an inventory box, and smart_terrain binds only when the spawn ini contains [smart_terrain].

Lifecycle methods

Most binders implement some subset of:

  • reinit: reset local state and registry state;
  • net_spawn: object came online;
  • update: per-frame object update;
  • net_destroy: object went offline;
  • save and load: client-side save state;
  • net_save_relevant: whether binder state should be saved.

Server object classes use related server callbacks such as on_register, on_unregister, STATE_Write, and STATE_Read.

ActorBinder

ActorBinder registers the actor object, initializes portable store, emits actor lifecycle events, and drives global update ticks.

On each update it emits:

  • ACTOR_UPDATE;
  • ACTOR_UPDATE_100;
  • ACTOR_UPDATE_500;
  • ACTOR_UPDATE_1000;
  • ACTOR_UPDATE_5000;
  • ACTOR_UPDATE_10000.

It also ticks EventsManager timers and updates simulation object availability for the actor server object.

StalkerBinder

StalkerBinder owns online stalker runtime setup:

  • resets object registry state;
  • creates the state manager and patrol manager;
  • sets up state and motivation planners;
  • registers the stalker in the global registry;
  • initializes sound themes, reach-task behavior, object logic, post-combat idle, trade, and light behavior;
  • forwards hit, death, use, sound, and patrol events to schemes and managers.

On offline switch it emits scheme events, runs on_offline overrides, stores offline state, stops sounds, and unregisters the stalker.

RestrictorBinder

RestrictorBinder registers zones, initializes restrictor scheme logic on first update, tracks visited restrictors, emits visit events, updates active schemes, and persists visited state.

Guidelines

  • Keep binders focused on lifecycle glue.
  • Put reusable behavior in schemes, managers, or utilities.
  • Always unregister callbacks and registry entries when an object goes offline.
  • When adding save/load fields, update the matching load path in the same order.

Registry

The registry is the shared runtime state table for the script engine. It is defined in src/engine/core/database/registry.ts and re-exported through src/engine/core/database.

Use it for state that must be visible across binders, schemes, managers, and utility modules.

Main state groups

FieldPurpose
simulatorCurrent ALife simulator
actorOnline actor game object
actorServerActor server object
managers / managersByNameManager singletons
schemesRegistered scheme constructors
objectsOnline object registry states
offlineObjectsSaved state for offline objects
simulationObjectsObjects that can participate in simulation
storyLinkStory id to object id mappings
stalkersOnline stalker id set
smartTerrainsRegistered smart terrains
smartCoversRegistered smart covers
zonesActive zones by name
dynamicDataMarshal-backed dynamic save data

There are also focused registries for wounded objects, doors, helicopters, crows, anomaly zones, light zones, silence zones, no-weapon zones, trade state, camp managers, ranks, goodwill, and save markers.

Object state

registry.objects is the central per-object store. Binders reset and register object state when objects come online. Schemes attach their state into the same object descriptor by scheme id.

Common object state includes:

  • object reference;
  • spawn ini and logic section;
  • active scheme and active section;
  • scheme states;
  • overrides;
  • state manager and patrol manager for stalkers.

Story links

Story links are stored both ways:

  • storyLink.sidById: object id to story id;
  • storyLink.idBySid: story id to object id.

Use the database helpers to register or unregister story links. Do not update these tables by hand unless the helper does not cover the case.

Manager access

Managers should be accessed through:

getManager(SoundManager);
getWeakManager(SoundManager);
getManagerByName("SoundManager");

Direct reads from registry.managers are reserved for low-level registry helpers and exceptional circular-reference cases.

Guidelines

  • Prefer a focused database helper over direct table mutation.
  • Store transient object-specific state under registry.objects.
  • Store persistent cross-system state in a manager or registry.dynamicData.
  • Clear registry entries in offline, unregister, or destroy paths.
  • Keep registry fields narrow. A broad table without a lifecycle owner becomes difficult to save and clean up.

Server objects

Server object classes extend X-Ray cse_alife_* classes and run on the ALife side of the engine. They register story links, simulation targets, save/load data, and object-specific ALife behavior.

Runtime server classes live under src/engine/core/objects.

Source layout

SourcePurpose
objects/creatureActor, stalker, and monster server classes
objects/itemItem, weapon, ammo, outfit, helmet, detector, torch, box, and related classes
objects/physicScripted physical server object classes
objects/zoneRestrictor, anomaly, torrid, and visual zone classes
objects/squadOnline/offline squad group class and actions
objects/smart_terrainSmart terrain class, jobs, respawn, and control
objects/smart_coverSmart cover server representation
objects/helicopterHelicopter server class
objects/levelLevel changer server class

Class registration

Server classes are linked to engine class ids through the registration flow exposed by register.registerGameClasses. Class-id helpers are implemented in src/engine/scripts/register.

The class-id helpers distinguish game class ids, UI class ids, and object class ids. Unknown game types abort during class-id resolution.

Common lifecycle

Server object classes usually implement some subset of:

  • on_register;
  • on_unregister;
  • STATE_Write;
  • STATE_Read;
  • on_death;
  • update;
  • engine-specific task methods.

Registration usually updates registry, story links, simulation objects, and events. Unregistration must reverse those links.

Actor

The actor server object registers actor server state, story id actor, and simulation participation. It delegates server save/load to SaveManager and emits ACTOR_REGISTER, ACTOR_UNREGISTER, and ACTOR_DEATH.

As a simulation target, the actor can be selected only when actor simulation is allowed and safe-zone restrictions do not exclude it.

Squad

Squad extends cse_alife_online_offline_group. It is both a server group and a simulation target.

It owns:

  • squad target condlists;
  • faction behavior;
  • current simulation action;
  • assigned terrain and target ids;
  • map spot state;
  • member registration;
  • scripted target rotation;
  • save/load for target, respawn, and terrain assignment state.

Squads select either SquadReachTargetAction or SquadStayOnTargetAction depending on whether the current target is already reached.

Smart terrain

SmartTerrain extends cse_alife_smart_zone. It owns terrain simulation, job assignment, respawn configuration, campfires, map spot state, alarm/control state, arriving objects, and job save/load data.

Smart terrain registration creates jobs and simulation descriptors. NPC registration either assigns a job immediately or marks the object as arriving until it reaches the terrain.

Guidelines

  • Server object state must be saved and loaded in matching order.
  • Register story links and simulation objects on on_register; unregister them on on_unregister.
  • Keep client-side behavior in binders or schemes. Server classes should own ALife state and server persistence.

Sounds

Sound scripts load sound themes from LTX, play one-shot and looped sounds, save active sound state, and map engine sound masks to script sound classes.

Source layout

SourcePurpose
src/engine/core/managers/soundsSound manager, config, sound story classes, playable sound objects
src/engine/core/managers/sounds/utilsTheme loading, story helpers, playback helpers
src/engine/core/utils/sound.tsConsole volume helpers and sound-mask mapping
src/engine/configsGenerated and static sound config inputs

Sound configuration

SoundsConfig.ts loads:

  • managers\sounds\script_sound.ltx;
  • managers\sounds\sound_stories.ltx.

script_sound.ltx is parsed into soundsConfig.themes. Runtime state is stored in:

  • soundsConfig.playing: current one-shot sound per object id;
  • soundsConfig.looped: looped sound themes per object id;
  • soundsConfig.managers: story managers by id.

SoundManager

SoundManager is registered during startup. It subscribes to:

  • DUMP_LUA_DATA;
  • ACTOR_GO_OFFLINE;
  • ACTOR_UPDATE.

The main methods are:

  • play(objectId, name, faction?, point?);
  • stop(objectId);
  • playLooped(objectId, name);
  • stopLooped(objectId, name);
  • stopAllLooped(objectId);
  • setLoopedSoundVolume(objectId, name, volume);
  • saveObject and loadObject for object-local sound state.

play rejects looped sound themes. playLooped requires a looped theme.

Heard-sound mapping

mapSoundMaskToSoundType converts X-Ray snd_type bit masks into script enum values used by hear and danger logic.

Supported groups include:

  • weapon sounds: WPN_shoot, WPN_empty, WPN_hit, WPN_reload, WPN;
  • item sounds: ITM_pckup, ITM_drop, ITM_hide, ITM_take, ITM_use, ITM;
  • monster sounds: MST_die, MST_damage, MST_step, MST_talk, MST_attack, MST_eat, MST;
  • fallback: NIL.

Volume helpers

getMusicVolume, setMusicVolume, getEffectsVolume, and setEffectsVolume read and write X-Ray console variables for music and effects volume.

Guidelines

  • Use SoundManager for scripted playback so save/load and looped state stay consistent.
  • Use sound_idle fields in schemes when a scheme already owns the playback.
  • Do not call play with looped themes or playLooped with one-shot themes.
  • Stop object sounds when objects go offline.

Simulation

Simulation scripts coordinate ALife squads, smart terrains, actor targeting, respawn, map spots, and online/offline movement tasks.

The core implementation is split between SimulationManager, server object classes, smart terrain utilities, and squad actions.

Source layout

SourcePurpose
src/engine/core/managers/simulationSimulation manager, config, activity rules, target selection
src/engine/core/objects/squadSquad server object and reach/stay actions
src/engine/core/objects/smart_terrainSmart terrain simulation target, jobs, respawn, control
src/engine/core/objects/creature/Actor.tsActor as a simulation target
src/engine/core/database/simulation.tsSimulation registration helpers
src/engine/core/utils/alife.tsALife update batching helpers
src/engine/core/utils/squadSquad state/action helpers

SimulationManager

SimulationManager is registered during startup. It listens for:

  • DUMP_LUA_DATA;
  • ACTOR_REGISTER;
  • ACTOR_GO_OFFLINE.

On actor registration it initializes default simulation squads. On actor offline it removes the actor from X-Ray ranking when that engine callback exists. It also saves and loads the IS_SIMULATION_INITIALIZED flag.

Simulation targets

The main simulation targets are:

  • actor server object;
  • squads;
  • smart terrains.

Targets expose methods such as:

  • getSimulationTask;
  • isSimulationAvailable;
  • isValidSimulationTarget;
  • isReachedBySimulationObject;
  • onSimulationTargetSelected;
  • onSimulationTargetDeselected.

Squads

Squad is an online/offline group with a faction, behavior table, assigned target, current action, map spot, story manager, and optional scripted target condlist.

When updating, a squad either:

  • follows a scripted target from target_smart;
  • helps the actor if the helper target is available;
  • selects a generic simulation target by priority.

It then runs either:

  • SquadReachTargetAction;
  • SquadStayOnTargetAction.

Smart terrains

SmartTerrain owns:

  • simulation role and simulation properties;
  • max population and arrival distance;
  • job creation and job assignment;
  • respawn configuration;
  • campfire state;
  • terrain control and alarm state;
  • arriving objects and assigned job descriptors.

When a terrain is selected as a target, squad members are soft-reset offline and assigned to the terrain. When NPCs arrive, the terrain assigns jobs from its job list.

ALife update rate

setUnlimitedAlifeObjectsUpdate() temporarily allows all ALife objects to update, which smooths initial spawn. setStableAlifeObjectsUpdate() restores the configured stable update count.

ActorBinder.reinit() enables unlimited updates, then schedules stable updates through an EventsManager timeout.

Guidelines

  • Treat squads and smart terrains as server-side simulation objects.
  • Do not change target selection from only one layer. Check squad actions, simulation priority utilities, terrain availability, and save/load state together.
  • When changing simulation persistence, update both STATE_Write and STATE_Read.

Time

Time helpers live in src/engine/core/utils/time.ts. They format game time, convert weather periods, advance game time, and serialize X-Ray CTime values for save data.

Formatting

toTimeDigit

Pads values below 10 with a leading zero.

gameTimeToString

Formats a CTime value as:

hh:mm MM/DD/YYYY

globalTimeToString

Formats a duration in milliseconds as:

h:mm:ss

This is used by visible timers such as sr_timer.

hoursToWeatherPeriod

Converts an hour to a weather period section label:

6 -> 06:00:00
12 -> 12:00:00

Time checks

isInTimeInterval(fromHours, toHours) checks whether the current level hour is inside a range. Ranges that cross midnight are supported.

For example, 22 to 4 means late night through early morning.

Changing time

setCurrentTime(hour, min, sec) advances game time to the requested time of day. If the current day has already passed that time, it advances to the next day.

The helper temporarily sets the level time factor to 10000, waits until game time reaches the target, then restores the previous time factor.

Use this carefully. It is not a passive setter; it advances the simulation while waiting.

Save and load

writeTimeToPacket(packet, time) writes a nullable time value to a net packet. null is stored as MAX_U8.

readTimeFromPacket(reader) restores a CTime value or returns null for the null marker.

serializeTime(time) and deserializeTime(data) use marshal to store and restore a CTime tuple as a string.

Guidelines

  • Use globalTimeToString for millisecond durations.
  • Use gameTimeToString for calendar game time.
  • Use packet helpers for net save data, not ad hoc string formatting.
  • Avoid setCurrentTime in ordinary update paths because it waits while time advances.

UI

Runtime UI scripts load XML forms, initialize X-Ray CUI controls, bind callbacks, and implement menus, debug screens, and in-game dialogs.

This page covers runtime scripts. Form source generation is covered in the Forms page.

Source layout

SourcePurpose
src/engine/core/uiRuntime UI classes
src/engine/core/utils/uiXML loading and element initialization helpers
src/engine/formsTSX/static XML form sources
src/engine/scripts/declarations/callbacks/interface.tsEngine-facing UI callbacks

XML loading

resolveXmlFormPath(path, hasWideScreenSupport) normalizes form paths and optionally selects a .16.xml variant when the current screen is wide and the file exists.

resolveXmlFile(path, xml?, hasWideScreenSupport?) creates or reuses CScriptXmlInit, resolves the form path, and calls ParseFile.

In debug mode, XML paths containing / abort because X-Ray expects Windows-style UI paths.

Element initialization

initializeElement(xml, type, selector, base, descriptor?) initializes CUI controls from XML and can register callbacks on the owning CUIScriptWnd.

Supported element types include:

  • buttons, check buttons, combo boxes, tabs, track bars, edit boxes, list boxes, scroll views;
  • static text and text windows;
  • frames and frame lines;
  • map list and map info controls;
  • message boxes;
  • generic windows.

initializeStatic and initializeStatics are shortcuts for static controls.

Runtime UI classes

The runtime UI tree includes:

  • main menu and options dialogs;
  • save/load dialogs;
  • extensions dialog;
  • debug dialog and debug sections;
  • sleep, numpad, and freeplay dialogs;
  • inventory menu integration through actor menu callbacks.

These classes load XML by selector names. If an XML element name changes, update the runtime class and tests together.

Interface externals

interface.ts registers engine-facing modules such as:

  • loadscreen;
  • inventory_upgrades;
  • actor_menu;
  • actor_menu_inventory;
  • pda;
  • ui_wpn_params.

These callbacks connect UI XML or C++ UI code to managers such as LoadScreenManager, UpgradesManager, ActorInventoryMenuManager, TradeManager, and PdaManager.

Guidelines

  • Keep generated XML and runtime selectors in sync.
  • Check paired 16:9 forms when changing layout.
  • Register UI callbacks through initializeElement when the control needs script events.
  • Use resolveXmlFile instead of constructing CScriptXmlInit paths by hand.

Utils

Utility modules provide shared, mostly stateless helpers used by binders, schemes, managers, config callbacks, and server object classes.

They live under src/engine/core/utils.

Main utility areas

AreaExamples
iniLTX reads, condlists, switch parsing
schemescheme setup, switching, events, object logic initialization
objectobject spawn ini, visual setup, wound/setup helpers
spawnitem, ammo, squad, object, and actor-near spawn helpers
soundsound masks, volume console helpers, object sound checks
timetime formatting and packet serialization
uiXML resolution, CUI element setup, screen helpers
position, vector, patrol, level, vertexX-Ray spatial helpers
relation, community, ranksfaction and relationship helpers
squad, smart_terrain, smart_coversimulation and job helpers
loggingLuaLogger and log registry helpers
gamegame-flow checks and waiting helpers
bindingglobal extern registration helpers

Spawn helpers

spawn.ts wraps common simulator creation paths:

  • spawnItemsForObject;
  • spawnItemsAtPosition;
  • spawnAmmoForObject;
  • spawnAmmoAtPosition;
  • spawnItemsForObjectFromList;
  • spawnSquadInSmart;
  • spawnObject;
  • spawnObjectInObject;
  • releaseObject;
  • spawnCreatureNearActor.

Ammo helpers respect section box_size and split large ammo counts into engine-safe chunks.

Binding helpers

binding.ts owns extern and getExtern, which are used by script declarations to expose Lua globals. Use these helpers instead of assigning globals directly.

INI and scheme helpers

The ini helpers parse fields, condlists, string lists, condition lists, timers, switch conditions, and bone descriptors.

The scheme helpers initialize object logic, register scheme constructors, dispatch scheme events, and switch active sections.

These helpers are the preferred path for LTX-driven behavior. Avoid ad hoc parsing in schemes when a parser already exists.

Logging

Use LuaLogger for runtime logs. It supports file-aware logging and named log channels used by systems such as simulation, smart terrain, and psy logic.

Guidelines

  • Keep utilities focused and dependency-light.
  • Use existing parser/helpers before adding custom string parsing.
  • Put stateful cross-object behavior in managers, not utilities.
  • Add tests next to utility modules; most utility folders already have focused Jest tests.

Shaders

Shader files are static game resources. XRF does not compile or generate shader source from TypeScript. The build copies shader files from the resource roots into target/gamedata/shaders as part of the resources build target.

Use this page when you need to find where shader files live, how they reach the game package, or where configs and UI forms reference shader names.

Source layout

SourcePurpose
src/resources/shadersBase shader files copied to gamedata/shaders.
src/resources/shaders/r1, r2, r3, glRenderer-specific shader directories.
src/resources/shaders/sharedShared include files used by shader source.
src/engine/configs/**/*.ltxConfig references to shader names.
src/engine/forms/**/*.tsx and static UI XMLUI texture nodes can set a shader attribute.
src/engine/lib/constants/roots.tsDefines the $game_shaders$ root alias.

The base resource directory also contains other static asset folders such as anims, levels, sounds, spawns, and textures. Shader files follow the same static-resource build path as those folders.

Build behavior

The CLI resources target copies static assets from configured resource roots:

npm run cli build -- --include resources

The default root is src/resources. When asset overrides are enabled, the CLI also checks locale and override resource roots from cli/config.json before the base root.

The static resource copier intentionally skips development-only folders such as textures_unpacked and particles_unpacked. It also rejects resource roots that overlap generated engine folders such as configs, scripts, core, or lib.

References from configs

Shader names are usually referenced by engine configs rather than by scripts. Examples include:

shader = font
tracer_shader = effects\bullet_tracer
sun_shader = effects\sun

UI forms can also set shader attributes on texture nodes:

<texture shader={"hud\\p3d"}>ui_inGame2_Detector_icon_acid_big</texture>

Keep these references aligned with files under gamedata/shaders. The game resolves shader names through the normal X-Ray filesystem aliases, including $game_shaders$.

Guidelines

  • Edit shader source under resource roots, not under target/.
  • Use the resources build target for shader-only changes.
  • Search configs and forms before renaming a shader path.
  • Keep renderer-specific variants together when a shader exists in more than one renderer directory.
  • Treat src/resources as resource-owned content. Avoid broad edits unless the task is about assets.

Translations

Translations provide string-table source for UI labels, dialogs, tasks, item names, achievements, subtitles, and other text shown by the game.

XRF keeps the main editable translation source in JSON files under src/engine/translations. The build converts those sources into X-Ray string-table XML under target/gamedata/configs/text.

Supported languages

The supported locale keys are defined in cli/config.json:

KeyLanguage
engEnglish
fraFrench
gerGerman
itaItalian
polPolish
rusRussian
spaSpanish
ukrUkrainian

The default build locale is also configured in cli/config.json. It can be overridden with the build command --language option.

Source format

Each JSON file is a dictionary keyed by translation id. Each translation id contains one value per supported locale. Values can be strings or string arrays:

{
  "st_example_name": {
    "eng": "Example",
    "fra": "Example",
    "ger": "Example",
    "ita": "Example",
    "pol": "Example",
    "rus": "Example",
    "spa": "Example",
    "ukr": "Example"
  }
}

String arrays are used when XML text contains explicit line breaks. The XML import helper splits \n into arrays when converting existing string-table XML into JSON.

Build behavior

The translations build target calls the bundled xrf-cli translation builder:

npm run cli build -- --include translations

The source path is src/engine/translations. The target path is:

target/gamedata/configs/text

Do not edit generated XML under target/. Fix the JSON source or the imported static XML source instead.

Translation utilities

The local CLI exposes helper commands under translations:

npm run cli translations init <path>
npm run cli translations check
npm run cli translations check -- --language eng
npm run cli translations to_json <path> -- --language eng --output src/engine/translations

Use init to add missing locale keys to JSON translation files. Use check to list missing or invalid entries. Use to_json when importing existing X-Ray XML string tables.

The XML importer accepts an optional --encoding value. If encoding is not passed, it tries the XML header first and then falls back to windows-1251 for ukr and rus, or windows-1250 for the other supported locales.

References from game data

Translation ids are referenced across multiple source types:

  • UI form text nodes;
  • dialog XML text nodes;
  • task configs and task functors;
  • item, weapon, outfit, and upgrade configs;
  • script callbacks that return text ids.

When changing an id, search the repository before renaming it. Task fields and dialog conditions may be condlists or script callbacks rather than plain string ids.

Guidelines

  • Keep ids stable when only the wording changes.
  • Fill every supported locale key used by the file.
  • Use arrays only for intentional multiline text.
  • Run npm run cli translations check after translation edits.
  • Run npm run cli build -- --include translations before packaging.
  • Do not patch generated files under target/gamedata/configs/text.