menu_bookDocumentation

Sandbox: PlayerInventory — weapon slots, pickup, drop, and loadout persistence

calendar_today May 4, 2026 schedule ~1 min read person patrickjr verified 50

Sandbox PlayerInventory — Weapon Slots, Pickup, Drop, and Loadout Persistence

PlayerInventory manages up to 6 weapon slots, handles pickup/drop logic, and delegates loadout save/restore to PlayerLoadout.

Slot system

CSHARP
public sealed class PlayerInventory : Component, Local.IPlayerEvents
{
    [Property] public int MaxSlots { get; set; } = 6;

    // All weapons ordered by slot number
    public IEnumerable<BaseCarryable> Weapons =>
        GetComponentsInChildren<BaseCarryable>( true ).OrderBy( x => x.InventorySlot );

    // Synced from host — change callback enables/disables weapon GameObjects
    [Sync( SyncFlags.FromHost ), Change]
    public BaseCarryable ActiveWeapon { get; private set; }

    public void OnActiveWeaponChanged( BaseCarryable oldWeapon, BaseCarryable newWeapon )
    {
        if ( oldWeapon.IsValid() ) oldWeapon.GameObject.Enabled = false;
        if ( newWeapon.IsValid() )
        {
            newWeapon.GameObject.Enabled = true;
            newWeapon.SetDropped( false );
        }
    }
}

Pickup

CSHARP
public bool Pickup( string prefabName, bool notice = true )
{
    if ( !Networking.IsHost ) return false;

    var prefab = GameObject.GetPrefab( prefabName );
    if ( !prefab.IsValid() ) return false;

    var slot = FindFreeSlot();
    if ( slot < 0 ) return false; // Inventory full

    var baseCarry = prefab.Components.Get<BaseCarryable>( true );
    if ( !baseCarry.IsValid() ) return false;

    // If we already have this weapon type, add ammo instead
    var existing = Weapons.Where( x => x.GameObject.Name == prefab.Name ).FirstOrDefault();
    if ( existing.IsValid() )
    {
        if ( existing is BaseWeapon existingWeapon && existingWeapon.UsesAmmo )
        {
            if ( existingWeapon.ReserveAmmo < existingWeapon.MaxReserveAmmo )
                existingWeapon.AddReserveAmmo( pickupWeapon.ClipContents );
        }
        return false;
    }

    // Clone the prefab as a child of the player
    var go = prefab.Clone( new CloneConfig { Parent = GameObject, StartEnabled = false } );
    go.Components.Get<BaseCarryable>( true ).InventorySlot = slot;
    go.NetworkSpawn( Network.Owner );

    if ( notice ) OnClientPickup( weapon );
    return true;
}

Drop

CSHARP
public bool Drop( BaseCarryable weapon )
{
    if ( !Networking.IsHost ) return false;

    var dropPosition = Player.EyeTransform.Position + Player.EyeTransform.Forward * 48f;
    var dropVelocity = Player.EyeTransform.Forward * 200f + Vector3.Up * 100f;

    if ( ActiveWeapon == weapon )
        SwitchWeapon( null, true );

    // Weapons with DroppedWeapon component: spawn a fresh prefab clone
    var droppedWeapon = weapon.GetComponent<DroppedWeapon>( true );
    if ( droppedWeapon.IsValid() )
    {
        var pickup = prefab.Clone( new CloneConfig { Transform = new Transform( dropPosition ), StartEnabled = true } );
        Ownable.Set( pickup, Player.Network.Owner );
        pickup.Tags.Add( "removable" );
        pickup.NetworkSpawn();

        if ( pickup.GetComponent<Rigidbody>() is { } rb )
        {
            rb.Velocity = Player.Controller.Velocity + dropVelocity;
            rb.AngularVelocity = Vector3.Random * 8.0f;
        }
    }

    weapon.DestroyGameObject();
    return true;
}

PlayerLoadout — persistence

PlayerLoadout serializes the inventory to JSON and saves it to the scene's save data. On respawn, it restores the loadout.
CSHARP
public struct LoadoutEntry
{
    public string PrefabPath { get; set; }
    public int Slot { get; set; }
    public string SpawnerDataPayload { get; set; } // For SpawnerWeapon
}

// Saved as JSON in the scene save file, keyed by SteamId
// Restored in Global.IPlayerEvents.OnSpawned

Presets are stored in LocalData (client-side) as List<SavedPreset> — they persist across sessions.

Key points

Was this helpful?