SECS — Scripting Engine C Sharp
A game-agnostic scripting and modding engine inspired by Paradox games (CK3, EU4, Victoria 3). SECS uses modified C# as its scripting language — .secs files are real C# with extensions for game scripting. The SECS compiler transpiles them to standard C# that integrates with the engine runtime.
Status: The runtime engine is functional, including the 6-phase HardOverride channel pipeline and activity/policy startup mod finalization. The Roslyn fork is present as secs-roslyn/ with Phase 0 complete. The SECS compiler surface is not yet built — all Valenar generated code is still hand-written. See TASKS.md for current status.
Architecture
src/
SECS.Abstractions/ Shared types (EntityHandle, Command, interfaces) — netstandard2.1
SECS.Engine/ Runtime engine (channel resolution, modifiers, pipeline, events)
SECS.Localization/ YAML-based localization provider
examples/
valenar/ Colony builder reference game (ASP.NET Core SignalR + React/Vite)
character-trainer/ Smaller training-loop example
map/ Map-generation prototypes (mapv10 is the active continent-scale direction)
The engine is game-agnostic. Channels, modifiers, contracts, scopes, and templates are all defined by the game — SECS provides the mechanisms.
Documentation
Start with the Design Reading Guide. That is the entry point for the live SECS docs and sends first-pass readers on to 00 — Overview. Historical, audit, and backlog material remains available under docs/, but the live design path is the authoritative reading route.
For a local docs portal, install the Docusaurus dependencies once:
npm install --prefix docs-site
Live authoring stays on a fixed loopback-only dev server:
npm run dev --prefix docs-site
That serves the portal at http://127.0.0.1:4174/secs/.
The stable personal-portal mode is build-first preview on loopback:
bash scripts/run-docs-portal.sh
That rebuilds the site, serves the built docs at http://127.0.0.1:4173/secs/, and starts a small loopback landing page plus zellij proxy at http://127.0.0.1:4180/. The landing page links to the docs portal and proxies non-root requests to the existing zellij web client on http://127.0.0.1:8082, with /zellij as the stable entry path. The underlying manual commands are npm run build --prefix docs-site, npm run serve --prefix docs-site, and npm run check --prefix docs-site.
The Docusaurus portal is built from generated publish content under docs-site/content/. Do not edit that generated directory directly; edit the source docs in docs/, examples/valenar/docs/, examples/map/mapv10/, .claude/rules/, or .github/, then rerun npm run check --prefix docs-site.
The portal syncs every Markdown file under docs/ and examples/valenar/docs/. For mapv10, it syncs the top-level mapv10 Markdown files plus the documentation-bearing subtrees (docs/, generator/, viewer/, schema/, viewer/src/{data,events,renderer,ui}/, prompts/, and verification/visual-foundation-audit/) without crawling generated map artifacts or dependency folders. npm run dev --prefix docs-site starts a watcher that resyncs source-doc changes while Docusaurus is running, so edits and newly added pages reload without restarting the dev server. npm run build --prefix docs-site and npm run check --prefix docs-site perform a fresh sync before building. For one-off manual syncs, run npm run sync --prefix docs-site.
Sidebars are generated from the synced doc tree by docs-site/sidebars/auto.js. Keep only high-level grouping rules there; do not hand-maintain leaf pages in the sidebars. New docs automatically appear in the left menu under their source-tree group, with unknown paths falling into an Other section until the grouping rules are intentionally refined.
On Linux, install the systemd user service for login-time autostart with:
bash scripts/install-docs-portal-user-service.sh
systemctl --user enable --now secs-docs-portal.service
systemctl --user stop secs-docs-portal.service
systemctl --user status secs-docs-portal.service
Use systemctl --user start secs-docs-portal.service for a one-shot manual start after installation. The installer writes ~/.config/systemd/user/secs-docs-portal.service with absolute paths for this checkout and the current npm and Node binaries; rerun it if the repo path or Node install changes. While the user service is running, its lifecycle hook points the tailnet root / at the repo-owned landing page so the base domain shows two clear options: Zellij and SECS docs. The landing page keeps zellij reachable at /zellij, while the docs portal stays on /secs/. When the user service stops, the lifecycle hook restores the old direct-zellij root route and removes only /secs. If Tailscale is missing or temporarily unavailable, the loopback docs portal still starts normally and only the tailnet route sync is skipped. If you want the user service available without an interactive login, enable linger separately with loginctl enable-linger "$USER".
For private access from another computer on the same tailnet, the user service now manages the /secs mount automatically during start and stop. The helper remains available for manual resync, ad-hoc use without the systemd service, and troubleshooting:
bash scripts/docs-portal-tailscale.sh enable
bash scripts/docs-portal-tailscale.sh url
bash scripts/docs-portal-tailscale.sh status
bash scripts/docs-portal-tailscale.sh disable
Those manual helper commands still manage only /secs. They do not change the root / route, which means users can continue to use enable and disable as the existing /secs-only control surface. The systemd service lifecycle hooks are the only place that swaps the tailnet root between the landing page and direct zellij. The resulting private URL shape for docs remains https://<host>.ts.net/secs/. This remains tailnet-only private access; it does not enable Funnel, public hosting, Caddy, nginx, or a broader reverse-proxy layer.
| # | Document | What it covers |
|---|---|---|
| 00 | Overview | Lowering-contract entry point, glossary, reading order, diagnostic map |
| 01 | World Shape | Scope declarations, contracts, top-level channel declarations, the H.* hash convention |
| 02 | Templates | template<Contract> Name { }, fields, intrinsic/dynamic channels in templates, CanBuild, lifecycle methods, bare templates |
| 03 | Channels and Modifiers | Modifiers, formulas, triggers, 6-phase target resolution recap, reads {}, ValidateDependencies() |
| 04 | Behavior | Systems, events (pulse + player choices), on-actions, activities, policies |
| 05 | Expressions | Scope sigils (@this/@root/…), resolve/increment/add_modifier/fire/save_scope_as, foreach, query keywords (any/every/count/random/first) |
| 06 | Mod Operations and Modding | inject / replace operation model, load order, typed identity, FNV-1a identity, slot schema, AST-level merger, conflict detection |
| 07 | Structured Template Data and Callables | C# source type binding, generated SecsTypeRef metadata, structured template fields, typed query returns, feature placement query contract |
| 08 | Collections and Propagation | ScopedList<T>, ScopedDictionary<TKey,T>, TemplateId, tags, modifier propagation, previous-tick bridge reads, aggregate channels, system phase/frequency slots, registry-only recipes |
| 09 | AI Policies and Activities | Policies, utility-AI scoring, activities, runtime player slots, mod injection examples |
| 10 | Host <-> SECS execution boundary | Boundary classification + assembly placement |
| — | Fork Decision | Roslyn fork vs source generator strategy |
| — | ADRs under docs/decisions/ and docs/adr/ | Architectural decisions that explain why the live design contract looks the way it does. Use alongside the numbered design docs when you need rationale rather than lowering detail. |
Key Concepts
- Templates define what entities ARE (static, contribute channels, lifecycle methods)
- Systems define what HAPPENS each tick (non-static, frequency-gated, phased)
- Events define what happens WHEN (pulse triggers, on_action hooks, player choices)
- Modifiers are reusable effect bundles that modify channel values
- Channels resolve through the implemented 6-phase target pipeline: base, additive, multiplicative, HardOverride, clamp, return.
- Formulas are dynamic channel contributions and modifier effects evaluated at resolution time
- Mod operations let layered content use
inject,replace,try inject,try replace,inject_or_create, andreplace_or_createagainst semantic slots. Activity/policy mods finalize at runtime startup; the full source-set merger for all declaration kinds is Phase 3 compiler work. Later writes win by load order, with conflicts reported for tooling.
Valenar Example
The primary example game is Valenar — a colony builder where a single MC explores an Estonia-sized region while the settlement survives demonic raids. It demonstrates:
- Region / Area / Province / Location scopes plus settlement and MC entities
- 11 building templates with costs, CanBuild checks, and formula-driven channels
- 3 systems: TaxCollection, FoodConsumption, PopulationGrowth (phased, load-distributed)
- 3 events: DemonRaid, PlagueSpreads, CelebrationBonus
- Dynamic formula channels (Farm scales with Fertility, Watchtower scales with Garrison)
- Modifier stacking policies (VictoryRally stacks x3, DemonDread is unique)
- ASP.NET Core SignalR server plus React/Vite client with Paradox-style tooltip work in progress
Building
dotnet build SECS.sln
Targets: netstandard2.1 (engine libraries, Unity-compatible), net8.0 (examples). Three test projects exist: tests/SECS.Engine.Tests/, tests/Valenar.Host.Tests/, tests/Valenar.Server.Tests/. Together they currently green ~580 tests.
Running Valenar
The Valenar example has a .NET server (SignalR) and a React client (Vite). During development, run both in separate terminals.
Server (SignalR hub on port 5062):
dotnet run --project examples/valenar/Server --urls http://localhost:5062
Client (Vite dev server on port 5173, proxies /gamehub to the server):
npm run dev --prefix examples/valenar/Client
Open http://localhost:5173 in your browser.