Skip to main content

Generated Code Is Hand-Written (Until the Compiler Exists)

Files under examples/valenar/Generated/ look like compiler output, but they are hand-written. The SECS compiler (a Roslyn fork — see secs-roslyn/) does not yet emit them; humans do, by translating .secs source files in examples/valenar/Content/ to the C# patterns the future compiler will produce.

Why both Content/ and Generated/ exist

  • examples/valenar/Content/*.secs — the future source of truth. This is what designers will eventually write and the compiler will consume. Today it documents intent.
  • examples/valenar/Generated/*.cs — the current source of truth that the engine actually runs. Hand-translated from the .secs files.

When you add or change game content, update both files. Treat the .secs file as the design spec and the .cs file as the build output you happen to be producing manually. The two must stay in sync — drift between them will silently break the eventual compiler bring-up.

What "matches the future compiler" looks like

The patterns in Generated/ are not arbitrary — they are the contract the future compiler must satisfy. When writing new generated code, mimic existing files exactly:

  • Templates lower to static class Name { ... } with an Activate channel-source method, contract query dictionaries, GetField* accessors, and contract method dictionaries for command-producing void methods. Void contract methods use ITemplateCommandContext, queue through context.Commands, and flush after command-producing source statements. Lifecycle bindings live on the contract and point to those method ids; names like OnBuilt are game vocabulary, not engine hooks. Each template self-registers a TemplateEntry. See Generated/Templates/Buildings/Farm.cs for the canonical shape.
  • Channel declarations live in Generated/Declarations.cs as ChannelDeclaration instances with id, default, clamps, and contract.
  • Modifier declarations also live in Generated/Declarations.cs as ModifierDeclaration with bindings, triggers, duration, ReApplyMode, decay.
  • Hashes live in Generated/Hashes.cs as const ulong FNV-1a-64 constants computed from canonical id strings. Every named entity (template id, channel id, modifier id, system id, event id, activity id, policy id, need id, selector id, rule id, slot kind id, scope id, contract id, tag id, phase id) gets a hash. The canonical id format is <namespace>:<kind>/[<owner>/]<name> colon-slash (Wave 5; see docs/design/01-world-shape.md § "Canonical id string format" and docs/design/06-overrides-and-modding.md § 4.2). Bare-name and dot-form (valenar.channel.x) hashes that earlier waves committed are no longer live identity targets — deferred families carry an explicit per-block deferral comment in Hashes.cs. Hash collisions are caught at registration.
    • Dual-use commodity split (Wave 5b). When a name is used as BOTH a Settlement scalar channel AND a Resource template id (Wood, Stone, Gold), the constants live as the explicit pair H.<Name>_Channel (canonical valenar:channel/settlement/<name>) and H.<Name>_Template (canonical valenar:template/resource/<name>). The unsuffixed H.<Name> constant is retired — every callsite must pick the role explicitly. Channel-only commodities (Food, Metal, Population) keep the _Channel suffix even though no template variant exists, so the role is still explicit at the callsite.
    • Mod-local rule / need / selector ids. Rules and other policy-substructure ids defined inside a mod live in Generated/Mods/<Name>Mod.cs (NOT in Hashes.cs), use the mod's source-set namespace, and carry the owning policy as the canonical owner segment: <source_set>:rule/<policy_name>/<rule_name> (e.g. cautious_survival:rule/character_survival/flee_on_low_hp). Collisions between mods are impossible by construction.
  • Systems live in Generated/Systems/ as classes implementing ITickSystem. They self-register via SystemRegistration and are wired through SecsModule.
  • Events live in Generated/Events/ and self-register similarly.
  • Activities live in Generated/Activities/. OnActions live in Generated/OnActions/.
  • The registration entry point is Generated/SecsModule.cs. It is the single function the host calls to install all definitions. New templates/systems/events must be wired up here.

Hard rules

  • Do not invent patterns. If a new construct does not exist in Generated/ yet, look at the corresponding .secs syntax in Content/, look at the spec in docs/, and choose the lowering that the compiler will most plausibly produce. Then check it against what the existing files do for analogous constructs.
  • Do not bypass SecsModule. Every registration goes through it. The host should never construct a TemplateEntry directly.
  • Do not use reflection in Generated/. The future compiler will emit static, AOT-friendly code. Reflection at startup would break Unity IL2CPP and would not match what the compiler can do.
  • Field hashes use FNV-1a-64 over the canonical id string. Computed identically to secs-roslyn's hash function so the eventual compiler output will match. The canonical id string is <namespace>:<kind>/[<owner>/]<name> colon-slash (Wave 5). When you add a new id, write the canonical string first, then hash it via FnvHash.Compute — never write a bare PascalCase or dot-form constant.
  • RegisterTemplateIdentifier registers ONLY the canonical string (Wave 5b). The dual-alias bridge that registered both the canonical id and the bare C# name (nameof(BareLocation) = "BareLocation") was collapsed; every WorldData JSON file, MapGenerationSystem fallback, test fixture, and TryGetTemplate consumer must quote the canonical id directly. The TemplatesByName dictionary on SecsModule is a SEPARATE designer-facing display-name lookup ("Great Hall" → TemplateId) and remains in place — do NOT collapse it; do NOT register its keys via RegisterTemplateIdentifier.
  • Scope-field hashes that share a name across scopes still share their H.* constant. H.OwnerId, H.CellId, H.LastCapitalChangedDay, etc. are intentionally one constant referenced by multiple ScopeFieldDeclaration rows; the engine distinguishes by (ScopeId, FieldId). Do not migrate these to per-scope canonical strings without a separate ADR — that would break the deliberate field-id sharing pattern documented at Hashes.cs Wave 25.

When deleting Generated/ content

If you delete a template or system, delete:

  1. The .cs file under Generated/
  2. The corresponding .secs file under Content/
  3. The hash entry in Generated/Hashes.cs
  4. Any registration call in Generated/SecsModule.cs
  5. Any host-side code that referenced the deleted name

The compiler will eventually do steps 3-4 automatically, but today they are your responsibility.

No-source stand-in marker

When a Generated stand-in has no real .secs source file yet — because the underlying SECS keyword is a runtime/compiler-lowering shape with no committed source syntax — the file must carry this exact header comment:

// Hand-written stand-in: no .secs source yet - runtime-only surface.

This is the canonical text that tests/Valenar.Host.Tests/GeneratedProvenanceTests.cs's bypass recognises (added in Recovery Wave A2). Do not invent a paraphrase. The four legal header forms are:

  1. // Source: <relative-path-to-.secs-file> — real committed source exists.
  2. // auto-generated (or // Source: auto-generated …) — compiler will emit this; no .secs file needed today.
  3. // Source: implicit … — compiler-inferred structural output, such as bare templates synthesized for create_entity, where no authored .secs source file exists.
  4. // Hand-written stand-in: no .secs source yet - runtime-only surface. — the runtime/lowering surface has no committed source keyword; the C# shape is kept as the working implementation while the source syntax is unresolved.

Banned source-form syntax

The following tokens must not appear in .secs files under examples/valenar/Content/ or in any Generated // Source: comment pointing to a hypothetical .secs file:

Banned formWhy bannedReference
candidate_builder Name { ... }Invented keyword not in committed listdocs/WAVE_REFACTOR_AUDIT.md § 1; docs/decisions/ADR-0002-behavior-vocabulary.md
slot X.Y of Type; inside candidate_builderSub-clause of an invented keywordsame
activity Y; inside candidate_builder bodyBody clause of an invented keywordsame
method ActivityRequest[] Build(...) inside candidate_builderBody method of an invented keywordsame
from collection X with builder YInvented selector source clausesame
with builder X as a selector source clauseSub-form of abovesame

The canonical runtime/compiler-lowering shapes that remain are CandidateBuilderId, CandidateBuilderDelegate, SelectorSource.FromCollection(...) — these are C# types, not .secs source syntax. Plain C# with these types is allowed in Generated stand-ins and engine code.

The consider activity X from all_registered selector clause is COMMITTED source syntax and is not banned.

If you would have written candidate_builder in a .secs file, there is no committed source equivalent. Express the selection logic as a registered CandidateBuilderDelegate in a Generated stand-in with the no-source marker, and leave the source syntax for a future ADR.

Fabricated source files

Never create a .secs file to satisfy tests/Valenar.Host.Tests/GeneratedProvenanceTests.cs or any other provenance check. If a Generated stand-in requires a // Source: pointer and no committed source exists, use the canonical no-source stand-in marker instead. The provenance test recognises that bypass (Recovery Wave A2). Creating a fabricated .secs file with invented syntax as a workaround is a governance violation that will be caught by the CI guard and the test GeneratedCitedSourceFilesMustNotContainBannedSyntaxForms, and must be reverted.