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.secsfiles.
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 anActivatechannel-source method, contract query dictionaries,GetField*accessors, and contract method dictionaries for command-producing void methods. Void contract methods useITemplateCommandContext, queue throughcontext.Commands, and flush after command-producing source statements. Lifecycle bindings live on the contract and point to those method ids; names likeOnBuiltare game vocabulary, not engine hooks. Each template self-registers aTemplateEntry. SeeGenerated/Templates/Buildings/Farm.csfor the canonical shape. - Channel declarations live in
Generated/Declarations.csasChannelDeclarationinstances with id, default, clamps, and contract. - Modifier declarations also live in
Generated/Declarations.csasModifierDeclarationwith bindings, triggers, duration,ReApplyMode, decay. - Hashes live in
Generated/Hashes.csasconst ulongFNV-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; seedocs/design/01-world-shape.md § "Canonical id string format"anddocs/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 inHashes.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(canonicalvalenar:channel/settlement/<name>) andH.<Name>_Template(canonicalvalenar:template/resource/<name>). The unsuffixedH.<Name>constant is retired — every callsite must pick the role explicitly. Channel-only commodities (Food, Metal, Population) keep the_Channelsuffix 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 inHashes.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.
- 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
- Systems live in
Generated/Systems/as classes implementingITickSystem. They self-register viaSystemRegistrationand are wired throughSecsModule. - Events live in
Generated/Events/and self-register similarly. - Activities live in
Generated/Activities/. OnActions live inGenerated/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.secssyntax inContent/, look at the spec indocs/, 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 aTemplateEntrydirectly. - 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 viaFnvHash.Compute— never write a bare PascalCase or dot-form constant. RegisterTemplateIdentifierregisters 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, andTryGetTemplateconsumer must quote the canonical id directly. TheTemplatesByNamedictionary onSecsModuleis a SEPARATE designer-facing display-name lookup ("Great Hall" → TemplateId) and remains in place — do NOT collapse it; do NOT register its keys viaRegisterTemplateIdentifier.- 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 multipleScopeFieldDeclarationrows; 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 atHashes.csWave 25.
When deleting Generated/ content
If you delete a template or system, delete:
- The
.csfile underGenerated/ - The corresponding
.secsfile underContent/ - The hash entry in
Generated/Hashes.cs - Any registration call in
Generated/SecsModule.cs - 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:
// Source: <relative-path-to-.secs-file>— real committed source exists.// auto-generated(or// Source: auto-generated …) — compiler will emit this; no.secsfile needed today.// Source: implicit …— compiler-inferred structural output, such as bare templates synthesized forcreate_entity, where no authored.secssource file exists.// 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 form | Why banned | Reference |
|---|---|---|
candidate_builder Name { ... } | Invented keyword not in committed list | docs/WAVE_REFACTOR_AUDIT.md § 1; docs/decisions/ADR-0002-behavior-vocabulary.md |
slot X.Y of Type; inside candidate_builder | Sub-clause of an invented keyword | same |
activity Y; inside candidate_builder body | Body clause of an invented keyword | same |
method ActivityRequest[] Build(...) inside candidate_builder | Body method of an invented keyword | same |
from collection X with builder Y | Invented selector source clause | same |
with builder X as a selector source clause | Sub-form of above | same |
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.