namespace NeonSprawl.Server.Game.Items;
/// Server-authoritative inventory add/remove rules (NEO-54).
public static class PlayerInventoryOperations
{
///
/// Adds of into the catalog-routed container.
/// All-or-nothing: denies with when the full amount cannot be placed.
///
public static PlayerInventoryMutationOutcome TryAddStack(
string playerId,
string itemId,
int quantity,
IItemDefinitionRegistry registry,
IPlayerInventoryStore store)
{
if (quantity <= 0)
{
return Deny(playerId, store, PlayerInventoryReasonCodes.InvalidQuantity);
}
var lookup = itemId.Trim();
if (lookup.Length == 0 || !registry.TryGetDefinition(lookup, out var def))
{
return Deny(playerId, store, PlayerInventoryReasonCodes.InvalidItem);
}
if (!store.TryGetSnapshot(playerId, out var before))
{
return new PlayerInventoryMutationOutcome(PlayerInventoryMutationKind.StoreMissing, null, null);
}
var container = PlayerInventorySnapshot.ContainerFromSlotKind(def.InventorySlotKind);
var slots = CloneSlots(before.GetSlots(container));
var remaining = quantity;
remaining = MergeIntoExistingStacks(slots, def.Id, remaining, def.StackMax);
remaining = FillEmptySlots(slots, def.Id, remaining, def.StackMax);
if (remaining > 0)
{
return new PlayerInventoryMutationOutcome(
PlayerInventoryMutationKind.Denied,
PlayerInventoryReasonCodes.InventoryFull,
before);
}
var after = before.WithSlots(container, slots);
if (!store.TryReplaceSnapshot(playerId, after))
{
return new PlayerInventoryMutationOutcome(PlayerInventoryMutationKind.StoreMissing, null, null);
}
return new PlayerInventoryMutationOutcome(PlayerInventoryMutationKind.Applied, null, after);
}
/// Removes from stacks of (lowest slot index first).
public static PlayerInventoryMutationOutcome TryRemoveStack(
string playerId,
string itemId,
int quantity,
IItemDefinitionRegistry registry,
IPlayerInventoryStore store)
{
if (quantity <= 0)
{
return Deny(playerId, store, PlayerInventoryReasonCodes.InvalidQuantity);
}
var lookup = itemId.Trim();
if (lookup.Length == 0 || !registry.TryGetDefinition(lookup, out var def))
{
return Deny(playerId, store, PlayerInventoryReasonCodes.InvalidItem);
}
if (!store.TryGetSnapshot(playerId, out var before))
{
return new PlayerInventoryMutationOutcome(PlayerInventoryMutationKind.StoreMissing, null, null);
}
var container = PlayerInventorySnapshot.ContainerFromSlotKind(def.InventorySlotKind);
var slots = CloneSlots(before.GetSlots(container));
var available = CountQuantity(slots, def.Id);
if (available < quantity)
{
return new PlayerInventoryMutationOutcome(
PlayerInventoryMutationKind.Denied,
PlayerInventoryReasonCodes.InsufficientQuantity,
before);
}
var remaining = quantity;
for (var i = 0; i < slots.Length && remaining > 0; i++)
{
var slot = slots[i];
if (slot.IsEmpty || !string.Equals(slot.ItemId, def.Id, StringComparison.Ordinal))
{
continue;
}
var take = Math.Min(remaining, slot.Quantity);
var left = slot.Quantity - take;
slots[i] = left <= 0
? new InventorySlotState(slot.SlotIndex, null, 0)
: new InventorySlotState(slot.SlotIndex, def.Id, left);
remaining -= take;
}
var after = before.WithSlots(container, slots);
if (!store.TryReplaceSnapshot(playerId, after))
{
return new PlayerInventoryMutationOutcome(PlayerInventoryMutationKind.StoreMissing, null, null);
}
return new PlayerInventoryMutationOutcome(PlayerInventoryMutationKind.Applied, null, after);
}
private static PlayerInventoryMutationOutcome Deny(string playerId, IPlayerInventoryStore store, string reasonCode)
{
if (!store.TryGetSnapshot(playerId, out var snapshot))
{
return new PlayerInventoryMutationOutcome(PlayerInventoryMutationKind.StoreMissing, null, null);
}
return new PlayerInventoryMutationOutcome(PlayerInventoryMutationKind.Denied, reasonCode, snapshot);
}
private static int MergeIntoExistingStacks(InventorySlotState[] slots, string itemId, int remaining, int stackMax)
{
for (var i = 0; i < slots.Length && remaining > 0; i++)
{
var slot = slots[i];
if (slot.IsEmpty || !string.Equals(slot.ItemId, itemId, StringComparison.Ordinal))
{
continue;
}
var space = stackMax - slot.Quantity;
if (space <= 0)
{
continue;
}
var add = Math.Min(space, remaining);
slots[i] = new InventorySlotState(slot.SlotIndex, itemId, slot.Quantity + add);
remaining -= add;
}
return remaining;
}
private static int FillEmptySlots(InventorySlotState[] slots, string itemId, int remaining, int stackMax)
{
for (var i = 0; i < slots.Length && remaining > 0; i++)
{
if (!slots[i].IsEmpty)
{
continue;
}
var add = Math.Min(stackMax, remaining);
slots[i] = new InventorySlotState(i, itemId, add);
remaining -= add;
}
return remaining;
}
private static int CountQuantity(InventorySlotState[] slots, string itemId)
{
var total = 0;
foreach (var slot in slots)
{
if (!slot.IsEmpty && string.Equals(slot.ItemId, itemId, StringComparison.Ordinal))
{
total += slot.Quantity;
}
}
return total;
}
private static InventorySlotState[] CloneSlots(InventorySlotState[] slots)
{
var copy = new InventorySlotState[slots.Length];
for (var i = 0; i < slots.Length; i++)
{
var s = slots[i];
copy[i] = new InventorySlotState(s.SlotIndex, s.ItemId, s.Quantity);
}
return copy;
}
}