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:
| Current | Actual Role | Problem |
|---|---|---|
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-specificruntime-> keep as-is orsers-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)
| System | Rationale |
|---|---|
| Engine/World lifecycle | Foundation — create engine, create world, load scripts |
| Schema/template/vtable/constant infrastructure | The core value proposition of SERS |
| Host callback registration + dispatch | Required for any host integration |
| Error handling | Infrastructure |
| Serialization primitives | Infrastructure |
| Variables (typed, entity-scoped, global) | Every game needs key-value state storage |
| Update/tick orchestration | Every game needs a frame/tick loop |
| Save/load infrastructure | Every game needs persistence |
Tier 2 — Common (most games need these, but could be disabled)
| System | Rationale | Genre Test |
|---|---|---|
| Stats + modifiers + resolution | RPG/strategy staple; but a puzzle game doesn't need modifier stacking | Fails arcade |
| Localization | Every shipped game needs it, but a prototype doesn't | Borderline core |
| Entity queries/iterators | Most games iterate entities, but the batch-protocol pattern is strategy-oriented | Borderline core |
| Timed modifiers | Extension of stats; buff/debuff pattern is RPG-specific | Fails arcade |
Tier 3 — Strategy/Simulation (Paradox-inspired, genre-specific)
| System | Rationale | Genre Test |
|---|---|---|
| Events (two-phase, MTTH, cooldowns, dispatch modes, on-actions) | Paradox event model — strategy/narrative games | Fails arcade, fails most action games |
| Decisions (is_shown/is_valid/effect/ai_will_do) | Player-initiated actions with AI scoring — strategy/RPG | Fails arcade |
| AI/Utility AI (considerations, curves, selection modes) | Utility-based AI — strategy/simulation | Fails platformer, fails puzzle |
| Conditional modifiers (WhileTrue/RemoveOnTrue) | Strategy/RPG buff management | Fails most genres |
| Game rules (pre-game settings) | Strategy-game settings screen | Borderline — most games have settings, but this specific pattern is strategy-oriented |
| Concepts/encyclopedia (hoverable wiki terms) | Strategy/RPG lore — CK3/EU4 pattern | Fails most genres |
Tier 4 — Specialized
| System | Rationale |
|---|---|
| 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
-
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. -
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'sSersBoolMethod<T1, T2, T3>generics depend on this. -
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.
-
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. -
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
| System | Current | Should Be | Rationale |
|---|---|---|---|
| Entity providers | Host-only | Mod-registrable via template constant pattern | A mod adding "character.allies" relationship can't register a provider. Use PROVIDER_KEY constant convention like EVENT_KEY/DECISION_KEY |
| On-actions | Host-only | Mod-definable | In Paradox, mods freely define new on-actions. SERS should allow templates with ON_ACTION_KEY to declare new ones |
| Game rules | Host-only | Mod-definable | Same pattern — templates with GAME_RULE_KEY |
| Conditional modifier conditions | Host-only callbacks | Should allow script-side conditions | Mods 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
| System | Verdict | Rationale |
|---|---|---|
| Scope system | Natural fit | Explicit @this.field desugars to typed pointer access; compile-time validated |
| Event system | Natural fit | Runtime-owned state + stateless vtable dispatch; methods are natural Rust signatures |
| Stats/modifiers | Natural fit | Data structure problem, not language problem; runtime-owned resolution engine |
| Decision system | Natural fit | Four-method evaluation contract maps cleanly to Rust trait methods |
| Entity queries | Natural fit | Rust closures replace 300 hardcoded Paradox keywords; batch protocol eliminates FFI overhead |
| Mod layering | Natural fit (strongest) | AST-level merge + type checking is only possible because SERS is compiled |
| Template constants | Natural fit | Valid Rust const syntax; type-checked; runtime-discoverable |
| AI evaluation | Natural fit | Dual-entry-point scoring with response curve library; generic batch eval |
| Scripted values/reuse | Better as Rust features | Free functions, modules, generics replace Paradox's "scripted_trigger" keyword |
| On-actions | Natural fit | Named event groups with schema validation at load time |
| Game rules | Natural fit | Simple key-value store; belongs in runtime for unified save/load |
| Character interactions | Natural fit | Multi-scope schema with this=sender, from=receiver; not yet implemented |
| Navigation links | Natural fit | Compile-time pointer cast; type-checked link traversal |
| Localization expressions | Mostly natural | Expression resolution is pragmatic but effect tooltips require manual authoring (compiled effects are opaque) |
| GUI data binding | Natural fit | Pull model with atomic version counter is correct for cross-FFI |
| Traits/laws/progression | Better as Rust features | Composable 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.
6. Recommended Actions
Naming
- Rename
game-std->sers-std(repo:sers-std, crate:sers_std)
Architecture
- Feature-gate Tier 3-4 subsystems in the runtime crate with Cargo features (events, decisions, ai, conditional_modifiers, game_rules, concepts, graph, programs, connectors)
- Keep Tier 1-2 always-on (core lifecycle, schemas, templates, variables, stats, localization, entity queries, update, save/load)
- Wire ABI cap flags to Cargo features so they're compile-time
Modding Gaps
- Allow mods to register entity providers via template constant convention
- Allow mods to define on-actions via template constants
- Allow mods to define game rules via template constants
- Allow script-side conditional modifier conditions
Scope System
- 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