Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/facepunch/sbox-public/llms.txt

Use this file to discover all available pages before exploring further.

Hot reload is one of s&box’s most important developer features. When you save a C# file, the engine recompiles your code, swaps the old assembly for the new one at runtime, migrates existing object state to the new types, and continues running — all without stopping or restarting the game. This page explains how the system works, how to use IHotloadManaged to respond to reload events, and what the system cannot handle.

How hot reload works

The s&box hot-reload system is implemented in Sandbox.Hotload and works at the .NET assembly level:
1

Detect a change

The editor watches your project’s source files. When a .cs file is saved, a recompile is triggered using the incremental compiler in Sandbox.Compiling.
2

Build the new assembly

The compiler produces a new version of your game assembly. If compilation fails, the running assembly is unchanged and errors are reported in the editor console.
3

Register the assembly swap

The new assembly is registered alongside the old one using Hotload.ReplacingAssembly(oldAssembly, newAssembly). Both assemblies are tracked until UpdateReferences is called.
4

Migrate live instances

The hotload system walks all live objects reachable from static fields and watched roots. For each instance whose type has changed, it creates a new instance of the updated type, copies field values across (including newly-added fields set to their defaults), and replaces all references to the old instance with the new one.
5

Resume execution

Control returns to the game loop immediately. Components continue from where they were — OnAwake and OnEnabled are not re-fired unless the component itself was affected in a way that changes its enabled state.

IHotloadManaged

For types that need to save or restore custom state across a reload, implement the IHotloadManaged interface. The engine calls its methods during the upgrade pass.
public class MySystem : IHotloadManaged
{
    private List<string> _registeredNames = new();

    // Called on the OLD instance just before it is replaced.
    // Write anything you want to pass to the new instance into `state`.
    public void Destroyed( Dictionary<string, object> state )
    {
        state["names"] = _registeredNames;
    }

    // Called on the NEW instance after it replaces the old one.
    // `state` contains whatever the old instance wrote in Destroyed.
    public void Created( IReadOnlyDictionary<string, object> state )
    {
        if ( state.TryGetValue( "names", out var v ) && v is List<string> names )
            _registeredNames = names;
    }

    // Called on an instance that survived the reload without being replaced.
    public void Persisted()
    {
        // Invalidate caches that reference old type metadata, etc.
    }

    // Called when this instance could not be upgraded and references to it
    // have been set to null. Clean up unmanaged resources here.
    public void Failed()
    {
        // Dispose native handles, etc.
    }
}
All four IHotloadManaged methods have default implementations and are optional to override. Implement only the ones you need.

When to use IHotloadManaged

  • Caches keyed by type — type identity changes after a reload, so caches must be invalidated. The engine’s own ReflectionCache<TKey, TValue> uses Persisted() for this.
  • Unmanaged resources — if your object holds a native handle, implement Failed() to dispose it when the upgrade fails.
  • Registered state — if your system maintains a registry (e.g. a list of active listeners), use Destroyed / Created to transfer the list to the new instance.

SkipHotload

Apply [SkipHotload] to a class, struct, field, or property to tell the upgrade pass to skip it. The engine will not attempt to migrate the annotated member during a reload.
// Skip an entire class — none of its fields will be processed
[SkipHotload]
public class NativeInterop
{
    public IntPtr Handle;
}

// Skip a specific field
public class MyComponent : Component
{
    [SkipHotload]
    private static Dictionary<Type, object> _cache = new();
}
This is particularly useful for large object graphs that do not need migration, or for static members in generic types (which the Roslyn analyser SB3000 will warn you about).
Static members in generic types cannot be reliably migrated during a hotload. Add [SkipHotload] to suppress the warning and skip the field explicitly.

Limitations

Not everything can be hot-reloaded. The following changes require a full editor restart or project reload:

New base types

Adding or removing a base class from a type changes its identity in ways the upgrade pass cannot handle.

Added generic parameters

Changing a type from Foo to Foo<T> changes its name mangling and cannot be migrated.

Removed assemblies

Removing a project or assembly reference from the build requires a full reload.

Engine/framework code

Changes to the engine itself (Sandbox.Engine, Sandbox.System, etc.) are not hot-reloaded; they require a full editor restart.
If a hot reload fails mid-upgrade (e.g. due to a type incompatibility), references to the affected object are set to null and IHotloadManaged.Failed() is called. Always implement defensive null checks on any object that may have survived a failed reload.

Assembly ignorelist

Assemblies can be explicitly excluded from the upgrade pass by calling Hotload.IgnoreAssembly. Any fields declared on types in an ignored assembly are skipped during reference updates. The engine does this automatically for Sandbox.Hotload.dll itself and Mono.Cecil.dll.
// Inside engine bootstrap — illustrative, not required in game code
Loader.IgnoreAssembly( typeof( SkiaSharp.SKBitmap ).Assembly );
Game code rarely needs to interact with this API directly.

Practical tips

  • Keep component state in [Property]-decorated properties — the serializer preserves them across reloads because they are also written to the scene file.
  • Avoid caching System.Type references in static fields across hot-reload boundaries. Use [SkipHotload] on such caches and repopulate them lazily.
  • If a component stops behaving correctly after a reload, use Recompile All in the editor to force a clean assembly build.

Scripting basics

Component lifecycle, properties, and attributes.

Component API reference

Full API reference for the Component base class.

Build docs developers (and LLMs) love