Merge remote-tracking branch 'origin/feat/inport-props' into feat/craft-discovery
# Conflicts: # Assets/External/Animated PBR Chest Demo/Materials/WoodChest.mat # Packages/com.distantlands.cozy.core/Content/Integration/Import for BiRP.unitypackage.meta # Packages/com.distantlands.cozy.core/Content/Integration/Import for HDRP.unitypackage.meta # Packages/com.distantlands.cozy.core/Content/Integration/Import for URP.unitypackage.meta
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using FishNet.Connection;
|
||||
@@ -47,6 +48,18 @@ namespace Ashwild.Inventory
|
||||
private InventorySlot[] slots;
|
||||
private int selectedHotbarIndex;
|
||||
|
||||
/// <summary>
|
||||
/// Scratch copy of the slots used by the capacity dry run, kept as a field so a per-swing
|
||||
/// capacity check does not allocate. Never holds meaningful state between calls.
|
||||
/// </summary>
|
||||
private SlotContent[] fitSnapshot;
|
||||
|
||||
/// <summary>
|
||||
/// One-element buffer so the single-item <see cref="CanFit"/> can reuse the multi-item dry run
|
||||
/// without allocating an array on every call.
|
||||
/// </summary>
|
||||
private readonly SlotContent[] singleFitBuffer = new SlotContent[1];
|
||||
|
||||
public int InventorySize => inventorySize;
|
||||
public int HotbarSize => hotbarSize;
|
||||
public int SelectedHotbarIndex => selectedHotbarIndex;
|
||||
@@ -156,6 +169,12 @@ namespace Ashwild.Inventory
|
||||
/// <summary>
|
||||
/// Runs on the owning client: resolves the granted item and adds it locally, restoring its
|
||||
/// remaining uses and honouring the slot the player aimed at when one was requested.
|
||||
///
|
||||
/// A grant has already left its source by the time it arrives, so anything that does not fit must
|
||||
/// not simply evaporate the way it used to. The interactions that can be refused pre-check their
|
||||
/// capacity before asking the server, which makes an overflow here a last-resort case (the
|
||||
/// inventory filled up while the request was in flight); it is put back into the world at the
|
||||
/// player's feet, announced, and logged — never destroyed.
|
||||
/// </summary>
|
||||
[TargetRpc]
|
||||
private void TargetGrantItem(NetworkConnection conn, ushort itemId, int quantity, int uses, int preferredIndex, bool isTransfer)
|
||||
@@ -167,7 +186,13 @@ namespace Ashwild.Inventory
|
||||
return;
|
||||
}
|
||||
|
||||
AddItem(item, quantity, uses, preferredIndex, isTransfer);
|
||||
AddItem(item, quantity, uses, preferredIndex, isTransfer, out int leftover);
|
||||
if (leftover <= 0) return;
|
||||
|
||||
Debug.LogWarning($"[PlayerInventory] Inventory full — {leftover}x '{item.ItemName}' could not be " +
|
||||
"stored and was returned to the world.", this);
|
||||
PlayerEvents.RaiseInteractionRefused("Inventory full");
|
||||
SpawnInWorld(item, leftover, uses);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -269,7 +294,19 @@ namespace Ashwild.Inventory
|
||||
/// pulling an item a co-op partner left in a chest is a genuine first acquisition.
|
||||
/// </summary>
|
||||
public bool AddItem(ItemData item, int quantity = 1, int uses = -1, int preferredIndex = -1, bool isTransfer = false)
|
||||
=> AddItem(item, quantity, uses, preferredIndex, isTransfer, out _);
|
||||
|
||||
/// <summary>
|
||||
/// Same as <see cref="AddItem(ItemData,int,int,int,bool)"/> but reports how many units could not be
|
||||
/// placed. Callers that received the stack from the server need that number: whatever is left over
|
||||
/// has already left its source (the harvestable is damaged, the pickup is claimed) and would simply
|
||||
/// cease to exist if it were dropped on the floor here, which is exactly how loot used to go
|
||||
/// missing without a single log line.
|
||||
/// </summary>
|
||||
public bool AddItem(ItemData item, int quantity, int uses, int preferredIndex, bool isTransfer, out int leftover)
|
||||
{
|
||||
leftover = quantity;
|
||||
|
||||
if (item == null)
|
||||
{
|
||||
Debug.LogError("[PlayerInventory] AddItem called with no ItemData — pickup ignored.", this);
|
||||
@@ -287,7 +324,9 @@ namespace Ashwild.Inventory
|
||||
for (int i = 0; i < inventorySize && !incoming.IsEmpty; i++)
|
||||
if (slots[i].IsEmpty) StackInto(i, ref incoming);
|
||||
|
||||
int added = quantity - (incoming.IsEmpty ? 0 : incoming.Quantity);
|
||||
leftover = incoming.IsEmpty ? 0 : incoming.Quantity;
|
||||
|
||||
int added = quantity - leftover;
|
||||
if (added > 0)
|
||||
{
|
||||
onItemAdded?.Invoke(item, added);
|
||||
@@ -317,23 +356,43 @@ namespace Ashwild.Inventory
|
||||
{
|
||||
if (item == null) return false;
|
||||
|
||||
SlotContent incoming = SlotContent.Of(item, quantity, -1);
|
||||
singleFitBuffer[0] = SlotContent.Of(item, quantity, -1);
|
||||
return CanFitAll(singleFitBuffer);
|
||||
}
|
||||
|
||||
for (int i = 0; i < inventorySize && !incoming.IsEmpty; i++)
|
||||
/// <summary>
|
||||
/// Returns whether several stacks would fit *together*, without mutating anything. Asking
|
||||
/// <see cref="CanFit"/> once per item is not equivalent and quietly over-promises: with a single
|
||||
/// empty slot left, two different items each answer "yes" on their own, then the second one has
|
||||
/// nowhere to go. Harvest loot rolls several items at once, so it needs the combined answer — the
|
||||
/// dry run therefore places each stack into a running snapshot of the slots, exactly as the real
|
||||
/// add would, and fails as soon as one leftover cannot be placed.
|
||||
/// </summary>
|
||||
public bool CanFitAll(IReadOnlyList<SlotContent> incoming)
|
||||
{
|
||||
if (incoming == null || incoming.Count == 0) return true;
|
||||
|
||||
if (fitSnapshot == null || fitSnapshot.Length != inventorySize)
|
||||
fitSnapshot = new SlotContent[inventorySize];
|
||||
|
||||
for (int i = 0; i < inventorySize; i++)
|
||||
fitSnapshot[i] = ReadSlot(i);
|
||||
|
||||
for (int n = 0; n < incoming.Count; n++)
|
||||
{
|
||||
if (slots[i].IsEmpty) continue;
|
||||
SlotContent target = ReadSlot(i);
|
||||
SlotTransfer.TryStack(ref incoming, ref target);
|
||||
SlotContent pending = incoming[n];
|
||||
if (pending.IsEmpty) continue;
|
||||
|
||||
for (int i = 0; i < inventorySize && !pending.IsEmpty; i++)
|
||||
if (!fitSnapshot[i].IsEmpty) SlotTransfer.TryStack(ref pending, ref fitSnapshot[i]);
|
||||
|
||||
for (int i = 0; i < inventorySize && !pending.IsEmpty; i++)
|
||||
if (fitSnapshot[i].IsEmpty) SlotTransfer.TryStack(ref pending, ref fitSnapshot[i]);
|
||||
|
||||
if (!pending.IsEmpty) return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < inventorySize && !incoming.IsEmpty; i++)
|
||||
{
|
||||
if (!slots[i].IsEmpty) continue;
|
||||
SlotContent target = SlotContent.Empty;
|
||||
SlotTransfer.TryStack(ref incoming, ref target);
|
||||
}
|
||||
|
||||
return incoming.IsEmpty;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void RemoveItem(int index, int quantity = 1)
|
||||
@@ -408,21 +467,36 @@ namespace Ashwild.Inventory
|
||||
|
||||
ItemData item = slots[index].ItemData;
|
||||
int uses = item.HasUses ? slots[index].CurrentUses : -1;
|
||||
Transform origin = dropOrigin != null ? dropOrigin : transform;
|
||||
if (item.WorldPrefab != null && origin != null && PickableRegistry.Instance != null)
|
||||
{
|
||||
Vector3 dropPos = origin.position + origin.forward * dropForwardDistance;
|
||||
ushort id = ItemDatabase.Instance != null ? ItemDatabase.Instance.GetId(item) : (ushort)0;
|
||||
if (id != 0)
|
||||
PickableRegistry.Instance.RequestDropServerRpc(id, quantity, uses, dropPos, origin.rotation);
|
||||
else
|
||||
Debug.LogWarning($"[PlayerInventory] '{item.ItemName}' is not in the ItemDatabase — drop not spawned. Run Rebuild Item Database.", this);
|
||||
}
|
||||
|
||||
SpawnInWorld(item, quantity, uses);
|
||||
|
||||
RemoveItem(index, quantity);
|
||||
PlayerEvents.RaiseItemDropped(item, quantity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asks the server to spawn a stack in front of the player, without touching the inventory. Shared
|
||||
/// by the deliberate drop (which removes the stack first) and by the overflow safety net (whose
|
||||
/// stack was never stored), so both go through the same authoritative drop path.
|
||||
/// </summary>
|
||||
private void SpawnInWorld(ItemData item, int quantity, int uses)
|
||||
{
|
||||
if (item == null || quantity <= 0) return;
|
||||
|
||||
Transform origin = dropOrigin != null ? dropOrigin : transform;
|
||||
if (item.WorldPrefab == null || origin == null || PickableRegistry.Instance == null) return;
|
||||
|
||||
ushort id = ItemDatabase.Instance != null ? ItemDatabase.Instance.GetId(item) : (ushort)0;
|
||||
if (id == 0)
|
||||
{
|
||||
Debug.LogWarning($"[PlayerInventory] '{item.ItemName}' is not in the ItemDatabase — drop not spawned. Run Rebuild Item Database.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 dropPos = origin.position + origin.forward * dropForwardDistance;
|
||||
PickableRegistry.Instance.RequestDropServerRpc(id, quantity, uses, dropPos, origin.rotation);
|
||||
}
|
||||
|
||||
public void SelectHotbarSlot(int index)
|
||||
{
|
||||
if (index < 0 || index >= hotbarSize) return;
|
||||
|
||||
Reference in New Issue
Block a user