Skip to content

Hot reload

Hot reload is the default workflow, not an afterthought: rebuilding a plugin replaces it in the running server (the delay is the 250 ms debounce plus load time), and ECS component values survive it. It is on by default (plugins.hot_reload).

  1. A file watcher on the plugins folder sees a changed *.dll (any dll inside plugins/Name/ counts, so a private dependency change reloads Name too) and waits 250 ms after the last write.
  2. It checks the file can be opened (a half-written dll is retried later), then loads the new version first: a new collectible AssemblyLoadContext, assemblies read from bytes, so files are never locked and a rebuild can overwrite them while the server runs.
  3. The new instance’s OnEnable runs while the old one is still registered. Names the old version holds (commands, systems) are taken over by the successor, and components or events it re-declares are re-bound.
  4. Only if that succeeded is the old version unloaded: OnDisable, every tracked registration released in reverse order, native leftovers dropped by owner tag, the old context unloaded and collected.
  5. If the new version fails to load, nothing changes: the old one keeps running and the log says why.

Mc.Scripting.Api always resolves to the server’s own copy, in every plugin context, so types unify across plugins and across reloads.

Survives a reload?
ECS component values with an unchanged layout and key yes
Entities yes (they are not owned by plugins)
Your plugin object, its fields, static fields, caches, delegates no: it is a new instance in a new load context
Subscriptions, commands, systems, queries, scheduled and posted tasks released, then re-registered by the new OnEnable
Files on disk, config yes

A component keeps its values when its size, alignment and field layout (types and offsets, computed from the struct) and its key stay the same. Change a field type or order, and the values are dropped and the component recreated, but only if the reloading plugin (or the one it replaces) is the only one bound to the key; otherwise registration fails with a message in the log. Renaming a field does not change the layout.

Because plugin state in fields is lost, design for it: keep what must survive in components or on disk, and rebuild caches in OnEnable.

Some operations restart the plugin worker process rather than reloading one plugin. A restart starts from nothing: every plugin is loaded again from disk, and the component values and the entities the worker created are gone.

  • /plugins reload restarts the worker on request. Every plugin is disabled first (OnDisable runs and they save), then the worker is started again and loads them all.
  • A crash of the worker (a plugin that brings the process down) is handled the same way, automatically.
  • A plugin added while the server runs is picked up by the watcher. Its data folder may not be writable until the worker restarts, in which case the plugin waits and the server restarts the worker for it. That restart reloads every plugin.

/plugins shows the state of the worker.

Unloading a collectible context only completes when nothing references it. The runtime checks this after every unload (the tests assert zero leaked contexts), and logs <name> was unloaded but is still referenced (leak) if you break it. The ways to break it:

  1. Do not store anything from your assembly in a static field of another assembly (a framework or API class, a ThreadLocal<T>, AppDomain data, a ConditionalWeakTable of your types…). That pins your context.
  2. Do not start threads or timers that outlive OnDisable: a Task.Run loop, a System.Threading.Timer, a FileSystemWatcher of your own. Stop them in OnDisable, or use the scheduler, which is cancelled for you.
  3. Register through the plugin context (Events, Commands, Systems, Scheduler, Ecs) rather than a global registry, so the context can release it. Anything custom: implement IPluginResource and call Context.Track(...).
  4. Do not keep Entity refs (ref T) or event structs across ticks: see entities.
  5. A plugin’s own dependencies (NuGet packages) are loaded privately into its context. Share types between plugins only through the API assembly or a shared assembly that both plugins reference with Private=false and the server can resolve.
  6. After a failed reload, if the failed version already re-declared a component or event layout, that declaration stays. Fix and reload again.
  • Build into the watched folder: set RAMJET_PLUGINS to the server’s plugins folder (see create a plugin). The template copies dependencies and the pdb first and the main dll last, because the dll is what the watcher reacts to.
  • Copying a dll over the old one works the same way; deleting it unloads the plugin; adding a new Name.dll or Name/Name.dll loads it.

A reload also reloads the plugins that hard-depend on the one being replaced (see depends in the lifecycle), so that their types bind to the new version. Plugins that only soft-depend on it keep running, and can see the service they used go away and come back through Services.