202 lines
7.3 KiB
C#
202 lines
7.3 KiB
C#
namespace NeonSprawl.Server.Game.Items;
|
|
|
|
/// <summary>Server-authoritative inventory add/remove rules (NEO-54).</summary>
|
|
public static class PlayerInventoryOperations
|
|
{
|
|
/// <summary>
|
|
/// Adds <paramref name="quantity"/> of <paramref name="itemId"/> into the catalog-routed container.
|
|
/// All-or-nothing: denies with <see cref="PlayerInventoryReasonCodes.InventoryFull"/> when the full amount cannot be placed.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
string? denyReason = null;
|
|
if (!store.TryMutateSnapshot(
|
|
playerId,
|
|
before =>
|
|
{
|
|
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)
|
|
{
|
|
denyReason = PlayerInventoryReasonCodes.InventoryFull;
|
|
return new PlayerInventoryMutationWrite(Write: false, before);
|
|
}
|
|
|
|
return new PlayerInventoryMutationWrite(Write: true, before.WithSlots(container, slots));
|
|
},
|
|
out var result))
|
|
{
|
|
return new PlayerInventoryMutationOutcome(PlayerInventoryMutationKind.StoreMissing, null, null);
|
|
}
|
|
|
|
if (denyReason is not null)
|
|
{
|
|
return new PlayerInventoryMutationOutcome(PlayerInventoryMutationKind.Denied, denyReason, result);
|
|
}
|
|
|
|
return new PlayerInventoryMutationOutcome(PlayerInventoryMutationKind.Applied, null, result);
|
|
}
|
|
|
|
/// <summary>Removes <paramref name="quantity"/> from stacks of <paramref name="itemId"/> (lowest slot index first).</summary>
|
|
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);
|
|
}
|
|
|
|
string? denyReason = null;
|
|
if (!store.TryMutateSnapshot(
|
|
playerId,
|
|
before =>
|
|
{
|
|
var container = PlayerInventorySnapshot.ContainerFromSlotKind(def.InventorySlotKind);
|
|
var slots = CloneSlots(before.GetSlots(container));
|
|
var available = CountQuantity(slots, def.Id);
|
|
if (available < quantity)
|
|
{
|
|
denyReason = PlayerInventoryReasonCodes.InsufficientQuantity;
|
|
return new PlayerInventoryMutationWrite(Write: false, 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;
|
|
}
|
|
|
|
return new PlayerInventoryMutationWrite(Write: true, before.WithSlots(container, slots));
|
|
},
|
|
out var result))
|
|
{
|
|
return new PlayerInventoryMutationOutcome(PlayerInventoryMutationKind.StoreMissing, null, null);
|
|
}
|
|
|
|
if (denyReason is not null)
|
|
{
|
|
return new PlayerInventoryMutationOutcome(PlayerInventoryMutationKind.Denied, denyReason, result);
|
|
}
|
|
|
|
return new PlayerInventoryMutationOutcome(PlayerInventoryMutationKind.Applied, null, result);
|
|
}
|
|
|
|
private static PlayerInventoryMutationOutcome Deny(string playerId, IPlayerInventoryStore store, string reasonCode)
|
|
{
|
|
if (store.TryGetSnapshot(playerId, out var snapshot))
|
|
{
|
|
return new PlayerInventoryMutationOutcome(PlayerInventoryMutationKind.Denied, reasonCode, snapshot);
|
|
}
|
|
|
|
return new PlayerInventoryMutationOutcome(PlayerInventoryMutationKind.Denied, reasonCode, null);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|