Skip to main content

Runtime & game-std Architecture Review

Analysis of naming, system boundaries, plugin vs core, modding perspective, and scope system value.

1. Naming Problem

The current names don't communicate actual roles:

CurrentActual RoleProblem
runtime/Engine backend — C API, all subsystem state, FFI boundary"Runtime" is vague; could mean Rust runtime, game runtime, or scripting runtime
game-std/Script standard library — AtomicPtr bridge to engine"game" implies game logic, but this is engine infrastructure (like Unity's UnityEngine.dll)

Suggested renaming:

  • game-std -> sers-std — it's the SERS script standard library, engine infrastructure, not game-specific
  • runtime -> keep as-is or sers-engine — "runtime" is defensible for the engine backend that hosts load

2. What Belongs in the Engine vs What Should Be Optional

Applying the AGENTS.md test: "Does an arcade game need this? Does an RTS need this? If only one genre needs it, it does NOT belong here."

The runtime currently has 14 subsystems all compiled into one cdylib. The ABI capability flags already exist (21 bits), which means the infrastructure for optional subsystems is partially there. But everything is always compiled and loaded.

Tier 1 — Core (always present, any game genre needs these)

SystemRationale
Engine/World lifecycleFoundation — create engine, create world, load scripts
Schema/template/vtable/constant infrastructureThe core value proposition of SERS
Host callback registration + dispatchRequired for any host integration
Error handlingInfrastructure
Serialization primitivesInfrastructure
Variables (typed, entity-scoped, global)Every game needs key-value state storage
Update/tick orchestrationEvery game needs a frame/tick loop
Save/load infrastructureEvery game needs persistence

Tier 2 — Common (most games need these, but could be disabled)

SystemRationaleGenre Test
Stats + modifiers + resolutionRPG/strategy staple; but a puzzle game doesn't need modifier stackingFails arcade
LocalizationEvery shipped game needs it, but a prototype doesn'tBorderline core
Entity queries/iteratorsMost games iterate entities, but the batch-protocol pattern is strategy-orientedBorderline core
Timed modifiersExtension of stats; buff/debuff pattern is RPG-specificFails arcade

Tier 3 — Strategy/Simulation (Paradox-inspired, genre-specific)

SystemRationaleGenre Test
Events (two-phase, MTTH, cooldowns, dispatch modes, on-actions)Paradox event model — strategy/narrative gamesFails arcade, fails most action games
Decisions (is_shown/is_valid/effect/ai_will_do)Player-initiated actions with AI scoring — strategy/RPGFails arcade
AI/Utility AI (considerations, curves, selection modes)Utility-based AI — strategy/simulationFails platformer, fails puzzle
Conditional modifiers (WhileTrue/RemoveOnTrue)Strategy/RPG buff managementFails most genres
Game rules (pre-game settings)Strategy-game settings screenBorderline — most games have settings, but this specific pattern is strategy-oriented
Concepts/encyclopedia (hoverable wiki terms)Strategy/RPG lore — CK3/EU4 patternFails most genres

Tier 4 — Specialized

SystemRationale
Graph evaluator (DAG node execution)Skill trees, ability graphs, narrative flow — specific use cases
Programs (flow-graph tick stepping)Advanced graph execution with lifecycle
Connectors (component blob access)Advanced graph integration

The Problem

A studio making a simple action game loads all 14 subsystems. The ABI cap flags let scripts detect which are available, but the runtime always compiles and initializes everything.

Recommendation

Use Cargo feature flags in the runtime crate to make Tier 2-4 subsystems compile-optional. The World struct already holds each subsystem behind Mutex-wrapped fields. Feature-gate the mod events;, mod decisions;, mod ai;, etc. declarations and their FFI exports. The ABI cap flags already exist to communicate availability across the boundary.

3. Scope System — Does It Add Value?

Yes. Keep it.

The @this/@from/@root syntax is a thin sugar (desugars to unsafe { &mut *self.__this_view }.field), but the scope concept is load-bearing across the entire architecture:

Five Purposes

  1. Schema-level type contract — The schema declares this: Location, from: Location, root: Empire, and the compiler validates every template method only accesses valid fields. Without this, modders would need to manually declare function signatures on every method and could pass wrong types silently.

  2. Uniform FFI ABI — Every vtable function pointer for a schema has the same (*mut u8, *mut u8, *mut u8) shape based on scope count. The host only needs per-schema introspection (not per-method). The C# SDK's SersBoolMethod<T1, T2, T3> generics depend on this.

  3. Event chain propagation — Chained/delayed events carry FROM context and saved scopes through the full lifecycle including save/load. Without the scope abstraction, each event would need ad-hoc parameter passing.

  4. Entity-type namespace — The scope string ("character", "province") is used as a discriminator in stats, modifiers, variables, localization, programs. It's the universal "what kind of entity is this ID?" tag.

  5. Localization expressions[from.field], [root.field], [scope:saved.field] all depend on the named scope model.

Why It Works Better in SERS Than in Paradox

SERS didn't transplant Paradox's implementation (declarative text, tree-walking interpreter). It transplanted Paradox's domain model and re-expressed it using Rust-native abstractions (typed pointers, closures, vtable dispatch). The scope system gets compile-time type checking — Paradox's implicit scope is a constant source of modding bugs that only surface at runtime.

Limitation

The @ syntax is limited to 3 named scopes. If a game needs 4+ participants in an interaction, additional function parameters are needed. But 3 covers the vast majority of cases (actor + target + context).

4. Systems from Modding Perspective

Currently Moddable (correctly so)

  • Template logic (full override or member-level override/inject)
  • Template constants (balance values, metadata)
  • Events, decisions, AI weights — all via template override
  • Localization — layer-merged TOML, later wins
  • New content — mods can add new templates for existing schemas

Currently NOT Moddable (host-only) — Some Should Change

SystemCurrentShould BeRationale
Entity providersHost-onlyMod-registrable via template constant patternA mod adding "character.allies" relationship can't register a provider. Use PROVIDER_KEY constant convention like EVENT_KEY/DECISION_KEY
On-actionsHost-onlyMod-definableIn Paradox, mods freely define new on-actions. SERS should allow templates with ON_ACTION_KEY to declare new ones
Game rulesHost-onlyMod-definableSame pattern — templates with GAME_RULE_KEY
Conditional modifier conditionsHost-only callbacksShould allow script-side conditionsMods should be able to define conditions in .sers, not just host callbacks

Correctly NOT Moddable

  • Views (data shapes) — immutable by design, changing shape breaks all other mods
  • Schemas (interface contracts) — same reason
  • Stat resolution formula — mathematical invariant; mods changing stacking order breaks everything
  • Save/load format — must remain stable across mods
  • Template merge semantics — foundational to the modding model itself

5. Paradox Pattern Fit Assessment

Every Paradox-inspired system translates naturally to compiled Rust. Zero systems need fundamental redesign. The reason: SERS transplanted the domain model, not the implementation.

System-by-System

SystemVerdictRationale
Scope systemNatural fitExplicit @this.field desugars to typed pointer access; compile-time validated
Event systemNatural fitRuntime-owned state + stateless vtable dispatch; methods are natural Rust signatures
Stats/modifiersNatural fitData structure problem, not language problem; runtime-owned resolution engine
Decision systemNatural fitFour-method evaluation contract maps cleanly to Rust trait methods
Entity queriesNatural fitRust closures replace 300 hardcoded Paradox keywords; batch protocol eliminates FFI overhead
Mod layeringNatural fit (strongest)AST-level merge + type checking is only possible because SERS is compiled
Template constantsNatural fitValid Rust const syntax; type-checked; runtime-discoverable
AI evaluationNatural fitDual-entry-point scoring with response curve library; generic batch eval
Scripted values/reuseBetter as Rust featuresFree functions, modules, generics replace Paradox's "scripted_trigger" keyword
On-actionsNatural fitNamed event groups with schema validation at load time
Game rulesNatural fitSimple key-value store; belongs in runtime for unified save/load
Character interactionsNatural fitMulti-scope schema with this=sender, from=receiver; not yet implemented
Navigation linksNatural fitCompile-time pointer cast; type-checked link traversal
Localization expressionsMostly naturalExpression resolution is pragmatic but effect tooltips require manual authoring (compiled effects are opaque)
GUI data bindingNatural fitPull model with atomic version counter is correct for cross-FFI
Traits/laws/progressionBetter as Rust featuresComposable from existing primitives; no engine changes needed

One Genuine Tension

Effect tooltips: Paradox's declarative add_gold = -50 is introspectable for auto-generated tooltips. SERS's compiled @this.gold -= 50.0 is opaque. Effect descriptions must be authored as localization strings. This is the fundamental trade-off of compiled-vs-interpreted scripting.

Naming

  1. Rename game-std -> sers-std (repo: sers-std, crate: sers_std)

Architecture

  1. Feature-gate Tier 3-4 subsystems in the runtime crate with Cargo features (events, decisions, ai, conditional_modifiers, game_rules, concepts, graph, programs, connectors)
  2. Keep Tier 1-2 always-on (core lifecycle, schemas, templates, variables, stats, localization, entity queries, update, save/load)
  3. Wire ABI cap flags to Cargo features so they're compile-time

Modding Gaps

  1. Allow mods to register entity providers via template constant convention
  2. Allow mods to define on-actions via template constants
  3. Allow mods to define game rules via template constants
  4. Allow script-side conditional modifier conditions

Scope System

  1. Keep as-is. The scope concept is deeply embedded in the entire runtime, and it provides genuine value (type safety, uniform ABI, event propagation, entity namespacing, localization).

Current Module Inventory

Runtime (230+ FFI exports across 14 subsystems)

lib.rs -- World/Engine, variable/stat/template FFI
ffi.rs -- All #[no_mangle] extern "C" se_*() exports
error.rs -- RuntimeError, error codes, thread-local last-error
helpers.rs -- FFI helpers (cstr_to_str, world_ref, etc.)
stats.rs -- StatsRuntime, stat registration + query
modifiers.rs -- ModifierSetOwned, sync, timed modifiers
resolution.rs -- Stat resolution (f32/f64, batch, explain)
variables.rs -- VariableStore (entity + global), timed variables
project.rs -- LoadedProject, load_project, load_bundle, HostVTable
schemas.rs -- SchemaRegistry, metadata loading
templates.rs -- Template/vtable/constant discovery
events/ -- EventsRuntime (registry, triggers, execution, queries, save_load)
localization.rs -- LocalizationStore, TOML loading
loc_expression.rs -- Expression resolver, field providers, navigation
ffi_loc.rs -- Localization expression FFI
concepts.rs -- ConceptRegistry, categories
iterators.rs -- IteratorsRuntime, entity providers, batch query
game_rules.rs -- GameRulesRuntime, save/load
decisions.rs -- DecisionsRuntime, evaluation, AI best-pick
ai.rs -- AiRuntime, utility AI, curves, selection, momentum
ai_eval.rs -- Backward-compat FFI wrappers
ffi_ai.rs -- AI consideration FFI
update.rs -- UpdateRuntime, per-system toggle, hooks, profiling
conditional_modifiers.rs -- WhileTrue/RemoveOnTrue, decay, conditions
graph/ -- GraphEvaluatorRuntime (definition, evaluation, callbacks, ffi, save_load)
save_load.rs -- Unified "SERW" blob (7 subsystems)
serialization.rs -- Low-level binary read/write

game-std (8 source files, ~90 dynamic callbacks)

lib.rs -- Core: vtable, registries, ModifierSet builder, call_host_* dispatch
events.rs -- fire_event, delayed events, on-actions, save/get scope, localize, game rules
stats.rs -- Base stat reads, resolution, stat definition registration
templates.rs -- ABI exports: __sers_init, template/const/schema registration
variables.rs -- Entity + global variables (f64/i64/bool/str), timed, navigation
modifiers.rs -- sync_modifiers, timed modifier add/remove, decay
queries.rs -- Entity query iteration, response curves, deterministic randomness
graphs.rs -- Graph evaluator, AI evaluation, decisions, programs, connectors, memory