Provider commands — SimLOD 1.3#
SimLOD 1.3 adds a generic, opt-in way to change a provider's authoritative payload from ordinary game code. Accepted commands and their results are saved with the entity. Replaying an identical request returns its original receipt without invoking the provider or repeating the mutation.
This supplies the framework boundary needed by Frontier's planned start, recall, extension and delivery-acknowledgment actions. The framework does not implement those gameplay rules, take payments, transfer cargo or mutate native Pawns from a command.
Public API#
Implement SimLOD.API.ICommandSimulationProvider alongside either SimulationProvider or PawnSimulationProvider:
public ProviderCommandDecision PlanCommand(EntitySnapshot state,
ProviderCommand command, ProviderCommandContext context)
{
// Decode, account deterministic partial time, check domain rules, return a complete payload.
if (command.Kind != "my-action")
return ProviderCommandDecision.Refuse("unknown-command");
return ProviderCommandDecision.Accept(nextPayload, savedResult);
}
nextPayload and savedResult above stand for provider-computed values. The complete compilable example is CommandMeterProvider. It accounts its old rate through the effective command tick, then changes rate or adds a quantity. It is independent of Frontier and uses only the public API.
From the game's main thread, outside all provider/native callbacks:
var sim = Find.World.GetComponent<SimLODWorldComponent>();
var state = sim.Get(savedEntityId);
var receipt = sim.SubmitCommand(savedEntityId, state.Revision,
savedRequestKey, "set-rate", "3");
var observed = sim.GetCommandReceipt(savedEntityId, savedRequestKey);
Save the entity ID, command intent and request key in the caller's own Scribe root. Do not invent a new key after an uncertain response. Accepted receipts are durable; all other statuses are observations, not stored commands. GetCommandReceipt returns NotFound for a valid known entity without that key. Unknown entities, invalid arguments, wrong-thread/reentrant access, and blocked archive/native journal access throw according to the existing facade convention. Never parse an exception message to choose an economic or recovery action.
Admission and response order#
The native facade validates the abstract archive and native journal before both submission and receipt lookup. New commands cannot bypass a broken native journal. The common native validator also checks retained owners and native/core lock consistency on first attachment.
After access/input validation, an existing accepted key is checked before current revision, provider availability, native lock and lateness. Equal kind and arguments return the original immutable receipt; changed arguments or kind return FingerprintMismatch. Expected revision is an admission guard, not part of the replay fingerprint. Replay works during native work, pause, quarantine and provider absence because it applies no work.
For new keys, the response precedence is:
| Status | Meaning |
|---|---|
NativeLocked |
Finish or explicitly settle the existing native plan. Includes pending, faulted and prepared work. |
ProviderUnavailable |
Saved provider is missing or incompatible. Preserve the intent and matching code requirement. |
EntityUnavailable |
Record is paused, quarantined or otherwise not Ready. Inspect its current state. |
Unsupported |
Compatible provider does not implement the opt-in interface. |
StaleRevision |
Obtain current state and revalidate the domain intent/quote. |
CatchUpRequired |
Next logical deadline is due; allow normal bounded scheduled work first. |
RevisionExhausted |
No revision increment is representable. No callback or mutation. |
CapacityExceeded |
Saved receipt history would exceed a documented limit. No receipt is evicted. |
Refused |
Provider returned a normal domain refusal; use its stable ReasonCode and explanatory Detail. |
ProviderFailure |
Planner threw, returned null/invalid data, or failed payload validation. The record is quarantined; no payload/revision/RNG/receipt commit occurred. |
Accepted |
Complete payload plus receipt committed once, including accepted same-payload/no-op domain actions. |
History byte capacity is checked again after the bounded result is known. That refusal may follow a pure callback, but still commits nothing. Expected domain refusals do not quarantine, consume a key, increment revision, or accumulate history. A corrected request may reuse an unaccepted key; the caller must first establish that no accepted receipt exists.
On provider failure, failure/status metadata and queue eligibility change to quarantine. Original payload, simulation times, random state, revision and prior receipts remain intact. Fix the cause and explicitly use Retry; it is not an automatic per-frame retry mechanism.
Time, randomness and native ownership#
ProviderCommandContext exposes EffectiveTick and LogicalTick. It supplies no RNG and no native facade. A command must not call Advance itself, draw from native randomness, read a mutable command singleton, access another entity, or mutate native objects.
Admission requires NextTick > current game tick. A command does not advance logical time, rebase the deadline, change the interval, consume RNG, change schema/model, or call a scheduled step. New command effective ticks cannot go backwards. A successful command increments revision exactly once.
The provider may account deterministic partial time through the effective tick using a saved cursor, then apply the change. Later scheduled steps account only previously uncovered time. If partial-time accounting would cross an unresolved random event, the provider must return a stable domain refusal such as boundary-pending until scheduled work resolves it. This is a provider reason code under Refused, not a framework status or permission to simulate hidden random work.
Stable Pawn providers can opt in. Commands may change domain fields such as funding, mission choice and acknowledgment state. Representation/member accounting changes remain the responsibility of materialization/reconciliation/rematerialization/settlement callbacks. A provider cannot use commands to invent retained people, rewrite native membership, release a lock or change an already prepared plan. Generic payload validation cannot prove arbitrary provider-specific conservation; provider authors must implement and test those invariants.
The existing Pawn pause restriction is unchanged. Use cheap inactive-domain updates for a standby crew. Retained Pawns remain unticked. No item transaction, arbitrary Pawn ingestion, map retargeting, event bus or cross-record transaction is introduced.
Immutable receipt and persistence#
Each accepted receipt retains entity ID, key, kind, exact arguments, effective and logical ticks at acceptance, previous/committed revisions, original provider schema/model and the provider's result string. Later schema migration does not reinterpret or overwrite historical receipt results. Nonaccepted receipt fields describe the attempted/current observation; only Accepted fields are a durable historical fact. NotFound uses effective tick -1.
Archive 4 writes a required per-entity commands section, with count and SHA-256 integrity check covering ordered command metadata and encoded arguments/results. Malformed/duplicate/missing fields, bad UTF-8, out-of-order or impossible revisions/times, invalid version metadata, over-limit histories and checksum mismatches block the whole archive and preserve the original bytes. Checksums detect corruption; this is not a tamper-proof security ledger.
Archives 1–3 load with empty command history. Native journal remains 3, RNG remains SplitMix64-v1, provider API major remains 1, and the framework CLR identity remains 0.1.0.0. Older framework versions preserve archive 4 as blocked; downgrade is unsupported. Keep a pre-upgrade save when an older framework remains necessary.
Both the accepted payload and receipt are part of one main-thread commit and the same Scribe archive. Commit has no provider or native callback. Supported guarantees concern ordinary completed save boundaries, not power loss mid-save, OOM, reentrant saves or arbitrary forbidden external side effects. Frontier's native payment/delivery journals must separately coordinate their effects with these receipts.
Limits and cost#
- Key: 1–160 printable ASCII characters; scoped per entity.
- Kind and refusal code: 1–80 lowercase ASCII letters, digits, dots, underscores or hyphens.
- Arguments: at most 8,192 valid UTF-8 bytes, exact ordinal comparison.
- Result or refusal detail: at most 2,048 valid UTF-8 bytes.
- Accepted payload: existing 256 KiB UTF-8 limit.
- At most 4,096 accepted receipts per entity and 65,536 per world.
- At most 16 MiB of accounted command history per world: exact variable UTF-8 bytes plus 128 bytes per receipt for accounting. This is not a measurement of CLR heap/XML size.
Receipts are never silently pruned. Rejected commands do not accumulate. Limits are admission guards, not performance promises or a long-running campaign capacity certification. Lookup is indexed by entity/key. Save/restore cost grows with retained history. Ordinary ticks do not scan it, and providers without accepted commands do not retain an allocated command journal. Query a known key instead of inventing a per-tick whole-history scan.
Compatibility and downstream handoff#
SimLOD.API.ProviderCommandContract.Version is a public constant with value 1. A baseline-only bootstrap should find the type by name on the loaded SimLOD assembly, then inspect the public literal using GetRawConstantValue() before loading code/types that require the interface. A directly compiled constant is inlined and is not a runtime capability check. CLR 0.1.0.0 cannot distinguish the capability. The bootstrap must not reference new command types in a path that loads before the check. The marker does not by itself make a new provider loadable against old DLLs. Test the actual consumer's missing/old-framework startup behavior.
The frozen 1.2 baseline is preserved and checked with declaring-type scope. The reviewed 1.3 surface contains 386 metadata lines, retaining all 311 old entries and adding 75. Existing binary consumers continue to use their old entry points.
The Frontier handoff maps the new capability to the companion's pending M2 work. See the current acceptance report for executed evidence rather than treating this contract as proof of tests that have not run.