Abstract runtime API 1.x#
This is the implemented abstract-record API; see the v1 compatibility contract. The optional pawn lifecycle extension adds native operations in 0.2. The larger architecture proposal includes future capabilities. Source lives in Source/SimLOD, with separate optional examples under Examples.
Provider authoring#
Subclass SimLOD.API.SimulationProvider and register the class through a SimLOD.Integration.SimulationProviderDef. The provider ID must be stable, lowercase ASCII (a-z, 0-9, ., _, -) and unique across loaded providers. Duplicate IDs or invalid registration stop runtime initialization visibly while preserving the existing saved archive.
Implement these members:
| Member | Contract |
|---|---|
ProviderId |
Stable identity stored as a string in every record. Never derive from an assembly-qualified class name. |
CurrentSchema |
Positive integer payload format version. |
ModelVersion |
Positive integer behavior version; defaults to 1. A mismatch suspends the record until a compatible provider is available. |
ApiMajor |
Defaults to 1. Compatible preview contract 0 remains accepted during 1.x; other majors are refused. |
Validate(payload) |
Validate current-schema data, throwing on invalid input. Pure, bounded, and main-thread. |
Advance(payload, context) |
Return the complete next payload for exactly FromTick → ToTick. Do not mutate the world, globals or another entity. |
Migrate(payload, fromSchema) |
Optional migration of exactly one version. Framework chains upgrades and validates the final result before committing. |
Payloads are immutable strings with provider-defined meaning, limited to 256 KiB of valid UTF-8 per entity. They are base64-encoded inside the archive so CR/LF, NUL and Unicode survive XML unchanged. null and invalid surrogate sequences are rejected. This first runtime parses small synthetic payloads per update; a cached typed codec API is deferred until real provider needs justify it.
Example registration:
<Defs>
<SimLOD.Integration.SimulationProviderDef>
<defName>MyMod_SimulationProvider</defName>
<label>my simulation</label>
<providerClass>MyMod.MyProvider</providerClass>
</SimLOD.Integration.SimulationProviderDef>
</Defs>
Use XML for provider discovery; there is no mutable global registration API or required Harmony patch. Provider instances are created per world. Keep their IDs/versions constant and store authoritative simulation state in the payload, not in provider fields. A provider's defining Def/assembly can be absent because neither is needed to deserialize its records.
World facade#
From normal game code on the main thread:
var simulation = Find.World.GetComponent<SimLODWorldComponent>();
EntityId id = simulation.Create("my.mod.provider", initialPayload, interval: 250, seed: 42);
EntitySnapshot state = simulation.Get(id);
IReadOnlyList<EntitySnapshot> mine = simulation.GetEntities("my.mod.provider");
SimulationDiagnostics diagnostics = simulation.GetDiagnostics();
Create uses the current native game tick as the initial logical time. Intervals must be positive; the first deadline is start + interval. Entity IDs are positive monotonically allocated integers scoped to this save. Branching a save makes a separate world; cross-save identity/import is unsupported. Failed creation does not consume an ID.
Get throws for an unknown ID. Snapshots and enumeration results are immutable observations, sorted by numeric ID. Provider-filtered lookup uses an index; unfiltered enumeration is an explicit diagnostic/query operation. There is no per-tick global entity scan.
Retry(id, expectedRevision) explicitly revalidates a suspended record; it does not immediately simulate it. A stale revision is rejected. Retrying a ready or validated paused record returns false and cannot enqueue it twice. Successful retry preserves overdue logical time. A saved pause flag remains paused after retry; otherwise the scheduler catches up normally. Repeated failures stay quarantined; they do not automatically retry every tick.
Since 1.2, Pause(id, expectedRevision) removes eligible abstract work from scheduling, and Resume(id, expectedRevision) requeues its unchanged deadline. A changed flag increments revision once and returns true; a current-revision no-op returns false. Neither call advances state. Resume catches up missed intervals through ordinary future ticks and the existing quota. IsPaused preserves intent independently of provider/quarantine status. Pawn providers and any native member/operation history are excluded. See dormancy for the full policy.
Describe() and ExportArchive() are diagnostics. Export does not constitute a supported import/edit API. There is no public arbitrary payload setter, delete operation, or tick-rate mutation. Native operations use the separate pawn lifecycle API. A record with a pending native transition cannot be unlocked using ordinary Retry.
Time, randomness and error behavior#
Work is ordered by (next logical tick, numeric entity ID), with one queued entry per ready entity. Each world tick attempts at most 16 steps by default; failed attempts consume the quota too. The quota is saved in the archive. Providers receive fixed logical intervals even when dispatch is late, and the next deadline derives from the previous deadline. A duplicate dispatch at the same tick does no work.
StepContext.Random is a local SplitMix64-v1 stream. Its state commits together with the new payload, logical time and revision only after the callback and validation succeed. A throwing provider leaves all four unchanged. NextInt(max) uses rejection sampling for an unbiased bounded draw. Do not call Verse.Rand, System.Random, wall clocks or native generation inside a synthetic step.
The runtime catches provider errors, records a bounded diagnostic reason, removes that entity from scheduling and continues other eligible work. It cannot undo native/global side effects a provider performed in violation of its contract. Main-thread ownership and reentrant world access are guarded. Registration and archive initialization are lazy at the first game API/tick entry because RimWorld may run loading work on a long-event thread.
Logical quotas are cooperative limits, not millisecond preemption. The facade records observed tick durations, but timing never influences scheduling. Under overload, oldest lateness grows visibly; no time is silently dropped. Only independent own-state steps are supported; cross-entity interactions and reliable events are deferred.
Persistence and compatibility boundaries#
The abstract save field is a framework-owned base64 archive string on SimLODWorldComponent. Archive v3 stores next ID, operation quota, last dispatch tick, RNG version and sorted records. Each record contains provider/schema/model, payload checksum, interval, logical/next ticks, revision, pause flag, RNG state, quarantine reason, and native transition lock/reason. Archives v1/v2 default to unpaused; v1 also upgrades with empty native locks. Runtime indexes, provider classes, callbacks and heap layout are rebuilt. A separate native journal and deep Pawn holder share this component.
| Loaded condition | Behavior |
|---|---|
| Provider available and compatible | Validate/migrate, then schedule its saved deadline unless paused. |
| Compatible and paused | Paused; retain state/deadlines without a queue entry until explicit resume. |
| Provider absent | ProviderMissing; retain payload, identity, RNG, revision and deadlines across another save. |
| Provider schema newer than available code or model mismatch | ProviderIncompatible; preserve data without mutation. |
| Provider validation/upgrade fails | Quarantined; preserve original payload/schema/time/RNG with diagnostic reason. |
| Prior quarantine | Remains quarantined across reload until explicit retry. |
| Pending native transition | TransitionPending or TransitionFaulted; do not migrate or schedule until explicit native recovery succeeds. Provider absence/incompatibility takes precedence. |
| Malformed archive, invalid counters/duplicate IDs/checksum, future format/RNG | Freeze the archive; preserve its original string on subsequent saves and reject new mutations. |
The abstract archive contains no native references; optional native members are stored separately. Removing mods defining native Pawn/Thing/Hediff classes or Defs is outside this promise. Removing SimLOD itself cannot be made safe by code that is no longer loaded. The 100,000-record and per-payload admission limits are validation limits, not verified performance capacities.
What remains outside this stage#
No arbitrary existing-pawn conversion, map lifecycle management, world markers, location/faction indexes, fidelity profiles, event-only cadence, shared interactions/event bus, automatic LOD, background simulation or Multiplayer adapter. The supported API is frozen under the v1 contract; those additional capabilities remain future scope. These require their own evidence gates. The example providers exercise the public extension boundary but do not substitute for independently authored ecosystem integrations.