Skip to content

Systems and tick phases

A system is a function the engine calls once per tick, in a chosen phase, with the tick’s delta time in seconds. Systems are how you run per-tick logic over many entities (with a query), and how you hook, reorder, replace or switch off the server’s own behaviour. Your systems live in the engine’s own system registry: they run in the same pipelines as native systems, are ordered against them by name, and show up with timings in the engine’s system list.

ManaPlugin.cs
[PluginInfo("ManaDemo", Version = "1.0.0")]
public sealed class ManaPlugin : Plugin
{
private Query<Mana> _mana = null!;
protected override void OnEnable()
{
Ecs.RegisterComponent<Mana>("manademo:mana");
_mana = Ecs.Query<Mana>();
Systems.Add("regen", Phase.Tick, dt =>
{
foreach (var chunk in _mana)
{
var mana = chunk.Field<Mana>(0);
for (int i = 0; i < mana.Length; i++) mana[i].Value += 1;
}
});
}
}
public struct Mana
{
public int Value;
}

Phase values, in the order they run each tick:

Phase What happens there
NetworkReceive drain inbound packets, apply them to entities
PreTick timers, input integration
Tick game logic: entities, blocks … (the default for Add)
PostTick removals, collision resolution, derived state
Tracking spatial index update, view diffs
NetworkSend build and encode outbound packets
AsyncIntegration integrate worker results (chunk loads, lighting)

Deferred changes (entity creation, component add/set/remove) made in one phase are merged at the phase boundary and are visible to every later phase of the same tick. A Tick system sees an entity a native NetworkReceive system created in the same tick; the other way round only from the next phase on.

Systems.Register(Phase.Tick, "regen", dt => ...); // end of the phase (same as Add)
Systems.After("vanilla:living_physics", "after_physics", dt => ...); // right behind a native or scripted system
Systems.Before("mobs:ai", "before_ai", dt => ...); // right in front of one
var h = Systems.Replace("mobs:ai", "my_ai", dt => ...); // disables mobs:ai, runs in its place
h.Dispose(); // ... and restores mobs:ai
Systems.SetEnabled("mobs:spawner", false); // re-enabled when the plugin unloads

The names you can anchor on are listed on the generated native systems page.

  • Names are the engine registry’s names and are unique. Yours are namespaced by the plugin: Add("regen", ...) in a plugin called Hello registers Hello:regen (a name that already starts with Hello: is kept). Registering a name that a native system or another plugin holds throws. SystemHandle.Name returns the effective name.
  • Anchors for After/Before/Replace are full registry names (not namespaced), and the new system takes the anchor’s phase. Register with a placement other than Append needs an anchor.
  • Replace is reversible by design: the original is disabled and yours runs right after it. Dispose the handle or unload the plugin and the original comes back. Two plugins replacing or disabling the same system do not fight: it is re-enabled only when the last holder goes.
  • Registry changes made from inside a system (Register, Dispose, SetEnabled) are queued and applied right after the tick; the handle is valid immediately.
  • SetEnabled(name, bool) works on native and scripted systems, returns false for an unknown name, and is undone for a system your plugin disabled when it unloads.

Your systems run in the plugin worker, inside the window of the phase they were registered for: after the engine’s systems of earlier phases and before the later ones. A system positioned next to an engine system (After, Before, Replace) runs where the engine’s registry puts it, through a proxy. The dt a system receives is the engine’s frame delta.

Two things differ from in-process plugins:

  • A component your plugin defines lives in the worker. Engine systems and engine queries do not see it.
  • An engine component can be read from the worker, but not written: a write is ignored with a warning.

A reloaded plugin registers its successor before the predecessor is released. For that moment the new system runs as Hello:regen#2, and is renamed back to Hello:regen (at the same position) once the old one is gone.

Systems versus events versus the scheduler

Section titled “Systems versus events versus the scheduler”
You want Use
react to something that happened an event
run logic every tick over many entities a system and a query
run something later or periodically, not per entity the scheduler
change how vanilla behaves Replace, Before, After or SetEnabled on a native system

Managed systems are not multithreaded: they run on the tick thread, one after another, never at the same time as each other or as your event handlers.