Code/StorageContainer.cs
using Sandbox;
using System;
/// <summary>
/// A world container (chest, crate, etc.) with its own inventory grid.
/// Open it via InventoryHud.OpenContainer( this ).
/// TransferSlotToPlayer / TransferAllToPlayer are convenience helpers —
/// wire them up to your interaction system or call them from the HUD.
/// </summary>
public sealed class StorageContainer : Component
{
[Property, Group( "Grid" )] public int Width { get; set; } = 5;
[Property, Group( "Grid" )] public int Height { get; set; } = 4;
[Property, Group( "Display" )] public string ContainerName { get; set; } = "Container";
/// <summary>
/// Optional loot table. If set and AutoPopulate is true, the container fills
/// itself on start when empty. Call PopulateFromLootTable() to re-roll at runtime.
/// </summary>
[Property, Group( "Loot" )] public LootTable LootTable { get; set; }
/// <summary>
/// Automatically roll the loot table on Start if the container is empty.
/// </summary>
[Property, Group( "Loot" )] public bool AutoPopulate { get; set; } = true;
[Sync, Change( nameof(OnDataChanged) )]
private string _data { get; set; } = "";
public string DataHash => _data;
/// <summary>True when this container has a loot table — used by the HUD to style it differently.</summary>
public bool IsLootContainer => LootTable != null;
public InventoryContainer Container { get; private set; }
/// <summary>Fired on all clients whenever container state changes.</summary>
public Action OnContainerChanged;
/// <summary>
/// Fire this to request any listening HUD to open this container's loot panel.
/// PlayerPickupHandler calls it on E-press. Subscribe in your HUD or game code.
/// </summary>
public static Action<StorageContainer> OnOpenRequested;
// ─── Lifecycle ───────────────────────────────────────────────
protected override void OnStart()
{
Container = new InventoryContainer( Width, Height );
Container.Deserialize( _data );
if ( AutoPopulate && LootTable != null && IsContainerEmpty() )
PopulateFromLootTable();
}
private bool IsContainerEmpty()
{
for ( int i = 0; i < Container.SlotCount; i++ )
if ( !Container.GetSlot( i ).IsEmpty ) return false;
return true;
}
/// <summary>
/// Clear the container and re-roll the loot table. Safe to call at runtime.
/// </summary>
public void PopulateFromLootTable()
{
if ( LootTable == null ) return;
Container.Clear();
foreach ( var result in LootTable.Roll() )
Container.AddItem( result.Item, result.Amount );
Push();
}
private void OnDataChanged( string oldValue, string newValue )
{
Container?.Deserialize( newValue );
OnContainerChanged?.Invoke();
}
// ─── Container API ───────────────────────────────────────────
/// <summary>Add items to this container. Returns leftover that didn't fit.</summary>
public int GiveItem( InventoryItem item, int amount = 1 )
{
if ( !AssertAuthority() ) return amount;
int leftover = Container.AddItem( item, amount );
Push();
return leftover;
}
/// <summary>Remove items from this container. Returns how many were removed.</summary>
public int TakeItem( InventoryItem item, int amount = 1 )
{
if ( !AssertAuthority() ) return 0;
int removed = Container.RemoveItem( item, amount );
Push();
return removed;
}
public bool HasItem( InventoryItem item, int amount = 1 )
=> Container.HasItem( item, amount );
public void SwapSlots( int indexA, int indexB )
{
if ( !AssertAuthority() ) return;
if ( indexA < 0 || indexB < 0 || indexA >= Container.SlotCount || indexB >= Container.SlotCount ) return;
Container.SwapSlots( indexA, indexB );
Push();
}
public void Clear()
{
if ( !AssertAuthority() ) return;
Container.Clear();
Push();
}
// ─── Transfer Helpers ────────────────────────────────────────
/// <summary>
/// Move the item in the given container slot into the player's bag.
/// Returns how many items could not fit in the player's bag (left in container).
/// Note: requires authority on both this container and the player's InventoryComponent.
/// </summary>
public int TransferSlotToPlayer( int slotIndex, InventoryComponent player )
{
if ( !AssertAuthority() ) return 0;
if ( player == null ) return 0;
var slot = Container.GetSlot( slotIndex );
if ( slot.IsEmpty ) return 0;
var item = slot.Item;
int qty = slot.Quantity;
int leftover = player.GiveItemSilent( item, qty );
int taken = qty - leftover;
if ( taken > 0 )
{
slot.Remove( taken );
player.ForcePushBagSync();
Push();
}
return leftover;
}
/// <summary>
/// Move every item from this container into the player's bag.
/// Items that don't fit remain in the container.
/// Syncs both the bag and container once at the end rather than per-item.
/// </summary>
public void TransferAllToPlayer( InventoryComponent player )
{
if ( !AssertAuthority() ) return;
if ( player == null ) return;
bool changed = false;
for ( int i = 0; i < Container.SlotCount; i++ )
{
var slot = Container.GetSlot( i );
if ( slot.IsEmpty ) continue;
int leftover = player.GiveItemSilent( slot.Item, slot.Quantity );
int taken = slot.Quantity - leftover;
if ( taken > 0 )
{
slot.Remove( taken );
changed = true;
}
}
if ( changed )
{
player.ForcePushBagSync();
Push();
}
}
/// <summary>
/// Move an item from the player's bag into this container.
/// If containerIndex is -1, it stacks or fills the first available slot.
/// </summary>
public void TransferFromPlayer( int bagIndex, InventoryComponent player, int containerIndex = -1 )
{
if ( !AssertAuthority() ) return;
if ( player == null ) return;
var bagSlot = player.Bag.GetSlot( bagIndex );
if ( bagSlot.IsEmpty ) return;
var item = bagSlot.Item;
int qty = bagSlot.Quantity;
if ( containerIndex >= 0 && containerIndex < Container.SlotCount )
{
var target = Container.GetSlot( containerIndex );
if ( target.IsEmpty || target.Item == item )
{
int leftover = target.Add( item, qty );
int taken = qty - leftover;
if ( taken > 0 )
{
bagSlot.Remove( taken );
player.ForcePushBagSync();
Push();
}
return;
}
}
int bagLeftover = Container.AddItem( item, qty );
int moved = qty - bagLeftover;
if ( moved > 0 )
{
bagSlot.Remove( moved );
player.ForcePushBagSync();
Push();
}
}
// ─── Internal ────────────────────────────────────────────────
/// <summary>Force a sync push after direct slot manipulation from the HUD.</summary>
public void ForcePush() => Push();
private void Push()
{
_data = Container.Serialize();
OnContainerChanged?.Invoke();
}
private bool AssertAuthority()
{
if ( !IsProxy ) return true;
Log.Warning( "StorageContainer: Cannot modify from a proxy. Use an RPC or run on the owning client." );
return false;
}
}