Build a SimLOD provider#
Start with the SimLOD.ProviderStarter.zip source kit from the 1.3 release, or produce it with this checkout's build. It builds outside this repository against an extracted public SimLOD DLL and your installed game assemblies. The kit README gives the command and a five-minute native smoke test. It creates one clock on explicit request and demonstrates a saved consumer link without involving Pawns.
For player-directed domain changes, add the optional 1.3 provider-command interface. A pure planner returns a complete validated payload or a typed refusal. SimLOD commits the payload, revision and accepted receipt together. Keep physical payments/cargo and their recovery in consumer-owned state; a command is not an item transaction. The command meter demonstrates partial-time accounting and repeat-safe requests. Command-dependent types require the 1.3 public DLL; the unchanged CLR version cannot establish capability.
Choose durable identities first#
Before distributing a derived mod, replace simlod.starter.clock, simlod.providerstarter, SimLOD_StarterClockProvider, the assembly/namespace and SimLOD_Starter_ translation prefix. Update the registration XML and build script paths together. Keep the framework dependency and its real download URL. Use a unique lowercase ASCII provider ID containing only letters, digits, dots, underscores or hyphens.
Provider IDs and consumer Scribe names are saved contracts. Renaming them after release can strand records. An entity ID belongs to one saved world; persist its scalar value in your consumer's WorldComponent. Keep the authoritative payload, logical time and RNG in the framework. A repeated UI command should reuse its saved entity link. Do not create entities in constructors, static initialization or every tick. Restore the link when its provider is missing instead of creating a replacement.
Implement a pure bounded update#
Subclass SimulationProvider, declare the stable ID and schema, validate the entire payload and return the complete next payload from Advance. Register the class with a SimulationProviderDef; no Harmony patch or example dependency is needed.
| Concern | Rule |
|---|---|
| Payload | Invariant serialization, explicit bounds, valid UTF-8 and at most 256 KiB. Authoritative state belongs here, not in provider instance fields. |
| Logical time | Use context.FromTick, ToTick and DeltaTicks. Late dispatch still receives one fixed interval. Clamp before addition and bound loops independently of arbitrary elapsed time. |
| Randomness | Use context.Random. Never use Verse.Rand, a wall clock, System.Random or native generation in an abstract callback. |
| Side effects | Validation, migration and planning must not mutate native objects, other entities or globals. Framework rollback cannot undo those effects. |
| Thread and timing | Access the facade on the native main thread, outside callbacks. Do not save or re-enter the world facade inside a provider callback. |
| Completion | A completed payload alone remains scheduled. In framework 1.2, consumer code can explicitly Pause(id, revision) an eligible abstract record outside callbacks; its history remains saved. There is no deletion or cadence-change call. |
The clock's additive, capped update is independent of interval partitioning. A stochastic domain needs its own documented draw boundaries; interval changes are not automatically equivalent just because both runs use the same seed. Survey Relay shows fixed hourly draw boundaries and completion stability. The abstract API explains quotas, catch-up and quarantine.
Version payload and behavior separately#
CurrentSchema describes the payload format. Increment it when the stored representation changes, and implement pure Migrate(payload, fromSchema) steps, one version at a time. Test each released input. The runtime validates the final result and preserves the original record when migration fails.
ModelVersion describes simulation behavior. A mismatch suspends a record; increasing it is not a migration mechanism. Decide and document how existing saved records remain compatible before releasing changed semantics. ApiMajor = 1 selects this framework contract. Package 1.3 keeps the framework CLR identity, API major and native journal 3. Abstract archive 4 saves pause intent and accepted command history; formats 1–3 load without receipts, and formats 1–2 default to unpaused. Never version a provider ID to bypass an unresolved migration.
Handle absence and recovery visibly#
Read current snapshots for decisions. Do not cache a revision across game updates and never parse exception text: messages are explanatory, can be translated and are not a machine contract.
| State | Consumer behavior |
|---|---|
Ready |
Allow appropriate domain actions using current state. |
Paused |
No scheduled callbacks. Explicit Resume(id, currentRevision) catches up missed fixed intervals on later ticks. |
ProviderMissing / ProviderIncompatible |
Preserve the saved link and ask for the matching provider; restore the mod and reload. |
Quarantined |
Correct the provider/payload cause, then explicitly use Retry(id, currentRevision); do not retry every frame. |
TransitionPending / TransitionFaulted |
Inspect the native operation. Ordinary abstract Retry cannot unlock it. |
| Archive initialization blocked | Preserve the save and report the diagnostic; do not replace the archive or create substitute records. |
Read pause/resume for eligibility and catch-up policy. Inspect IsPaused even when a missing-provider or quarantine status hides Paused. Successful Retry revalidates a paused record without scheduling it. The starter uses the base 1.0/1.1 API to stay compatible with those DLLs; Survey Relay demonstrates the 1.2 additions.
For native providers, keep a durable encounter/request key in consumer state before the queue can execute its first effect. Admission itself is synchronous and has no queued native member effects, so saving the returned key immediately after successful admission follows that rule. Refused admission must not consume a visit or overwrite the previous key. Replaying the same request returns its saved receipt; changing its fingerprint is refused. Use fresh revisions for new admissions, and distinct saved settlement keys when observations change.
Native failures preserve their original plan and applied progress. Inspect blockers, correct their cause, then recover or prepare/review/commit an explicit settlement. A prepared settlement does not move Pawns. Do not fabricate a missing body, take a Pawn from a foreign holder or delete a receipt to force progress. Read the Pawn lifecycle, queue and settlement contracts before adding this capability. The clock starter intentionally needs none of it.
Validate before a first release#
Run the isolated public-DLL build and confirm that only your DLL ships in your assembly folder. Use a disposable native save to check explicit creation, repeated commands, actual time advancement, save/load and provider restoration. For a combined provider/consumer assembly, remove only the provider registration in that test copy so the consumer's save root remains loaded. Back up real saves before upgrades.
For your domain, test malformed payloads, bounds/overflow, deterministic reload, migration success/failure, and every claimed cadence equivalence. A model test can exercise domain math; only native tests establish XML discovery and Scribe behavior. If you add Pawns, add partial-operation saves, ownership changes, failure/recovery and replay tests. Ship no game DLLs or test fixtures.
The repository's own checks are:
./tools/build.ps1
./tools/test-starter.ps1 -FrameworkPackageRoot 'C:\Mods\SimLOD'
./tools/test-beta.ps1 -Suite Starter
python tools/verify-localization.py --negative-control
./tools/test-api.ps1 -NegativeControl
The native starter scenario uses its compiled public DLL alone with Core and SimLOD. Broader game/mod compatibility still needs explicit evidence; an example passing does not certify a new provider. See the support policy and v1 compatibility contract.