InventoryContainer.cs
using Sandbox;
using System;
using System.Text;

public class InventoryContainer
{
    /// <summary>Number of columns in the grid.</summary>
    public int Width { get; }

    /// <summary>Number of rows in the grid.</summary>
    public int Height { get; }

    /// <summary>Total number of slots (Width * Height).</summary>
    public int Capacity => Width * Height;

    private readonly InventorySlot[] _slots;

    public InventoryContainer( int width, int height )
    {
        Width = width;
        Height = height;
        _slots = new InventorySlot[Capacity];
        for ( int i = 0; i < Capacity; i++ )
            _slots[i] = new InventorySlot();
    }

    /// <summary>Returns the slot at the given flat index.</summary>
    public InventorySlot GetSlot( int index ) => _slots[index];

    /// <summary>Returns the slot at grid coordinates (x, y).</summary>
    public InventorySlot GetSlot( int x, int y ) => _slots[y * Width + x];

    /// <summary>Total number of slots in the container.</summary>
    public int SlotCount => _slots.Length;

    // ─── Mutating ────────────────────────────────────────────────

    /// <summary>
    /// Add items to the container, stacking into existing slots first.
    /// Returns how many items could not fit (leftover).
    /// </summary>
    public int AddItem( InventoryItem item, int amount = 1 )
    {
        // Stack into existing slots first
        foreach ( var slot in _slots )
        {
            if ( !slot.IsEmpty && slot.Item == item )
                amount = slot.Add( item, amount );
            if ( amount <= 0 ) return 0;
        }

        // Then fill empty slots
        foreach ( var slot in _slots )
        {
            if ( slot.IsEmpty )
                amount = slot.Add( item, amount );
            if ( amount <= 0 ) return 0;
        }

        return amount;
    }

    /// <summary>
    /// Remove items from the container across all stacks.
    /// Returns how many were actually removed.
    /// </summary>
    public int RemoveItem( InventoryItem item, int amount = 1 )
    {
        int removed = 0;
        foreach ( var slot in _slots )
        {
            if ( slot.IsEmpty || slot.Item != item ) continue;
            int take = Math.Min( slot.Quantity, amount - removed );
            slot.Remove( take );
            removed += take;
            if ( removed >= amount ) break;
        }
        return removed;
    }

    /// <summary>Returns the total quantity of the given item across all slots.</summary>
    public int CountItem( InventoryItem item )
    {
        int total = 0;
        foreach ( var slot in _slots )
            if ( !slot.IsEmpty && slot.Item == item )
                total += slot.Quantity;
        return total;
    }

    /// <summary>Returns true if the container holds at least the given amount of the item.</summary>
    public bool HasItem( InventoryItem item, int amount = 1 )
        => CountItem( item ) >= amount;

    /// <summary>Swap the contents of two slots by index.</summary>
    public void SwapSlots( int indexA, int indexB )
    {
        var a = _slots[indexA];
        var b = _slots[indexB];
        var tempItem = a.Item;
        var tempQty  = a.Quantity;
        var tempDura = a.Durability;
        a.Set( b.Item, b.Quantity );
        a.Durability = b.Durability;
        b.Set( tempItem, tempQty );
        b.Durability = tempDura;
    }

    /// <summary>
    /// Sort all slots by the given comparison. Empty slots move to the end.
    /// </summary>
    public void SortBy( Comparison<InventorySlot> comparison )
    {
        var nonEmpty = _slots.Where( s => !s.IsEmpty ).ToList();
        nonEmpty.Sort( comparison );

        int idx = 0;
        foreach ( var s in nonEmpty )
        {
            _slots[idx].Set( s.Item, s.Quantity );
            _slots[idx].Durability = s.Durability;
            idx++;
        }
        for ( ; idx < _slots.Length; idx++ )
            _slots[idx].Clear();
    }

    /// <summary>Clears all slots in the container.</summary>
    public void Clear()
    {
        foreach ( var slot in _slots )
            slot.Clear();
    }

    // ─── Network Serialization ───────────────────────────────────

    /// <summary>
    /// Serialize entire container to a single string for [Sync].
    /// Slots are pipe-separated: "item/path:qty|item/path:qty|..."
    /// </summary>
    public string Serialize()
    {
        var sb = new StringBuilder();
        for ( int i = 0; i < _slots.Length; i++ )
        {
            if ( i > 0 ) sb.Append( '|' );
            sb.Append( _slots[i].Serialize() );
        }
        return sb.ToString();
    }

    /// <summary>
    /// Restore all slot state from a serialized string.
    /// Called on clients when the [Sync] value changes.
    /// </summary>
    public void Deserialize( string data )
    {
        if ( string.IsNullOrEmpty( data ) )
        {
            Clear();
            return;
        }

        var parts = data.Split( '|' );
        for ( int i = 0; i < _slots.Length; i++ )
        {
            _slots[i].Deserialize( i < parts.Length ? parts[i] : "" );
        }
    }
}