Struct TickContext
Context passed to CallbackSystem and QuerySystem delegates during tick execution. Provides a valid Transaction for entity operations and a factory for side-transactions.
public struct TickContext
- Inherited Members
Remarks
Each CallbackSystem/QuerySystem receives its own TickContext with a dedicated Transaction created on the worker thread (respecting Transaction's single-thread affinity). The Transaction is committed automatically after the system completes — systems must NOT commit or dispose it.
Pipeline systems do NOT receive TickContext — they use an Action<T1, T2> and access entity data through Gather/Scatter pipelines (separate mechanism).
Fields
NonWorkerId
Sentinel WorkerId for a context that is not executing on a scheduler worker — the runtime lifecycle hooks
(OnFirstTick, OnShutdown), which run on the tick thread or the caller's thread rather than on a dispatched worker.
public const int NonWorkerId = -1
Field Value
Remarks
Deliberately negative so that indexing a per-worker array with it throws instead of silently aliasing worker 0 (#860). Code that indexes per-worker storage by WorkerId must either be unreachable from a lifecycle hook or handle this value explicitly.
Properties
Accessor
Per-worker EntityAccessor for parallel QuerySystems that do NOT write Versioned components. Provides Open/OpenMut with warm ChunkAccessor caches, zero per-entity dictionary overhead. Null when the system uses Transaction-based access (WritesVersioned=true or non-parallel systems).
public EntityAccessor Accessor { readonly get; init; }
Property Value
AmortizedDeltaTime
Elapsed time in seconds since the last tick this system processed the work it is about to process (issue #231, #1293) — the effective integration step
for movement, decay, or a state machine that advances once per cycle. Equal to DeltaTime for a system that runs every tick over its whole
selection; otherwise DeltaTime × TickDivisor × CellAmortize.
public float AmortizedDeltaTime { readonly get; init; }
Property Value
Remarks
Use this, not DeltaTime, for anything that integrates. The two coincide only for an undivided, unamortized system, and the
difference is silent: a system at TickDivisor 10 integrating by DeltaTime advances at a tenth of real time with nothing raised.
Both strides count, and they multiply rather than compete — the divisor skips whole runs, cellAmortize skips clusters within a run — so a
cluster in a TickDivisor 2 + cellAmortize 3 system is revisited every six ticks and steps by six periods (DSEL-03).
ChunkCount
Total number of chunks for chunked-parallel systems. For non-chunked systems, always 1.
For a ChunkedCallbackSystem, the value passed to ChunkedParallel(int) (or its Prepare's). For a parallel
QuerySystem, the live dispatch's count, which CostBasedChunking sets from measured cost: it can change from one tick
to the next and reach twice round(WorkerCount × ChunksPerWorker), so size nothing by that ceiling.
public int ChunkCount { readonly get; init; }
Property Value
ChunkIndex
Zero-based chunk index for chunked-parallel systems (e.g. ChunkedCallbackSystem).
Range: [0, ChunkCount). For non-chunked systems, always 0. Use to compute the per-chunk slice of arbitrary work:
start = ChunkIndex × totalSize / ChunkCount.
public int ChunkIndex { readonly get; init; }
Property Value
ClusterIds
Source array for the StartClusterIndex / EndClusterIndex partition (issue #231).
Points at Typhon.Engine.Internals.ArchetypeClusterState.ActiveClusterIds for systems with no tier filter, or at a per-tier (or per-bucket, for cellAmortize)
cluster list for tier-filtered systems. Null when the system has no cluster partition (non-parallel, non-cluster-eligible, or empty view).
public int[] ClusterIds { readonly get; init; }
Property Value
- int[]
Commands
Queues a spawn or a destroy from inside this chunk, without a transaction (#1099). Resolve it ONCE per system body and queue through it.
public readonly EntityCommands Commands { get; }
Property Value
Remarks
The handle is bound to this context's worker slot and chunk index, so a producer cannot write another worker's segment and the ids it hands out do not depend on which worker picked the chunk up. The returned EntityId is real and final; the entity becomes alive at the fence.
IsValid is false in a lifecycle hook and in a scheduler running without a database engine, and every method on an
invalid handle is a no-op — the same shape as the queue?.Push(...) idiom Writer<T>(EventQueue<T>) keeps working for. It is not a property to
store: a ref struct cannot leave the stack, which is what makes the no-atomics design safe.
ConsumedQueues
Event queues this system consumes. Null if the system has no consumed queues.
Cast to EventQueue<T> and call Drain(span) to read events; size the span from Count, which a short span throws on.
public EventQueueBase[] ConsumedQueues { readonly get; init; }
Property Value
CreateSideTransaction
Creates a side-transaction with the specified durability mode. Side-transactions commit independently and are NOT visible to the main tick Transaction (snapshot isolation — the main Transaction's TSN is fixed at creation). The caller owns the returned Transaction and must Dispose it.
public SideTransactionFactory CreateSideTransaction { readonly get; init; }
Property Value
Remarks
Use for economy-critical operations (trades, purchases, progression) that must be durable immediately, independent of the main tick's commit. Pass Commit to make SingleVersion-layout writes zero-loss, atomic and commit-scoped (no revision chain). Null when running without a DatabaseEngine.
DeltaTime
Elapsed time in seconds since the previous tick. Zero on the first tick.
public float DeltaTime { readonly get; init; }
Property Value
EndClusterIndex
Exclusive end index into ClusterIds for this worker's assigned cluster range.
public int EndClusterIndex { readonly get; init; }
Property Value
Remarks
Default 0. Check EndClusterIndex > StartClusterIndex for validity — a zero range means not applicable.
Entities
Filtered entity set for this system's execution.
- CallbackSystem: empty (no entity input)
- QuerySystem/PipelineSystem without changeFilter: full View entity set
- QuerySystem/PipelineSystem with changeFilter: dirty entities ∪ Added (only entities whose filtered components were written since last tick)
public IReadOnlyCollection<EntityId> Entities { readonly get; init; }
Property Value
Realms
The realm policy this tick (Realms D4): each realm's state and divisor, and the delta time a cluster of a realm integrates over —
ctx.Realms.DeltaTime(cluster.Realm) in a system that runs over realms simulated at a divisor.
public RealmsAccessor Realms { readonly get; init; }
Property Value
SpatialGrid
Game-facing accessor for the engine's spatial grid (issue #232). Provides cell tier assignment, coordinate conversion, and multi-observer helpers (SetCellTierMin(int, int, int, SimTier), ResetAllTiers(SimTier), SetTierInAABB(double, double, double, double, double, double, SimTier)). Check IsValid before use — false when no grid is configured.
public SpatialGridAccessor SpatialGrid { readonly get; init; }
Property Value
StartClusterIndex
Inclusive start index into ClusterIds for this worker's assigned cluster range. Used by cluster-native systems that iterate
via ctx.Accessor.GetClusterEnumerator<TArch>(ctx.ClusterIds, ctx.StartClusterIndex, ctx.EndClusterIndex) for 2-3 ns/entity performance.
Default 0.
public int StartClusterIndex { readonly get; init; }
Property Value
Remarks
Before issue #231 this range indexed directly into ArchetypeClusterState.ActiveClusterIds. After #231 it indexes
into ClusterIds, which points at either the full ActiveClusterIds (for All systems) or a per-tier cluster
list (for tier-filtered systems). Game code that passed ctx.StartClusterIndex / ctx.EndClusterIndex to the old two-argument
GetClusterEnumerator(int, int) overload must migrate to the new three-argument overload that takes ClusterIds explicitly.
Default 0 (not -1) due to struct constraint. Check EndClusterIndex > StartClusterIndex for validity — a zero range means not applicable
(non-parallel, non-cluster, or entity-level dispatch).
Subscriptions
What a system reads this tick's client commands through, and answers them with — ctx.Subscriptions.Commands<MoveIntent>()
(design/Subscriptions/01-model.md § 7). null when the application declared no subscriptions, and on a context built without a
TyphonRuntime at all (standalone scheduler tests).
public SubscriptionsCommands Subscriptions { readonly get; init; }
Property Value
Remarks
It is the runtime's own object, not a per-tick wrapper. SubscriptionsRuntime creates exactly one at Start and never replaces it,
so this is one reference copied into the context — no allocation per tick, per system or per chunk, and nothing to dispose. Everything it exposes
describes the tick the context belongs to, because the Engine-Pre drain refills the buffers it reads before the application's track runs.
Null is a real answer, and the reason to check it is the same as for Transaction. A runtime whose application never touched
TyphonRuntime.Subscriptions builds no session table, no pools and no command registry — "an unused subsystem costs a database exactly nothing" —
so there is no empty instance to hand out that would not be a 544 KiB allocation for nobody.
TickNumber
Monotonically increasing tick number (0-based).
public long TickNumber { readonly get; init; }
Property Value
TierBudgetMetrics
Per-tier cost and entity count metrics from the previous tick (issue #234). Available to all systems — primarily consumed by TierAssignment
CallbackSystem for adaptive tier boundary adjustment. Zero on the first tick (no previous-tick data).
public TierBudgetMetrics TierBudgetMetrics { readonly get; init; }
Property Value
Transaction
Transaction for this system's entity operations (Spawn, Open, OpenMut, Query, etc.). Created on the current worker thread. Valid only during this system's execution. Do NOT Commit or Dispose — the scheduler manages the Transaction lifecycle. Null when running without a DatabaseEngine (standalone scheduler tests).
public Transaction Transaction { readonly get; init; }
Property Value
WorkerId
Worker slot for the thread executing this system chunk. Use it to index per-worker data structures (per-worker scratch buffers, accumulators) without any synchronization.
public int WorkerId { readonly get; init; }
Property Value
Remarks
Range is [0, WorkerCount] — inclusive of the upper bound, so per-worker arrays must be sized
WorkerSlotCount, not WorkerCount. Slots [0, WorkerCount) are the pool's worker threads; the extra slot
DispatcherWorkerId belongs to the dispatcher (timer) thread, which runs a system body when a skipped root chains its
successor into ExecuteInline. The dispatcher slot is disjoint from every worker slot by construction, which is what makes it safe to write
without synchronization — see DispatcherWorkerId for why disjointness, not quiescence, is the guarantee.
Single-worker runtimes never produce the dispatcher slot. WorkerCount == 1 takes ExecuteTickSingleThreaded, which runs the
whole tick inline on the tick thread and stamps slot 0 — truthfully, since that thread is the one and only worker. The dispatcher slot arises
only from multi-worker track dispatch. Size per-worker arrays by WorkerSlotCount regardless, so the same code is
correct under both.
Contexts handed to the runtime lifecycle hooks (OnFirstTick, OnShutdown) carry NonWorkerId instead — those run on the
tick thread or on whichever thread called Shutdown, outside system dispatch entirely, so no slot belongs to them.
Withdrawn in #860: this used to be documented as "for non-parallel systems, always 0", and four of the six construction sites left it at
the default 0 — so every thread claimed slot 0 and per-worker partitioning aliased instead of separating. A non-parallel system now reports
whichever pool worker picked it up that tick, which varies from tick to tick. Code that read perWorker[0] on the strength of the old
wording must aggregate across all slots instead.
Methods
Writer<T>(EventQueue<T>)
Opens a writer on queue bound to this context's worker slot (#861). Resolve it once per system body and push through it.
public readonly EventWriter<T> Writer<T>(EventQueue<T> queue)
Parameters
queueEventQueue<T>The queue to produce into, or null for a no-op writer — the
queue?.Push(...)idiom, kept working. The system should declare the queue viaWritesEvents/produces:.
Returns
- EventWriter<T>
Type Parameters
TThe event type.
Remarks
This is the only supported way to produce events: it supplies WorkerId for you, so a producer cannot accidentally write another worker's segment. Calling it from a lifecycle hook throws — those contexts carry NonWorkerId and own no segment.