Skip to main content

SECS Core Concepts

The engine is game-agnostic. It provides mechanisms; the game (via .secs content + Generated/ code) defines the data. 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 (valenar:template/<owner>/<name> colon-slash; see docs/design/01-world-shape.md § "Canonical id string format"). The matching hash lives as H.<TemplateName> in Generated/Hashes.cs. When a name is dual-use (Wood, Stone, Gold are both Settlement scalar channels AND Resource templates), Wave 5b splits the constant into H.<Name>_Channel and H.<Name>_Template 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; result cached in ChannelCache until invalidation.
ScopeHierarchy node. Walked via ISecsHostReads.WalkScope. 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. Host calls SecsModule.Initialize(registry) at startup — templates, systems, modifiers, contracts, scopes, on_actions, activities, and policies all land in SecsRegistry.
  2. Activation. Host calls Activate(handle, contractId, templateId) for each entity. Engine looks up the template, attaches channel sources, then invokes the contract's ContractLifecycleIds.Activation binding if that binding points at an implemented void method. 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, written into ChannelCache for the current tick
  4. Dynamic formulas re-evaluate every read using owner's current state (their other resolved channels, scope walks, etc.) — they bypass the cache where appropriate.
  5. Dirty tracking. When a channel changes, DirtySet records it. At end of tick, SyncDirty writes back to host via ISecsHostWrites.
  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.Engine/Resolution/ChannelResolution.cs — the current six-phase channel pipeline
  • src/SECS.Engine/Pipeline/TickContext.cs — system execution order
  • src/SECS.Engine/Events/EventDispatcher.cs — event evaluation
  • src/SECS.Engine/SecsRegistry.cs — definition registration, validation, and shipped activity/policy mod finalization
  • examples/valenar/Generated/Templates/ — concrete template patterns (the spec for what the SECS compiler will eventually emit)
  • examples/valenar/Generated/SecsModule.cs — registration entry point pattern