Skip to main content
s&box uses C# attributes extensively to declare intent to the engine and editor. Many of these attributes trigger the Sandbox.Generator Roslyn source generator, which rewrites method and property bodies at compile time to inject networking, console variable, and other cross-cutting behaviours.

Editor attributes

[Property]

Marks a property or field for display and editing in the Inspector panel. The editor reads this attribute to know what to show, serialize, and hot-reload.
public class Projectile : Component
{
    [Property] public float Speed { get; set; } = 500f;
    [Property] public float Damage { get; set; } = 10f;

    // Optional metadata
    [Property( "display_name" )]
    public string DisplayName { get; set; }
}
PropertyAttribute accepts an internal name (lowercase, no spaces) and a Title for the UI label. If omitted, the editor auto-generates both from the C# identifier. Additional display attributes that work alongside [Property]:
AttributePurpose
[KeyProperty]Marks the single property that represents the whole object in a collapsed row
[InlineEditor]Forces the editor to expand the property inline rather than behind a popup
[Advanced]Hides the property unless the user enables Advanced mode

Networking attributes

[Sync]

Automatically synchronises a component property from its owner to all other clients. The code generator wraps the property getter and setter with __sync_GetValue / __sync_SetValue intercepts.
public class Player : Component
{
    // Replicated from the owner to everyone else
    [Sync] public int Health { get; set; } = 100;

    // Control how synchronisation works
    [Sync( SyncFlags.FromHost )]
    public float GameTime { get; set; }
}
SyncFlags lets you customise the direction and polling behaviour:
[CodeGenerator( CodeGeneratorFlags.Instance | CodeGeneratorFlags.WrapPropertySet, "__sync_SetValue" )]
[CodeGenerator( CodeGeneratorFlags.Instance | CodeGeneratorFlags.WrapPropertyGet, "__sync_GetValue" )]
public class SyncAttribute : Attribute
{
    public SyncFlags Flags { get; set; }
}

[HostSync]

[HostSync] is obsolete as of December 2024. Use [Sync( SyncFlags.FromHost )] instead.
Previously used to replicate a property from the host to all clients. It is now a thin wrapper over SyncAttribute:
[Obsolete( "Use [Sync] with SyncFlags.FromHost" )]
public class HostSyncAttribute : SyncAttribute
{
    public HostSyncAttribute() : base( SyncFlags.FromHost ) { }
}

RPC attributes

Remote procedure calls (RPCs) let you invoke methods across the network. All RPC variants inherit from RpcAttribute and use the code generator to intercept calls.
public class Weapon : Component
{
    // Called on all clients
    [Rpc.Broadcast]
    public void PlayFireEffect()
    {
        // Runs everywhere
    }

    // Called only on the host
    [Rpc.Host]
    public void RequestAmmoRefill( int amount )
    {
        // Only runs on host
    }

    // Called only on the owner of this GameObject
    [Rpc.Owner]
    public void ReceiveHitConfirm()
    {
        // Only runs on the owning client
    }
}

Console attributes

[ConVar]

Exposes a static property as a console variable. The code generator wraps get/set with ConsoleSystem.OnWrappedGet / ConsoleSystem.OnWrappedSet.
public static class GameSettings
{
    [ConVar( "sv_gravity", Help = "World gravity scale" )]
    public static float Gravity { get; set; } = 1.0f;

    // Saved between sessions
    [ConVar( Saved = true )]
    public static float MusicVolume { get; set; } = 0.8f;

    // Replicated from server to clients
    [ConVar( flags: ConVarFlags.Replicated )]
    public static int MaxPlayers { get; set; } = 16;
}
ConVarFlags values include Saved, Replicated, Cheat, UserInfo, Hidden, ChangeNotice, Protected, Server, Admin, and GameSetting.

[ConCmd]

Marks a static method as a console command. It inherits from ConVarAttribute and supports the same flags.
public static class AdminCommands
{
    [ConCmd( "kill_all", flags: ConVarFlags.Server | ConVarFlags.Admin )]
    public static void KillAll()
    {
        // Only runs on the server, admin only
        foreach ( var player in Game.ActiveScene.GetAllComponents<Player>() )
            player.Kill();
    }
}

Code generation

[CodeGenerator]

CodeGeneratorAttribute is a meta-attribute — you apply it to your own custom Attribute class to tell the source generator what transformation to perform whenever that attribute is used on a method or property.
[AttributeUsage( AttributeTargets.Class, AllowMultiple = true )]
public class CodeGeneratorAttribute : Attribute
{
    public CodeGeneratorFlags Type { get; init; }
    public string CallbackName { get; init; }
    public int Priority { get; init; } = 0;

    public CodeGeneratorAttribute( CodeGeneratorFlags type, string callbackName, int priority = 0 ) { ... }
}

CodeGeneratorFlags

[Flags]
public enum CodeGeneratorFlags
{
    WrapPropertyGet = 1,   // Intercept property getter
    WrapPropertySet = 2,   // Intercept property setter
    WrapMethod      = 4,   // Intercept method calls
    Static          = 8,   // Apply to static targets
    Instance        = 16   // Apply to instance targets
}

Writing a custom code-generated attribute

The pattern is: create an Attribute class, decorate it with one or more [CodeGenerator] calls, then apply your attribute to methods or properties.
// 1. Define the attribute
[AttributeUsage( AttributeTargets.Method )]
[CodeGenerator( CodeGeneratorFlags.Instance | CodeGeneratorFlags.WrapMethod, "MyMod.Profiler.OnEnter" )]
public class ProfiledAttribute : Attribute { }

// 2. Provide the callback
public static class Profiler
{
    public static void OnEnter( object target, string methodName, Action inner )
    {
        var sw = System.Diagnostics.Stopwatch.StartNew();
        inner();
        Log.Info( $"{methodName} took {sw.ElapsedMilliseconds}ms" );
    }
}

// 3. Use it
public class ExpensiveComponent : Component
{
    [Profiled]
    public void DoExpensiveWork() { ... }
}
The source generator (Sandbox.Generator) processes every syntax tree in your compilation in parallel via Processor.Run, then applies IL hot reload detection through ILHotloadProcessor.
Higher-priority [CodeGenerator] attributes wrap the target first, so they are the outermost wrapper in the call chain. The default priority is 0.

Build docs developers (and LLMs) love