Skip to main content

SECS Core Concepts

The engine is game-agnostic. It provides mechanisms; the game defines the data through .secs content, compiler-owned generated output, and explicit retained host-owned surfaces. These seven concepts together form the SECS programming model — read docs/design/00-overview.md as the current entry point, then follow the numbered design docs for the live contract.

The seven definitions

ConceptWhat it isLifecycle
TemplateDefines what entities ARE. Static. Registers own-root intrinsic channel sources, implements contract queries (CanBuild, CanEquip, etc.), and implements contract void methods (OnBuilt, OnEquipped, etc.). Contract lifecycle bindings choose which void methods the runtime calls automatically.Registered at startup. Looked up by FNV-1a-64 hash of the template's canonical id string, which the compiler emits owner-less with the declared name reproduced verbatim: valenar:template/<Name> colon-slash (e.g. valenar:template/Farm; see docs/design/01-world-shape.md § "Canonical id string format" and docs/decisions/ADR-0003-canonical-id-grammar.md). The matching hash lives as H.Template<Name> in compiler-emitted Hashes.g.cs under the generated project obj/**/SecsGenerated output. When a name is dual-use (Wood, Stone, Gold are both Settlement scalar channels AND Resource templates), the compiler emits the distinct constants H.Channel<Name> (from valenar:channel/<Name>) and H.Template<Name> (from valenar:template/<Name>) so the channel and template hashes are distinct and the engine no longer needs to disambiguate by usage site.
SystemDefines what HAPPENS each tick. Non-static (per-tick instance). Frequency-gated, phased. Iterates entities via AllByContractForTick (zero-alloc ref struct).Runs in pipeline order during TickContext.Tick().
EventDefines what happens WHEN. Pulse-triggered, on_action-triggered, or player-choice-triggered.EventDispatcher evaluates triggers per-tick; player choices park on PendingChoice.
ModifierReusable effect bundle. Bindings + triggers + duration + stacking policy + decay. Can be ReApplyMode.Refresh, Stack, or Reject.Attached to entities by host or by OnAction. Decay handled by ModifierBindingStore.
FormulaDynamic value used by an intrinsic channel source or modifier effect at resolution time with owner context. Has access to scope walks.Evaluated lazily during channel resolution; a formula-backed channel BYPASSES the read-through cache and recomputes on every read against the owner's current state (the runtime has no readsChannels dependency graph yet, so caching on (entity, channel) alone could serve a stale value). Non-formula sibling reads inside the formula are still cache-served within the tick.
ScopeHierarchy node. In v2, scope walks traverse the kernel relation table (ScopeWalk relation kind per docs/runtime/rn-kernel-contract.md); the host-facing surface is IHostFieldBridge / IHostEntityBridge. Host defines parent-child relationships (e.g., Empire → County → Province → Building).Static hierarchy maintained by host; engine never mutates scope graph.
Mod operation / slot mergeLayered content change expressed with inject, replace, try inject, try replace, inject_or_create, or replace_or_create. Operations target compiler-owned semantic slots; later load-order writes win for the same slot.Resolved by the compiler pre-binding merge pass, with activity/policy runtime finalization as the current executable subset.

Entity & channel flow (the resolution pipeline)

  1. Registration. In v2, hosts construct a WorldSchema from compiler-owned output such as Generated.SecsModule.BuildSchema() and pass it to SecsRuntimeBuilder.Build(); templates, systems, modifiers, contracts, scopes, on_actions, activities, and policies are declared through the schema rather than registered imperatively.
  2. Activation. Host calls Activate(handle, contractId, templateId) for each entity. Engine looks up the template, attaches channel sources, then routes the contract's activation binding by ContractMethodRow.ImplementationKind: Generated uses generated handlers, HostBridge uses the host callable bridge, and ContractMethodId.None is the only no-op. Method names such as OnBuilt are game vocabulary, not hard-coded engine hooks.
  3. Resolution on demand. When the host or a system reads a channel, ChannelResolution runs the 6-phase pipeline documented in docs/design/03-channels-and-modifiers.md:
    • base — read host base for Base / Accumulative, or start at zero and add own-root intrinsic channel sources for Contributed
    • additive — sum additive contributions (+5, -2)
    • multiplicative — apply multiplicative contributions (*1.2, *0.85)
    • HardOverride — last-applied override replaces the post-multiply value
    • clamp — apply min/max clamps from declarations
    • return — final value; for a cache-eligible (non-formula) channel it is written into the kernel current-tick channel store, which doubles as the per-tick read-through cache. A later read of the same pair this tick is served from the cache unless the pair was marked dirty.
  4. Dynamic formulas re-evaluate every read using owner's current state (their other resolved channels, scope walks, etc.) — a formula-backed channel BYPASSES the cache entirely and recomputes on every read. This is the correctness-safe policy while the runtime has no readsChannels dependency graph; a non-formula sibling the formula reads is still cache-served within the tick.
  5. Dirty tracking. When an input changes the dirty set records the affected (entity, channel) pairs at the invalidation chokepoint — a host-field write through the cache-invalidating field bridge marks the field's dependent channels dirty, and a modifier-binding rebuild marks the target's modifier-affected channels dirty. The dirty-aware second ChannelResolveStep re-resolves only the dirty pairs; at end of tick SyncDirty writes the settled value of each Contributed-with-field channel back to the host through IHostFieldBridge in v2 (per docs/runtime/rn-external-engine-adapters.md § The Seven Host Bridges). Base and Accumulative channels are NOT written back — their host field is the source of truth, not a cache.
  6. Tick pipeline. Systems run in phase order (Pre → Main → Post). Each system iterates its contract's entities. Load distribution spreads expensive per-entity work across multiple ticks via AllByContractForTick.

Critical pattern: Templates are data

Host data and engine channels are deliberately separate. Base and Accumulative channels read host-owned fields as their source of truth. Contributed channels start at zero; the host field, when present, is only a cache that SyncDirty writes after resolution.

Template-body channel declarations lower to compiler-emitted channel sources on the template activation root. They are not a generic anonymous contribution mechanism, and they never push into a different scope. Cross-scope influence is expressed through named modifiers so ownership, teardown, tooltips, and mod operations all have a stable target.

Where to look for examples

Terminology split: keep stat for player-facing UI concepts (the Stats rail tab, stat tooltips, "No pinned stats" empty states, stat-category filters, file names like StatsExpansion.tsx / statCategories.ts); use channel for engine/runtime plumbing AND the data layer (ChannelResolver, ChannelCache, ChannelSourceStore, RegisterChannelSource, the channel keyword in .secs source, DTO channel: string fields, internal tip-IDs like channel:Food). Do not blindly global-rename — the prior global Stats→Channels sweep over UI labels was the wrong direction and had to be reverted; UI says "Stats", the data/engine layer says "channel".

  • src/SECS.Runtime/Channels/ChannelResolverV2.cs — v2 current six-phase channel pipeline
  • src/SECS.Runtime/Tick/TickContextV2.cs — v2 system execution order
  • src/SECS.Runtime/Events/EventDispatcherV2.cs — v2 event evaluation
  • src/SECS.Runtime/Boot/SecsRuntimeBuilder.cs — builds the runtime from a WorldSchema
  • src/SECS.Runtime/Mods/ModRegistryV2.cs — v2 mod accumulator
  • examples/valenar-v2/Generated/obj/**/SecsGenerated/*Templates.g.cs — compiler-emitted v2 concrete template-row patterns
  • examples/valenar-v2/Generated/obj/**/SecsGenerated/SecsModule.g.cs — compiler-emitted v2 schema entry point pattern