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).
What the server does
Section titled “What the server does”- A file watcher on the plugins folder sees a changed
*.dll(any dll insideplugins/Name/counts, so a private dependency change reloadsNametoo) and waits 250 ms after the last write. - 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. - The new instance’s
OnEnableruns 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. - 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. - 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.
What survives
Section titled “What survives”| 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.
Restarts of the plugin worker
Section titled “Restarts of the plugin worker”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 reloadrestarts the worker on request. Every plugin is disabled first (OnDisableruns 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.
Rules to keep reload leak-free
Section titled “Rules to keep reload leak-free”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:
- Do not store anything from your assembly in a static field of another assembly (a framework or API class, a
ThreadLocal<T>,AppDomaindata, aConditionalWeakTableof your types…). That pins your context. - Do not start threads or timers that outlive
OnDisable: aTask.Runloop, aSystem.Threading.Timer, aFileSystemWatcherof your own. Stop them inOnDisable, or use the scheduler, which is cancelled for you. - Register through the plugin context (
Events,Commands,Systems,Scheduler,Ecs) rather than a global registry, so the context can release it. Anything custom: implementIPluginResourceand callContext.Track(...). - Do not keep
Entityrefs (ref T) or event structs across ticks: see entities. - 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=falseand the server can resolve. - After a failed reload, if the failed version already re-declared a component or event layout, that declaration stays. Fix and reload again.
Triggering reloads
Section titled “Triggering reloads”- Build into the watched folder: set
RAMJET_PLUGINSto 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.dllorName/Name.dllloads it.
Plugins that depend on the reloaded one
Section titled “Plugins that depend on the reloaded one”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.