From 654ddca3572b9a457b06181113669400e56c6fea Mon Sep 17 00:00:00 2001 From: I am creating a game Date: Thu, 30 Jul 2026 17:25:13 -0700 Subject: [PATCH 1/2] Server-Authoritative contributions. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These items should facilitate Server-Authoritative functionality in GC2 Arawn Extensions. Tested and utilized in PurrNet with Edgegap. Primary changes are 1) NetworkWorldObject - Facilitates server control of the existence of an item that can be interacted with. 2) NetworkLootContainer - “This object is allowed to generate loot, using this server-side loot table.” 3) InstructionNetworkPickupRequest - Submits server request to pick up an item and move it to a GC2 Inventory Bag 4) InstructionNetworkLootRequest - “Player interacted with this lootable object, ask the server to generate loot.” This is my first pull request to a real repo. Feedback appreciated. Much of what I accomplish is via GPT. --- InstructionNetworkLootRequest.cs | 96 + InstructionNetworkPickupRequest.cs | 96 + NetworkInventoryController.Client.cs | 1456 +++++++++++++ ...ventoryController.Server.SyncAndHelpers.cs | 924 +++++++++ NetworkInventoryController.Server.cs | 1766 ++++++++++++++++ ...rkInventoryController.WorldObjectPickup.cs | 264 +++ NetworkInventoryController.cs | 536 +++++ NetworkInventoryManager.cs | 1815 +++++++++++++++++ NetworkInventoryPatchHooks.cs | 153 ++ NetworkInventoryTypes.cs | 917 +++++++++ NetworkLootContainer.cs | 41 + NetworkWorldObject.cs | 229 +++ 12 files changed, 8293 insertions(+) create mode 100644 InstructionNetworkLootRequest.cs create mode 100644 InstructionNetworkPickupRequest.cs create mode 100644 NetworkInventoryController.Client.cs create mode 100644 NetworkInventoryController.Server.SyncAndHelpers.cs create mode 100644 NetworkInventoryController.Server.cs create mode 100644 NetworkInventoryController.WorldObjectPickup.cs create mode 100644 NetworkInventoryController.cs create mode 100644 NetworkInventoryManager.cs create mode 100644 NetworkInventoryPatchHooks.cs create mode 100644 NetworkInventoryTypes.cs create mode 100644 NetworkLootContainer.cs create mode 100644 NetworkWorldObject.cs diff --git a/InstructionNetworkLootRequest.cs b/InstructionNetworkLootRequest.cs new file mode 100644 index 0000000..7363b6d --- /dev/null +++ b/InstructionNetworkLootRequest.cs @@ -0,0 +1,96 @@ +#if GC2_INVENTORY +using System; +using System.Threading.Tasks; +using GameCreator.Runtime.Characters; +using GameCreator.Runtime.Common; +using GameCreator.Runtime.VisualScripting; +using UnityEngine; + +namespace Arawn.GameCreator2.Networking.Inventory +{ + // [LOCAL-EDIT] #INVENTORY-SERVER-LOOT + // Adds server-authoritative GC2 loot-container generation on top of Arawn's inventory networking layer. + [Title("Network Loot Request")] + [Description("Requests server-authoritative loot generation for a Network Loot Container")] + [Category("Network/Inventory/Network Loot Request")] + [Parameter("Loot Container", "GameObject with the NetworkInventoryController and NetworkLootContainer. Usually Self.")] + [Parameter("Actor", "Player or character GameObject with the NetworkInventoryController that owns the request.")] + [Parameter("Log Diagnostics", "Print diagnostic logs when this instruction sends or rejects a loot request.")] + [Keywords("Network", "Inventory", "Loot", "Container", "Server")] + [Serializable] + public sealed class InstructionNetworkLootRequest : Instruction + { + [Header("Loot Container")] + [SerializeField] + [Tooltip("GameObject with the NetworkInventoryController and NetworkLootContainer. Usually Self.")] + private PropertyGetGameObject m_LootContainer = GetGameObjectSelf.Create(); + + [Header("Actor")] + [SerializeField] + [Tooltip("Player or character GameObject with the NetworkInventoryController that owns the request.")] + private PropertyGetGameObject m_Actor = GetGameObjectPlayer.Create(); + + [Header("Debug")] + [SerializeField] + [Tooltip("Print diagnostic logs when this instruction sends or rejects a loot request.")] + private bool m_LogDiagnostics; + + public override string Title => $"Network Loot {m_LootContainer}"; + + protected override Task Run(Args args) + { + GameObject containerObject = m_LootContainer.Get(args); + GameObject actorObject = m_Actor.Get(args); + + if (containerObject == null) + { + LogWarning("No loot container resolved."); + return DefaultResult; + } + + if (actorObject == null) + { + LogWarning("No actor resolved."); + return DefaultResult; + } + + NetworkInventoryController containerInventory = + containerObject.GetComponentInParent() ?? + containerObject.GetComponentInChildren(); + + NetworkInventoryController actorInventory = + actorObject.GetComponentInParent() ?? + actorObject.GetComponentInChildren(); + + if (containerInventory == null) + { + LogWarning($"Loot container '{containerObject.name}' has no NetworkInventoryController."); + return DefaultResult; + } + + if (actorInventory == null) + { + LogWarning($"Actor '{actorObject.name}' has no NetworkInventoryController."); + return DefaultResult; + } + + actorInventory.RequestLootGeneration(containerInventory); + Log($"sent loot request actor={actorObject.name} actorBag={actorInventory.NetworkId} container={containerObject.name} containerBag={containerInventory.NetworkId}"); + + return DefaultResult; + } + + private void Log(string message) + { + if (!m_LogDiagnostics) return; + Debug.Log($"[InstructionNetworkLootRequest] {message}"); + } + + private void LogWarning(string message) + { + if (!m_LogDiagnostics) return; + Debug.LogWarning($"[InstructionNetworkLootRequest] {message}"); + } + } +} +#endif diff --git a/InstructionNetworkPickupRequest.cs b/InstructionNetworkPickupRequest.cs new file mode 100644 index 0000000..9c81804 --- /dev/null +++ b/InstructionNetworkPickupRequest.cs @@ -0,0 +1,96 @@ +#if GC2_INVENTORY +using System; +using System.Threading.Tasks; +using GameCreator.Runtime.Characters; +using GameCreator.Runtime.Common; +using GameCreator.Runtime.Inventory; +using GameCreator.Runtime.VisualScripting; +using UnityEngine; + +namespace Arawn.GameCreator2.Networking.Inventory +{ + [Title("Network Pickup Request")] + [Description("Requests a server-authoritative pickup of a Network World Object")] + [Category("Network/Inventory/Network Pickup Request")] + [Parameter("Pickup Source", "GameObject with the NetworkWorldObject to pick up. Usually Self.")] + [Parameter("Picker", "Player or character GameObject with the NetworkInventoryController that receives the item.")] + [Parameter("Destination Position", "Inventory destination cell. Use (-1, -1) to let the bag auto-place the item.")] + [Parameter("Log Diagnostics", "Print diagnostic logs when this instruction sends or rejects a pickup request.")] + [Keywords("Network", "Inventory", "Pickup", "Item", "World")] + [Serializable] + public sealed class InstructionNetworkPickupRequest : Instruction + { + // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT + [Header("Pickup")] + [SerializeField] + [Tooltip("GameObject with the NetworkWorldObject to pick up. Usually Self.")] + private PropertyGetGameObject m_PickupSource = GetGameObjectSelf.Create(); + + [SerializeField] + [Tooltip("Player or character GameObject with the NetworkInventoryController that receives the item.")] + private PropertyGetGameObject m_Picker = GetGameObjectPlayer.Create(); + + [SerializeField] + [Tooltip("Inventory destination cell. Use (-1, -1) to let the bag auto-place the item.")] + private Vector2Int m_DestinationPosition = TBagContent.INVALID; + + [Header("Debug")] + [SerializeField] + [Tooltip("Print diagnostic logs when this instruction sends or rejects a pickup request.")] + private bool m_LogDiagnostics; + + public override string Title => $"Network Pickup {m_PickupSource}"; + + protected override Task Run(Args args) + { + GameObject sourceObject = m_PickupSource.Get(args); + GameObject pickerObject = m_Picker.Get(args); + + if (sourceObject == null) + { + LogWarning("No pickup source resolved."); + return DefaultResult; + } + + if (pickerObject == null) + { + LogWarning("No picker resolved."); + return DefaultResult; + } + + NetworkWorldObject worldObject = sourceObject.GetComponentInParent(); + if (worldObject == null) + { + LogWarning($"Pickup source '{sourceObject.name}' has no NetworkWorldObject."); + return DefaultResult; + } + + NetworkInventoryController pickerInventory = pickerObject.GetComponent(); + if (pickerInventory == null) + { + LogWarning($"Picker '{pickerObject.name}' has no NetworkInventoryController."); + return DefaultResult; + } + + pickerInventory.RequestWorldObjectPickup(worldObject, m_DestinationPosition); + Log( + $"sent pickup source={sourceObject.name} picker={pickerObject.name} " + + $"worldObject={worldObject.NetworkId} item={worldObject.Item?.ID.String} destination={m_DestinationPosition}"); + + return DefaultResult; + } + + private void Log(string message) + { + if (!m_LogDiagnostics) return; + Debug.Log($"[InstructionNetworkPickupRequest] {message}"); + } + + private void LogWarning(string message) + { + if (!m_LogDiagnostics) return; + Debug.LogWarning($"[InstructionNetworkPickupRequest] {message}"); + } + } +} +#endif diff --git a/NetworkInventoryController.Client.cs b/NetworkInventoryController.Client.cs new file mode 100644 index 0000000..18d8ba1 --- /dev/null +++ b/NetworkInventoryController.Client.cs @@ -0,0 +1,1456 @@ +#if GC2_INVENTORY +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using UnityEngine; +using GameCreator.Runtime.Common; +using GameCreator.Runtime.Inventory; + +namespace Arawn.GameCreator2.Networking.Inventory +{ + // ════════════════════════════════════════════════════════════════════════════════════════════ + // CLIENT-SIDE — Requests, response handlers, and local change detection + // ════════════════════════════════════════════════════════════════════════════════════════════ + + public partial class NetworkInventoryController + { + // ════════════════════════════════════════════════════════════════════════════════════════ + // CLIENT-SIDE: REQUEST OPERATIONS + // ════════════════════════════════════════════════════════════════════════════════════════ + + #region Content Requests + + /// + /// Request to add an item type to the bag. + /// + public void RequestAddItem(Item item, Vector2Int position, bool allowStack, + InventoryModificationSource source = InventoryModificationSource.Direct, int sourceHash = 0) + { + if (m_IsRemoteClient) + { + Debug.LogWarning("[NetworkInventoryController] Cannot modify inventory on remote client"); + return; + } + + var request = new NetworkContentAddRequest + { + RequestId = GetNextRequestId(), + ActorNetworkId = NetworkId, + CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), + TargetBagNetworkId = NetworkId, + ItemHash = item.ID.Hash, + ItemIdString = item.ID.String, + Position = position, + AllowStack = allowStack, + Source = source, + SourceHash = sourceHash + }; + + m_PendingAdds[GetPendingKey(request.ActorNetworkId, request.CorrelationId, request.RequestId)] = new PendingContentAdd + { + Request = request, + SentTime = Time.time + }; + + OnContentAddRequested?.Invoke(request); + + if (m_IsServer) + { + var response = ProcessContentAddRequest(request, NetworkId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + ReceiveContentAddResponse(response); + } + else + { + NetworkInventoryManager.Instance?.SendContentAddRequest(request); + } + } + + /// + /// Request to add an existing RuntimeItem to the bag. + /// + public void RequestAddRuntimeItem(NetworkRuntimeItem runtimeItem, Vector2Int position, bool allowStack, + InventoryModificationSource source = InventoryModificationSource.Direct, int sourceHash = 0) + { + if (m_IsRemoteClient) return; + + // Arbitrary runtime payload creation is server-authorized only. + if (!m_IsServer) + { + if (m_LogRejections) + { + Debug.LogWarning("[NetworkInventoryController] RequestAddRuntimeItem is server-authorized only"); + } + OnOperationRejected?.Invoke(InventoryRejectionReason.SecurityViolation, "Add runtime item"); + return; + } + + if (runtimeItem.ItemHash == 0) + { + if (m_LogRejections) + { + Debug.LogWarning("[NetworkInventoryController] Runtime item payload missing item hash"); + } + OnOperationRejected?.Invoke(InventoryRejectionReason.IdentityMismatch, "Add runtime item"); + return; + } + + var request = new NetworkContentAddRequest + { + RequestId = GetNextRequestId(), + ActorNetworkId = NetworkId, + CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), + TargetBagNetworkId = NetworkId, + // Server-authorized flow resolves by deterministic item hash. + ItemHash = runtimeItem.ItemHash, + ItemIdString = string.Empty, + RuntimeItem = runtimeItem, + Position = position, + AllowStack = allowStack, + Source = source, + SourceHash = sourceHash + }; + + m_PendingAdds[GetPendingKey(request.ActorNetworkId, request.CorrelationId, request.RequestId)] = new PendingContentAdd + { + Request = request, + SentTime = Time.time + }; + + OnContentAddRequested?.Invoke(request); + + if (m_IsServer) + { + var response = ProcessContentAddRequest(request, NetworkId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + ReceiveContentAddResponse(response); + } + } + + /// + /// Request to remove an item from the bag. + /// + public void RequestRemoveItem(RuntimeItem runtimeItem, + InventoryModificationSource source = InventoryModificationSource.Direct) + { + if (m_IsRemoteClient) return; + if (runtimeItem == null) return; + + var request = new NetworkContentRemoveRequest + { + RequestId = GetNextRequestId(), + ActorNetworkId = NetworkId, + CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), + TargetBagNetworkId = NetworkId, + RuntimeIdHash = runtimeItem.RuntimeID.Hash, + UsePosition = false, + Source = source + }; + + m_PendingRemoves[GetPendingKey(request.ActorNetworkId, request.CorrelationId, request.RequestId)] = new PendingContentRemove + { + Request = request, + RemovedItem = runtimeItem, + SentTime = Time.time + }; + + OnContentRemoveRequested?.Invoke(request); + + if (m_IsServer) + { + var response = ProcessContentRemoveRequest(request, NetworkId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + ReceiveContentRemoveResponse(response); + } + else + { + NetworkInventoryManager.Instance?.SendContentRemoveRequest(request); + } + } + + /// + /// Request to remove item at position. + /// + public void RequestRemoveAtPosition(Vector2Int position, + InventoryModificationSource source = InventoryModificationSource.Direct) + { + if (m_IsRemoteClient) return; + + var request = new NetworkContentRemoveRequest + { + RequestId = GetNextRequestId(), + ActorNetworkId = NetworkId, + CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), + TargetBagNetworkId = NetworkId, + Position = position, + UsePosition = true, + Source = source + }; + + m_PendingRemoves[GetPendingKey(request.ActorNetworkId, request.CorrelationId, request.RequestId)] = new PendingContentRemove + { + Request = request, + SentTime = Time.time + }; + + OnContentRemoveRequested?.Invoke(request); + + if (m_IsServer) + { + var response = ProcessContentRemoveRequest(request, NetworkId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + ReceiveContentRemoveResponse(response); + } + else + { + NetworkInventoryManager.Instance?.SendContentRemoveRequest(request); + } + } + + /// + /// Request to move item within bag. + /// + public void RequestMoveItem(Vector2Int fromPosition, Vector2Int toPosition, bool allowStack) + { + if (m_IsRemoteClient) return; + + var request = new NetworkContentMoveRequest + { + RequestId = GetNextRequestId(), + ActorNetworkId = NetworkId, + CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), + TargetBagNetworkId = NetworkId, + FromPosition = fromPosition, + ToPosition = toPosition, + AllowStack = allowStack + }; + + m_PendingMoves[GetPendingKey(request.ActorNetworkId, request.CorrelationId, request.RequestId)] = new PendingContentMove + { + Request = request, + SentTime = Time.time + }; + + OnContentMoveRequested?.Invoke(request); + + if (m_IsServer) + { + var response = ProcessContentMoveRequest(request, NetworkId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + ReceiveContentMoveResponse(response); + } + else + { + NetworkInventoryManager.Instance?.SendContentMoveRequest(request); + } + } + + /// + /// Request to use an item. + /// + public void RequestUseItem(RuntimeItem runtimeItem) + { + if (m_IsRemoteClient) return; + if (runtimeItem == null) return; + + var request = new NetworkContentUseRequest + { + RequestId = GetNextRequestId(), + ActorNetworkId = NetworkId, + CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), + TargetBagNetworkId = NetworkId, + RuntimeIdHash = runtimeItem.RuntimeID.Hash, + UsePosition = false + }; + + OnContentUseRequested?.Invoke(request); + + if (m_IsServer) + { + var response = ProcessContentUseRequest(request, NetworkId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + ReceiveContentUseResponse(response); + } + else + { + NetworkInventoryManager.Instance?.SendContentUseRequest(request); + } + } + + /// + /// Request to drop an item. + /// + public void RequestDropItem(RuntimeItem runtimeItem, Vector3 dropPosition, int maxAmount = 1) + { + if (m_IsRemoteClient) return; + if (runtimeItem == null) return; + + SendDropRequest(NetworkId, NetworkId, runtimeItem, dropPosition, maxAmount); + } + + private void SendDropRequest(uint actorNetworkId, uint targetBagNetworkId, RuntimeItem runtimeItem, Vector3 dropPosition, int maxAmount = 1) + { + if (runtimeItem == null || actorNetworkId == 0 || targetBagNetworkId == 0) return; + + var request = new NetworkContentDropRequest + { + RequestId = GetNextRequestId(), + ActorNetworkId = actorNetworkId, + CorrelationId = NetworkCorrelation.Compose(actorNetworkId, m_LastIssuedRequestId), + TargetBagNetworkId = targetBagNetworkId, + RuntimeIdHash = runtimeItem.RuntimeID.Hash, + DropPosition = dropPosition, + MaxAmount = maxAmount + }; + + LogPickupDebug( + $"{name}: sending drop request req={request.RequestId} actor={actorNetworkId} targetBag={targetBagNetworkId} item={DescribeRuntimeItem(runtimeItem)} position={dropPosition} server={m_IsServer} local={m_IsLocalClient}", + this); + + if (m_IsServer) + { + var response = ProcessContentDropRequest(request, NetworkId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + ReceiveContentDropResponse(response); + } + else + { + NetworkInventoryManager.Instance?.SendContentDropRequest(request); + } + } + + // [LOCAL-EDIT] #PILFER-INVENTORY-UI-TRANSFER-BRIDGE + public void RequestTransferItem(NetworkInventoryController destination, RuntimeItem runtimeItem, Vector2Int destinationPosition, bool allowStack) + { + if (destination == null || runtimeItem == null) return; + + if (destination == this) + { + Vector2Int sourcePosition = m_Bag.Content.FindPosition(runtimeItem.RuntimeID); + if (sourcePosition == TBagContent.INVALID) return; + + // TODO: Same world/container reorganization needs explicit access rules before client UI can request it. + if (IsWorldInventory) + { + LogPickupWarning( + $"{name}: same world-container move skipped until container access rules are implemented item={DescribeRuntimeItem(runtimeItem)} source={sourcePosition} destination={destinationPosition}", + this); + return; + } + + RequestMoveItem(sourcePosition, destinationPosition, allowStack); + return; + } + + if (NetworkId == 0 || destination.NetworkId == 0) return; + if (!TryGetLocalActorNetworkId(out uint actorNetworkId)) + { + LogPickupWarning( + $"{name}: transfer request skipped no local actor network id sourceBag={NetworkId} destinationBag={destination.NetworkId} item={DescribeRuntimeItem(runtimeItem)}", + this); + return; + } + + var request = new NetworkTransferRequest + { + RequestId = GetNextRequestId(), + ActorNetworkId = actorNetworkId, + CorrelationId = NetworkCorrelation.Compose(actorNetworkId, m_LastIssuedRequestId), + SourceBagNetworkId = NetworkId, + DestinationBagNetworkId = destination.NetworkId, + RuntimeIdHash = runtimeItem.RuntimeID.Hash, + DestinationPosition = destinationPosition, + AllowStack = allowStack, + Source = InventoryModificationSource.Loot + }; + + LogPickupDebug( + $"{name}: sending transfer request req={request.RequestId} actor={actorNetworkId} sourceBag={NetworkId} destinationBag={destination.NetworkId} item={DescribeRuntimeItem(runtimeItem)} destination={destinationPosition} server={m_IsServer}", + this); + + if (m_IsServer) + { + NetworkTransferResponse response = ProcessTransferRequest(request, destination, NetworkId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + NetworkInventoryManager.Instance?.ReceiveTransferResponse(response, actorNetworkId); + return; + } + + NetworkInventoryManager.Instance?.SendTransferRequest(request); + } + + // [LOCAL-EDIT] #INVENTORY-SERVER-LOOT + public void RequestLootGeneration(NetworkInventoryController containerInventory) + { + if (containerInventory == null) return; + if (m_IsRemoteClient) return; + if (!m_IsLocalClient && !m_IsServer) return; + + if (!TryGetLocalActorNetworkId(out uint actorNetworkId)) + { + LogPickupWarning($"{name}: loot request skipped no local actor network id container={containerInventory.NetworkId}", this); + return; + } + + var request = new NetworkLootRequest + { + RequestId = GetNextRequestId(), + ActorNetworkId = actorNetworkId, + CorrelationId = NetworkCorrelation.Compose(actorNetworkId, m_LastIssuedRequestId), + ContainerBagNetworkId = containerInventory.NetworkId + }; + + Debug.Log( + $"[NetworkInventoryLootDebug] {name}: sending loot request req={request.RequestId} actor={request.ActorNetworkId} container={request.ContainerBagNetworkId} server={m_IsServer} local={m_IsLocalClient}"); + + if (m_IsServer) + { + NetworkLootResponse response = containerInventory.ProcessLootRequest(request, NetworkId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + return; + } + + NetworkInventoryManager.Instance?.SendLootRequest(request); + } + + #endregion + + #region Equipment Requests + + /// + /// Request to equip an item. + /// + public void RequestEquip(RuntimeItem runtimeItem, int slot = -1) + { + if (m_IsRemoteClient) return; + if (runtimeItem == null) return; + + var request = new NetworkEquipmentRequest + { + RequestId = GetNextRequestId(), + ActorNetworkId = NetworkId, + CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), + TargetBagNetworkId = NetworkId, + RuntimeIdHash = runtimeItem.RuntimeID.Hash, + Action = slot >= 0 ? EquipmentAction.EquipToSlot : EquipmentAction.Equip, + SlotOrIndex = slot + }; + + m_PendingEquipment[GetPendingKey(request.ActorNetworkId, request.CorrelationId, request.RequestId)] = new PendingEquipment + { + Request = request, + SentTime = Time.time + }; + + OnEquipmentRequested?.Invoke(request); + + if (m_IsServer) + { + _ = ProcessLocalEquipmentRequestAsync(request); + } + else + { + NetworkInventoryManager.Instance?.SendEquipmentRequest(request); + } + } + + /// + /// Request to unequip an item. + /// + public void RequestUnequip(RuntimeItem runtimeItem) + { + if (m_IsRemoteClient) return; + if (runtimeItem == null) return; + + var request = new NetworkEquipmentRequest + { + RequestId = GetNextRequestId(), + ActorNetworkId = NetworkId, + CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), + TargetBagNetworkId = NetworkId, + RuntimeIdHash = runtimeItem.RuntimeID.Hash, + Action = EquipmentAction.Unequip, + SlotOrIndex = -1 + }; + + m_PendingEquipment[GetPendingKey(request.ActorNetworkId, request.CorrelationId, request.RequestId)] = new PendingEquipment + { + Request = request, + SentTime = Time.time + }; + + OnEquipmentRequested?.Invoke(request); + + if (m_IsServer) + { + _ = ProcessLocalEquipmentRequestAsync(request); + } + else + { + NetworkInventoryManager.Instance?.SendEquipmentRequest(request); + } + } + + /// + /// Request to unequip from specific index. + /// + public void RequestUnequipFromIndex(int index) + { + if (m_IsRemoteClient) return; + + var request = new NetworkEquipmentRequest + { + RequestId = GetNextRequestId(), + ActorNetworkId = NetworkId, + CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), + TargetBagNetworkId = NetworkId, + RuntimeIdHash = 0, + Action = EquipmentAction.UnequipFromIndex, + SlotOrIndex = index + }; + + m_PendingEquipment[GetPendingKey(request.ActorNetworkId, request.CorrelationId, request.RequestId)] = new PendingEquipment + { + Request = request, + SentTime = Time.time + }; + + OnEquipmentRequested?.Invoke(request); + + if (m_IsServer) + { + _ = ProcessLocalEquipmentRequestAsync(request); + } + else + { + NetworkInventoryManager.Instance?.SendEquipmentRequest(request); + } + } + + #endregion + + #region Socket Requests + + /// + /// Request to attach item to socket. + /// + public void RequestAttachToSocket(RuntimeItem parent, RuntimeItem attachment, IdString socketId = default) + { + if (m_IsRemoteClient) return; + if (parent == null || attachment == null) return; + + var request = new NetworkSocketRequest + { + RequestId = GetNextRequestId(), + ActorNetworkId = NetworkId, + CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), + TargetBagNetworkId = NetworkId, + ParentRuntimeIdHash = parent.RuntimeID.Hash, + AttachmentRuntimeIdHash = attachment.RuntimeID.Hash, + SocketHash = socketId.Hash, + SocketIdString = socketId.String, + Action = socketId.Hash != 0 ? SocketAction.AttachToSocket : SocketAction.Attach + }; + + OnSocketRequested?.Invoke(request); + + if (m_IsServer) + { + var response = ProcessSocketRequest(request, NetworkId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + ReceiveSocketResponse(response); + } + else + { + NetworkInventoryManager.Instance?.SendSocketRequest(request); + } + } + + /// + /// Request to detach from socket. + /// + public void RequestDetachFromSocket(RuntimeItem parent, IdString socketId) + { + if (m_IsRemoteClient) return; + if (parent == null) return; + + var request = new NetworkSocketRequest + { + RequestId = GetNextRequestId(), + ActorNetworkId = NetworkId, + CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), + TargetBagNetworkId = NetworkId, + ParentRuntimeIdHash = parent.RuntimeID.Hash, + SocketHash = socketId.Hash, + SocketIdString = socketId.String, + Action = SocketAction.DetachFromSocket + }; + + OnSocketRequested?.Invoke(request); + + if (m_IsServer) + { + var response = ProcessSocketRequest(request, NetworkId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + ReceiveSocketResponse(response); + } + else + { + NetworkInventoryManager.Instance?.SendSocketRequest(request); + } + } + + /// + /// Request to detach a specific attached item from its parent. + /// + public void RequestDetachFromSocket(RuntimeItem parent, RuntimeItem attachment) + { + if (m_IsRemoteClient) return; + if (parent == null || attachment == null) return; + + var request = new NetworkSocketRequest + { + RequestId = GetNextRequestId(), + ActorNetworkId = NetworkId, + CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), + TargetBagNetworkId = NetworkId, + ParentRuntimeIdHash = parent.RuntimeID.Hash, + AttachmentRuntimeIdHash = attachment.RuntimeID.Hash, + Action = SocketAction.Detach + }; + + OnSocketRequested?.Invoke(request); + + if (m_IsServer) + { + var response = ProcessSocketRequest(request, NetworkId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + ReceiveSocketResponse(response); + } + else + { + NetworkInventoryManager.Instance?.SendSocketRequest(request); + } + } + + #endregion + + #region Wealth Requests + + /// + /// Request to modify wealth. + /// + public void RequestWealthModify(Currency currency, int value, WealthAction action, + InventoryModificationSource source = InventoryModificationSource.Direct, int sourceHash = 0) + { + if (m_IsRemoteClient) return; + if (currency == null) return; + + var request = new NetworkWealthRequest + { + RequestId = GetNextRequestId(), + ActorNetworkId = NetworkId, + CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), + TargetBagNetworkId = NetworkId, + CurrencyHash = currency.ID.Hash, + CurrencyIdString = currency.ID.String, + Value = value, + Action = action, + Source = source, + SourceHash = sourceHash + }; + + int originalValue = m_Bag.Wealth.Get(currency); + + m_PendingWealth[GetPendingKey(request.ActorNetworkId, request.CorrelationId, request.RequestId)] = new PendingWealth + { + Request = request, + OriginalValue = originalValue, + SentTime = Time.time + }; + + OnWealthRequested?.Invoke(request); + + if (m_IsServer) + { + var response = ProcessWealthRequest(request, NetworkId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + ReceiveWealthResponse(response); + } + else + { + NetworkInventoryManager.Instance?.SendWealthRequest(request); + } + } + + #endregion + + // ════════════════════════════════════════════════════════════════════════════════════════ + // CLIENT-SIDE: RECEIVE RESPONSES + // ════════════════════════════════════════════════════════════════════════════════════════ + + #region Client Response Handlers + + public void ReceiveContentAddResponse(NetworkContentAddResponse response) + { + ulong key = GetPendingKey(response.ActorNetworkId, response.CorrelationId, response.RequestId); + if (!m_PendingAdds.TryGetValue(key, out var pending)) + return; + + m_PendingAdds.Remove(key); + + if (!response.Authorized) + { + if (m_LogRejections) + Debug.LogWarning($"[NetworkInventoryController] Add rejected: {response.RejectionReason}"); + OnOperationRejected?.Invoke(response.RejectionReason, "Add item"); + } + } + + public void ReceiveContentRemoveResponse(NetworkContentRemoveResponse response) + { + ulong key = GetPendingKey(response.ActorNetworkId, response.CorrelationId, response.RequestId); + if (!m_PendingRemoves.TryGetValue(key, out var pending)) + return; + + m_PendingRemoves.Remove(key); + + if (!response.Authorized) + { + if (m_LogRejections) + Debug.LogWarning($"[NetworkInventoryController] Remove rejected: {response.RejectionReason}"); + OnOperationRejected?.Invoke(response.RejectionReason, "Remove item"); + } + } + + public void ReceiveContentMoveResponse(NetworkContentMoveResponse response) + { + ulong key = GetPendingKey(response.ActorNetworkId, response.CorrelationId, response.RequestId); + if (!m_PendingMoves.TryGetValue(key, out var pending)) + return; + + m_PendingMoves.Remove(key); + + if (!response.Authorized) + { + if (m_LogRejections) + Debug.LogWarning($"[NetworkInventoryController] Move rejected: {response.RejectionReason}"); + OnOperationRejected?.Invoke(response.RejectionReason, "Move item"); + } + } + + public void ReceiveContentUseResponse(NetworkContentUseResponse response) + { + if (!response.Authorized) + { + if (m_LogRejections) + Debug.LogWarning($"[NetworkInventoryController] Use rejected: {response.RejectionReason}"); + OnOperationRejected?.Invoke(response.RejectionReason, "Use item"); + } + } + + public void ReceiveContentDropResponse(NetworkContentDropResponse response) + { + if (!response.Authorized) + { + if (m_LogRejections) + Debug.LogWarning($"[NetworkInventoryController] Drop rejected: {response.RejectionReason}"); + OnOperationRejected?.Invoke(response.RejectionReason, "Drop item"); + } + } + + public void ReceiveEquipmentResponse(NetworkEquipmentResponse response) + { + ulong key = GetPendingKey(response.ActorNetworkId, response.CorrelationId, response.RequestId); + if (!m_PendingEquipment.TryGetValue(key, out var pending)) + return; + + m_PendingEquipment.Remove(key); + + if (!response.Authorized) + { + if (m_LogRejections) + Debug.LogWarning($"[NetworkInventoryController] Equipment rejected: {response.RejectionReason}"); + OnOperationRejected?.Invoke(response.RejectionReason, "Equipment operation"); + } + } + + public void ReceiveSocketResponse(NetworkSocketResponse response) + { + if (!response.Authorized) + { + if (m_LogRejections) + Debug.LogWarning($"[NetworkInventoryController] Socket rejected: {response.RejectionReason}"); + OnOperationRejected?.Invoke(response.RejectionReason, "Socket operation"); + } + } + + public void ReceiveWealthResponse(NetworkWealthResponse response) + { + ulong key = GetPendingKey(response.ActorNetworkId, response.CorrelationId, response.RequestId); + if (!m_PendingWealth.TryGetValue(key, out var pending)) + return; + + m_PendingWealth.Remove(key); + + if (!response.Authorized) + { + if (m_LogRejections) + Debug.LogWarning($"[NetworkInventoryController] Wealth rejected: {response.RejectionReason}"); + OnOperationRejected?.Invoke(response.RejectionReason, "Wealth operation"); + } + } + + #endregion + + // ════════════════════════════════════════════════════════════════════════════════════════ + // LOCAL CHANGE DETECTION + // ════════════════════════════════════════════════════════════════════════════════════════ + + private void OnLocalItemAdded(RuntimeItem item) + { + if (item != null) + { + TrackRuntimeItemRecursive(item); + } + + LogPickupDebug( + $"{name}: local add observed item={DescribeRuntimeItem(item)} bag={NetworkId} server={m_IsServer} local={m_IsLocalClient} remote={m_IsRemoteClient} applying={m_IsApplyingNetworkState} " + + $"hasDroppedInstance={(item != null && s_DroppedItemInstances.ContainsKey(item.RuntimeID.Hash))} position={(item != null ? m_Bag.Content.FindPosition(item.RuntimeID).ToString() : "n/a")}", + this); + + if (m_IsServer && !m_IsApplyingNetworkState && item != null) + { + BroadcastServerPickupFromDroppedItemIfNeeded(item); + } + else if (!m_IsServer && !m_IsApplyingNetworkState) + { + if (!TrySendPickupForLocalAdd(item)) + { + TrySendTransferForLocalAdd(item); + } + } + + if (m_LogAllChanges && !m_IsServer) + Debug.Log($"[NetworkInventoryController] Local item added: {item?.ItemID.String}"); + } + + private void OnLocalItemRemoved(RuntimeItem item) + { + if (item != null) + { + if (ContainsRuntimeItemRecursive(item.RuntimeID.Hash)) + { + TrackRuntimeItemRecursive(item); + } + else + { + UntrackRuntimeItemRecursive(item); + } + } + + if (!m_IsApplyingNetworkState) + { + RememberLocalRemoval(this, item); + } + + if (m_LogAllChanges && !m_IsServer) + Debug.Log($"[NetworkInventoryController] Local item removed: {item?.ItemID.String}"); + } + + private void OnLocalItemUsed(RuntimeItem item) + { + if (!m_IsServer && !m_IsApplyingNetworkState && item != null && m_IsLocalClient) + { + RequestUseItem(item); + } + + if (m_LogAllChanges && !m_IsServer) + Debug.Log($"[NetworkInventoryController] Local item used: {item?.ItemID.String}"); + } + + private void OnLocalItemEquipped(RuntimeItem item, int index) + { + if (!m_IsServer && !m_IsApplyingNetworkState && item != null && m_IsLocalClient) + { + RequestEquipToIndexFromLocalEvent(item, index); + } + + if (m_LogAllChanges && !m_IsServer) + Debug.Log($"[NetworkInventoryController] Local item equipped: {item?.ItemID.String} at {index}"); + } + + private void OnLocalItemUnequipped(RuntimeItem item, int index) + { + if (!m_IsServer && !m_IsApplyingNetworkState && m_IsLocalClient) + { + RequestUnequipFromIndex(index); + } + + if (m_LogAllChanges && !m_IsServer) + Debug.Log($"[NetworkInventoryController] Local item unequipped: {item?.ItemID.String} from {index}"); + } + + private void OnLocalWealthChanged(IdString currencyId, int oldValue, int newValue) + { + if (m_LogAllChanges && !m_IsServer) + Debug.Log($"[NetworkInventoryController] Local wealth changed: {currencyId.String} {oldValue} -> {newValue}"); + } + + private async Task ProcessLocalEquipmentRequestAsync(NetworkEquipmentRequest request) + { + NetworkEquipmentResponse response = await ProcessEquipmentRequest(request, NetworkId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + ReceiveEquipmentResponse(response); + } + + private void RequestEquipToIndexFromLocalEvent(RuntimeItem runtimeItem, int index) + { + if (runtimeItem == null) return; + + var request = new NetworkEquipmentRequest + { + RequestId = GetNextRequestId(), + ActorNetworkId = NetworkId, + CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), + TargetBagNetworkId = NetworkId, + RuntimeIdHash = runtimeItem.RuntimeID.Hash, + Action = EquipmentAction.EquipToIndex, + SlotOrIndex = index + }; + + m_PendingEquipment[GetPendingKey(request.ActorNetworkId, request.CorrelationId, request.RequestId)] = new PendingEquipment + { + Request = request, + SentTime = Time.time + }; + + OnEquipmentRequested?.Invoke(request); + NetworkInventoryManager.Instance?.SendEquipmentRequest(request); + } + + private void TrySendTransferForLocalAdd(RuntimeItem item) + { + if (item == null) return; + if (!TryGetLocalActorNetworkId(out uint actorNetworkId)) return; + if (!TryTakePendingRemoval(item.RuntimeID.Hash, out PendingLocalRemoval removal)) return; + if (removal.SourceController == null || removal.SourceController == this) return; + if (removal.SourceController.NetworkId == 0 || NetworkId == 0) return; + + LogPickupDebug( + $"{name}: sending transfer fallback for local add item={DescribeRuntimeItem(item)} actor={actorNetworkId} sourceBag={removal.SourceController.NetworkId} destinationBag={NetworkId}", + this); + + var request = new NetworkTransferRequest + { + RequestId = GetNextRequestId(), + ActorNetworkId = actorNetworkId, + CorrelationId = NetworkCorrelation.Compose(actorNetworkId, m_LastIssuedRequestId), + SourceBagNetworkId = removal.SourceController.NetworkId, + DestinationBagNetworkId = NetworkId, + RuntimeIdHash = item.RuntimeID.Hash, + DestinationPosition = m_Bag.Content.FindPosition(item.RuntimeID), + AllowStack = true, + Source = InventoryModificationSource.Loot + }; + + NetworkInventoryManager.Instance?.SendTransferRequest(request); + } + + private bool TrySendPickupForLocalAdd(RuntimeItem item) + { + if (item == null) return false; + + bool hasDroppedInstance = TryGetDroppedItemInstance(item.RuntimeID.Hash, out DroppedItemInstance droppedItem); + bool exactRuntimeMatch = hasDroppedInstance; + if (!hasDroppedInstance) + { + hasDroppedInstance = TryFindDroppedItemInstanceForLocalPickup(item, out droppedItem); + } + + if (!m_IsLocalClient || !UsesNetworkCharacterId) + { + if (hasDroppedInstance) + { + LogPickupWarning( + $"{name}: pickup request skipped because controller is not a local network-character inventory item={DescribeRuntimeItem(item)} local={m_IsLocalClient} usesCharacterId={UsesNetworkCharacterId} bag={NetworkId}", + this); + } + return false; + } + + if (!TryGetLocalActorNetworkId(out uint actorNetworkId)) + { + LogPickupWarning( + $"{name}: pickup request skipped because no local actor network id was found item={DescribeRuntimeItem(item)} bag={NetworkId}", + this); + return false; + } + + if (!hasDroppedInstance) + { + LogPickupDebug( + $"{name}: pickup request skipped because local add is not a tracked network drop item={DescribeRuntimeItem(item)} bag={NetworkId} trackedDrops={s_DroppedItemInstances.Count}", + this); + return false; + } + + var request = new NetworkPickupRequest + { + RequestId = GetNextRequestId(), + ActorNetworkId = actorNetworkId, + CorrelationId = NetworkCorrelation.Compose(actorNetworkId, m_LastIssuedRequestId), + PickerBagNetworkId = NetworkId, + SourceBagNetworkId = droppedItem.SourceBagNetworkId, + RuntimeIdHash = droppedItem.Item.RuntimeIdHash, + DestinationPosition = m_Bag.Content.FindPosition(item.RuntimeID) + }; + + if (droppedItem.Item.RuntimeIdHash != 0 && droppedItem.Item.RuntimeIdHash != item.RuntimeID.Hash) + { + m_PendingPickupLocalRuntimeByServerRuntime[droppedItem.Item.RuntimeIdHash] = item.RuntimeID.Hash; + } + + LogPickupDebug( + $"{name}: sending pickup request req={request.RequestId} actor={actorNetworkId} pickerBag={NetworkId} sourceBag={droppedItem.SourceBagNetworkId} localItem={DescribeRuntimeItem(item)} serverRuntime={droppedItem.Item.RuntimeIdHash} exactRuntimeMatch={exactRuntimeMatch} destination={request.DestinationPosition} dropPosition={droppedItem.Position} instanceAlive={droppedItem.Instance != null}", + this); + + NetworkInventoryManager.Instance?.SendPickupRequest(request); + return true; + } + + private static void RememberLocalRemoval(NetworkInventoryController source, RuntimeItem item) + { + if (source == null || item == null) return; + if (!source.m_IsServer && !TryGetLocalActorNetworkId(out _)) return; + + PrunePendingLocalRemovals(); + long runtimeIdHash = item.RuntimeID.Hash; + + for (int i = s_PendingLocalRemovals.Count - 1; i >= 0; i--) + { + if (s_PendingLocalRemovals[i].RuntimeIdHash == runtimeIdHash) + { + s_PendingLocalRemovals.RemoveAt(i); + } + } + + s_PendingLocalRemovals.Add(new PendingLocalRemoval + { + SourceController = source, + Item = source.ConvertToNetworkItem(item), + RuntimeIdHash = runtimeIdHash, + Time = Time.unscaledTime + }); + } + + private static bool TryTakePendingRemoval(long runtimeIdHash, out PendingLocalRemoval removal) + { + PrunePendingLocalRemovals(); + + for (int i = 0; i < s_PendingLocalRemovals.Count; i++) + { + if (s_PendingLocalRemovals[i].RuntimeIdHash != runtimeIdHash) continue; + + removal = s_PendingLocalRemovals[i]; + s_PendingLocalRemovals.RemoveAt(i); + return true; + } + + removal = default; + return false; + } + + private static bool TryPeekPendingRemoval(long runtimeIdHash, out PendingLocalRemoval removal) + { + PrunePendingLocalRemovals(); + + for (int i = 0; i < s_PendingLocalRemovals.Count; i++) + { + if (s_PendingLocalRemovals[i].RuntimeIdHash != runtimeIdHash) continue; + + removal = s_PendingLocalRemovals[i]; + return true; + } + + removal = default; + return false; + } + + private static void PrunePendingLocalRemovals() + { + float now = Time.unscaledTime; + for (int i = s_PendingLocalRemovals.Count - 1; i >= 0; i--) + { + if (now - s_PendingLocalRemovals[i].Time <= 2f) continue; + s_PendingLocalRemovals.RemoveAt(i); + } + } + + private static bool TryGetLocalActorNetworkId(out uint actorNetworkId) + { + if (s_LocalPlayerController != null && + s_LocalPlayerController.NetworkId != 0 && + s_LocalPlayerController.m_IsLocalClient) + { + actorNetworkId = s_LocalPlayerController.NetworkId; + return true; + } + + for (int i = 0; i < s_Controllers.Count; i++) + { + NetworkInventoryController controller = s_Controllers[i]; + if (controller == null || !controller.m_IsLocalClient || !controller.UsesNetworkCharacterId) continue; + if (controller.NetworkId == 0) continue; + + s_LocalPlayerController = controller; + actorNetworkId = controller.NetworkId; + return true; + } + + actorNetworkId = 0; + return false; + } + + private static void HandleGlobalItemInstantiated() + { + RuntimeItem item = Item.LastItemInstantiated; + GameObject instance = Item.LastItemInstanceInstantiated; + if (item == null || instance == null) return; + if (!TryPeekPendingRemoval(item.RuntimeID.Hash, out PendingLocalRemoval removal)) return; + if (removal.SourceController == null) return; + + if (removal.SourceController.m_IsServer) + { + RememberDroppedItemInstance(item.RuntimeID.Hash, instance, removal.SourceController.NetworkId, removal.Item, instance.transform.position); + LogPickupDebug( + $"global instantiated server-side drop item={DescribeRuntimeItem(item)} sourceBag={removal.SourceController.NetworkId} position={instance.transform.position}", + instance); + removal.SourceController.BroadcastServerDropFromLocalMutation(removal.Item, item.RuntimeID.Hash, instance.transform.position); + TryTakePendingRemoval(item.RuntimeID.Hash, out _); + return; + } + + if (!TryGetLocalActorNetworkId(out uint actorNetworkId)) return; + + s_LocalDropRuntimeIds.Add(item.RuntimeID.Hash); + RememberDroppedItemInstance(item.RuntimeID.Hash, instance, removal.SourceController.NetworkId, removal.Item, instance.transform.position); + LogPickupDebug( + $"global instantiated client-side drop item={DescribeRuntimeItem(item)} sourceBag={removal.SourceController.NetworkId} actor={actorNetworkId} position={instance.transform.position}", + instance); + removal.SourceController.SendDropRequest( + actorNetworkId, + removal.SourceController.NetworkId, + item, + instance.transform.position, + 1); + + TryTakePendingRemoval(item.RuntimeID.Hash, out _); + } + + private static void RememberDroppedItemInstance( + long runtimeIdHash, + GameObject instance, + uint sourceBagNetworkId, + NetworkRuntimeItem item, + Vector3 position) + { + if (runtimeIdHash == 0 || instance == null) return; + + if (s_DroppedItemInstances.TryGetValue(runtimeIdHash, out DroppedItemInstance previous) && + previous.Instance != null && + previous.Instance != instance) + { + LogPickupDebug( + $"replacing tracked dropped instance runtime={runtimeIdHash} old={previous.Instance.name} new={instance.name} sourceBag={sourceBagNetworkId}"); + UnityEngine.Object.Destroy(previous.Instance); + } + + s_DroppedItemInstances[runtimeIdHash] = new DroppedItemInstance + { + Instance = instance, + SourceBagNetworkId = sourceBagNetworkId, + Item = item, + Position = position + }; + + LogPickupDebug( + $"remembered dropped instance runtime={runtimeIdHash} sourceBag={sourceBagNetworkId} item={DescribeNetworkItem(item)} instance={instance.name} position={position} trackedDrops={s_DroppedItemInstances.Count}"); + } + + private static bool TryAdoptPredictedDroppedItemInstance(NetworkItemDroppedBroadcast broadcast) + { + long serverRuntimeIdHash = broadcast.Item.RuntimeIdHash; + if (serverRuntimeIdHash != 0 && s_LocalDropRuntimeIds.Remove(serverRuntimeIdHash)) + { + if (s_DroppedItemInstances.TryGetValue(serverRuntimeIdHash, out DroppedItemInstance exactDrop) && + exactDrop.Instance != null) + { + s_DroppedItemInstances[serverRuntimeIdHash] = new DroppedItemInstance + { + Instance = exactDrop.Instance, + SourceBagNetworkId = broadcast.SourceBagNetworkId, + Item = broadcast.Item, + Position = broadcast.Position + }; + } + + LogPickupDebug( + $"adopted server dropped broadcast for exact local predicted drop runtime={serverRuntimeIdHash} sourceBag={broadcast.SourceBagNetworkId} item={DescribeNetworkItem(broadcast.Item)}"); + return true; + } + + if (serverRuntimeIdHash == 0 || s_LocalDropRuntimeIds.Count == 0) + { + return false; + } + + long bestLocalRuntimeIdHash = 0; + DroppedItemInstance bestDrop = default; + float bestDistance = float.MaxValue; + + foreach (KeyValuePair entry in s_DroppedItemInstances) + { + if (!s_LocalDropRuntimeIds.Contains(entry.Key)) continue; + + DroppedItemInstance candidate = entry.Value; + if (candidate.Item.ItemHash != broadcast.Item.ItemHash) continue; + if (broadcast.SourceBagNetworkId != 0 && + candidate.SourceBagNetworkId != 0 && + candidate.SourceBagNetworkId != broadcast.SourceBagNetworkId) + { + continue; + } + + Vector3 candidatePosition = candidate.Instance != null + ? candidate.Instance.transform.position + : candidate.Position; + float distance = Vector3.SqrMagnitude(candidatePosition - broadcast.Position); + if (distance >= bestDistance) continue; + + bestDistance = distance; + bestLocalRuntimeIdHash = entry.Key; + bestDrop = candidate; + } + + if (bestLocalRuntimeIdHash == 0 || bestDrop.Instance == null || bestDistance > 16f) + { + return false; + } + + s_LocalDropRuntimeIds.Remove(bestLocalRuntimeIdHash); + s_DroppedItemInstances.Remove(bestLocalRuntimeIdHash); + RememberDroppedItemInstance( + serverRuntimeIdHash, + bestDrop.Instance, + broadcast.SourceBagNetworkId, + broadcast.Item, + broadcast.Position); + + LogPickupDebug( + $"adopted server dropped broadcast by remapping local predicted drop localRuntime={bestLocalRuntimeIdHash} serverRuntime={serverRuntimeIdHash} sourceBag={broadcast.SourceBagNetworkId} distance={Mathf.Sqrt(bestDistance):0.00} item={DescribeNetworkItem(broadcast.Item)}", + bestDrop.Instance); + return true; + } + + private static bool TryGetDroppedItemInstance(long runtimeIdHash, out DroppedItemInstance droppedItem) + { + if (runtimeIdHash != 0 && s_DroppedItemInstances.TryGetValue(runtimeIdHash, out droppedItem)) + { + return true; + } + + droppedItem = default; + return false; + } + + private bool TryFindDroppedItemInstanceForLocalPickup(RuntimeItem localItem, out DroppedItemInstance droppedItem) + { + droppedItem = default; + if (localItem?.Item == null || s_DroppedItemInstances.Count == 0) return false; + + int itemHash = localItem.ItemID.Hash; + Vector3 pickerPosition = transform.position; + float bestDistance = float.MaxValue; + bool found = false; + + foreach (var entry in s_DroppedItemInstances) + { + DroppedItemInstance candidate = entry.Value; + if (candidate.Item.ItemHash != itemHash) continue; + + Vector3 candidatePosition = candidate.Instance != null + ? candidate.Instance.transform.position + : candidate.Position; + + float distance = Vector3.SqrMagnitude(candidatePosition - pickerPosition); + if (distance >= bestDistance) continue; + + bestDistance = distance; + droppedItem = candidate; + found = true; + } + + if (found) + { + LogPickupDebug( + $"{name}: matched local pickup by item type localItem={DescribeRuntimeItem(localItem)} serverItem={DescribeNetworkItem(droppedItem.Item)} sourceBag={droppedItem.SourceBagNetworkId} distance={Mathf.Sqrt(bestDistance):0.00}", + this); + } + + return found; + } + + private static bool TryDestroyDroppedItemInstance(long runtimeIdHash) + { + if (runtimeIdHash == 0) return false; + if (!s_DroppedItemInstances.TryGetValue(runtimeIdHash, out DroppedItemInstance droppedItem)) return false; + + s_DroppedItemInstances.Remove(runtimeIdHash); + s_LocalDropRuntimeIds.Remove(runtimeIdHash); + GameObject instance = droppedItem.Instance; + if (instance == null) return false; + + LogPickupDebug( + $"destroying tracked dropped instance runtime={runtimeIdHash} sourceBag={droppedItem.SourceBagNetworkId} instance={instance.name} remainingTrackedDrops={s_DroppedItemInstances.Count}", + instance); + UnityEngine.Object.Destroy(instance); + return true; + } + + private static bool TryDestroyDroppedItemInstance(NetworkDroppedItemRemovedBroadcast broadcast, out long destroyedRuntimeIdHash) + { + destroyedRuntimeIdHash = broadcast.RuntimeIdHash; + if (TryDestroyDroppedItemInstance(broadcast.RuntimeIdHash)) + { + return true; + } + + destroyedRuntimeIdHash = 0; + if (s_DroppedItemInstances.Count == 0) + { + return false; + } + + long bestRuntimeIdHash = 0; + DroppedItemInstance bestDrop = default; + float bestDistance = float.MaxValue; + bool foundSameSource = false; + + foreach (KeyValuePair entry in s_DroppedItemInstances) + { + DroppedItemInstance candidate = entry.Value; + bool sameSource = broadcast.SourceBagNetworkId == 0 || + candidate.SourceBagNetworkId == 0 || + candidate.SourceBagNetworkId == broadcast.SourceBagNetworkId; + + if (foundSameSource && !sameSource) continue; + if (!foundSameSource && sameSource) + { + foundSameSource = true; + bestDistance = float.MaxValue; + bestRuntimeIdHash = 0; + bestDrop = default; + } + + Vector3 candidatePosition = candidate.Instance != null + ? candidate.Instance.transform.position + : candidate.Position; + float distance = Vector3.SqrMagnitude(candidatePosition - broadcast.Position); + if (distance >= bestDistance) continue; + + bestDistance = distance; + bestRuntimeIdHash = entry.Key; + bestDrop = candidate; + } + + float maxDistance = foundSameSource ? 16f : 2.25f; + if (bestRuntimeIdHash == 0 || bestDrop.Instance == null || bestDistance > maxDistance) + { + return false; + } + + destroyedRuntimeIdHash = bestRuntimeIdHash; + return TryDestroyDroppedItemInstance(bestRuntimeIdHash); + } + + private static void HandleGlobalSocketAttached(RuntimeItem parent, RuntimeItem attachment) + { + if (parent == null || attachment == null) return; + NetworkInventoryController controller = FindControllerOwningRuntimeItem(parent.RuntimeID.Hash); + if (controller == null || controller.m_IsApplyingNetworkState) return; + if (!TryFindAttachedSocketId(parent, attachment, out IdString socketId)) socketId = IdString.EMPTY; + + if (controller.m_IsServer) + { + controller.BroadcastServerSocketAttach(parent, attachment, socketId); + return; + } + + if (!controller.m_IsLocalClient) return; + controller.RequestAttachToSocket(parent, attachment, socketId); + } + + private static void HandleGlobalSocketDetached(RuntimeItem parent, RuntimeItem attachment) + { + if (parent == null || attachment == null) return; + NetworkInventoryController controller = FindControllerOwningRuntimeItem(parent.RuntimeID.Hash); + if (controller == null || controller.m_IsApplyingNetworkState) return; + + if (controller.m_IsServer) + { + controller.BroadcastServerSocketDetach(parent); + return; + } + + if (!controller.m_IsLocalClient) return; + + controller.RequestDetachFromSocket(parent, attachment); + } + + private static NetworkInventoryController FindControllerOwningRuntimeItem(long runtimeIdHash) + { + for (int i = 0; i < s_Controllers.Count; i++) + { + NetworkInventoryController controller = s_Controllers[i]; + if (controller == null) continue; + if (controller.ContainsRuntimeItemRecursive(runtimeIdHash)) return controller; + } + + return null; + } + + private static bool TryFindAttachedSocketId(RuntimeItem parent, RuntimeItem attachment, out IdString socketId) + { + socketId = IdString.EMPTY; + if (parent == null || attachment == null) return false; + + foreach (var socketEntry in parent.Sockets) + { + RuntimeSocket socket = socketEntry.Value; + if (socket == null || !socket.HasAttachment) continue; + if (socket.Attachment.RuntimeID.Hash != attachment.RuntimeID.Hash) continue; + + socketId = socketEntry.Key; + return true; + } + + return false; + } + } +} +#endif diff --git a/NetworkInventoryController.Server.SyncAndHelpers.cs b/NetworkInventoryController.Server.SyncAndHelpers.cs new file mode 100644 index 0000000..a851060 --- /dev/null +++ b/NetworkInventoryController.Server.SyncAndHelpers.cs @@ -0,0 +1,924 @@ +#if GC2_INVENTORY +using System; +using System.Collections.Generic; +using UnityEngine; +using GameCreator.Runtime.Common; +using GameCreator.Runtime.Inventory; + +namespace Arawn.GameCreator2.Networking.Inventory +{ + // ════════════════════════════════════════════════════════════════════════════════════════════ + // SERVER-SIDE — Sync, snapshot, and runtime-item helper methods + // ════════════════════════════════════════════════════════════════════════════════════════════ + + public partial class NetworkInventoryController + { + // SERVER BROADCASTING + // ════════════════════════════════════════════════════════════════════════════════════════ + + private void BroadcastFullState() + { + var snapshot = GetFullSnapshot(); + NetworkInventoryManager.Instance?.BroadcastFullSnapshot(snapshot); + } + + private void BroadcastDeltaState() + { + bool cellsChanged = HasInventoryPositionStateChanged(); + bool equipmentChanged = HasEquipmentStateChanged(); + bool wealthChanged = HasWealthStateChanged(); + + if (!cellsChanged && !equipmentChanged && !wealthChanged) + { + return; + } + + const uint maskCells = 1u << 0; + const uint maskEquipment = 1u << 1; + const uint maskWealth = 1u << 2; + + var delta = new NetworkInventoryDelta + { + BagNetworkId = NetworkId, + Timestamp = Time.time, + ChangeMask = (cellsChanged ? maskCells : 0u) | + (equipmentChanged ? maskEquipment : 0u) | + (wealthChanged ? maskWealth : 0u), + ChangedCells = cellsChanged ? BuildChangedCellDelta() : Array.Empty(), + ChangedEquipment = equipmentChanged ? BuildChangedEquipmentDelta() : Array.Empty(), + ChangedWealth = wealthChanged ? BuildChangedWealthDelta() : Array.Empty() + }; + + NetworkInventoryManager.Instance?.BroadcastDelta(delta); + CacheCurrentSyncState(); + + if (m_LogAllChanges) + { + Debug.Log( + $"[NetworkInventoryController] Broadcasted delta update (mask={delta.ChangeMask}) " + + $"cells={delta.ChangedCells.Length} equipment={delta.ChangedEquipment.Length} wealth={delta.ChangedWealth.Length}"); + } + } + + /// + /// Get full inventory snapshot for initial sync. + /// + public NetworkInventorySnapshot GetFullSnapshot() + { + var cells = new List(); + var equipment = new List(); + var wealth = new List(); + + // Collect cells + foreach (var cell in m_Bag.Content.CellList) + { + if (cell == null || cell.Available) continue; + + var position = m_Bag.Content.FindPosition(cell.RootRuntimeItemID); + GetStackedRuntimeIdentity(cell, out long[] stackedRuntimeIds, out string[] stackedRuntimeIdStrings); + + cells.Add(new NetworkCell + { + Position = position, + ItemHash = cell.Item.ID.Hash, + StackCount = cell.Count, + RootItem = ConvertToNetworkItem(cell.RootRuntimeItem), + StackedRuntimeIds = stackedRuntimeIds, + StackedRuntimeIdStrings = stackedRuntimeIdStrings + }); + } + + // Collect equipment + for (int i = 0; i < m_Bag.Equipment.Count; i++) + { + var slotId = m_Bag.Equipment.GetSlotRootRuntimeItemID(i); + var baseId = m_Bag.Equipment.GetSlotBaseID(i); + + equipment.Add(new NetworkEquipmentSlot + { + SlotIndex = i, + BaseItemHash = baseId.Hash, + IsOccupied = !string.IsNullOrEmpty(slotId.String), + EquippedRuntimeIdHash = slotId.Hash + }); + } + + // Collect wealth + foreach (var currencyId in m_Bag.Wealth.List) + { + wealth.Add(new NetworkWealthEntry + { + CurrencyHash = currencyId.Hash, + Amount = m_Bag.Wealth.Get(currencyId) + }); + } + + return new NetworkInventorySnapshot + { + BagNetworkId = NetworkId, + Timestamp = Time.time, + Cells = cells.ToArray(), + Equipment = equipment.ToArray(), + Wealth = wealth.ToArray() + }; + } + + // ════════════════════════════════════════════════════════════════════════════════════════ + // HELPER METHODS + // ════════════════════════════════════════════════════════════════════════════════════════ + + private void CleanupPendingRequests() + { + float timeout = 5f; + float currentTime = Time.time; + + CleanupPendingBucket(m_PendingAdds, currentTime, timeout, "Add item"); + CleanupPendingBucket(m_PendingRemoves, currentTime, timeout, "Remove item"); + CleanupPendingBucket(m_PendingMoves, currentTime, timeout, "Move item"); + CleanupPendingBucket(m_PendingEquipment, currentTime, timeout, "Equipment operation"); + CleanupPendingBucket(m_PendingWealth, currentTime, timeout, "Wealth operation"); + + void CleanupPendingBucket(Dictionary pending, float now, float timeoutSeconds, string operationName) + where T : struct, ITimedPendingRequest + { + int removedCount = PendingRequestCleanup.RemoveTimedOut( + pending, + s_SharedKeyBuffer, + now, + timeoutSeconds); + + if (removedCount <= 0) return; + + if (m_LogRejections) + { + Debug.LogWarning($"[NetworkInventoryController] {operationName} timed out ({removedCount} pending request(s) dropped)."); + } + + if (!m_IsServer) + { + OnOperationRejected?.Invoke(InventoryRejectionReason.RequestTimeout, operationName); + } + } + } + + private bool HasInventoryPositionStateChanged() + { + Dictionary current = BuildCurrentPositionState(); + return !DictionariesEqual(m_LastSyncedPositions, current); + } + + private bool HasEquipmentStateChanged() + { + var current = new Dictionary(Mathf.Max(1, m_Bag.Equipment.Count)); + for (int i = 0; i < m_Bag.Equipment.Count; i++) + { + current[i] = m_Bag.Equipment.GetSlotRootRuntimeItemID(i).Hash; + } + + return !DictionariesEqual(m_LastSyncedEquipment, current); + } + + private bool HasWealthStateChanged() + { + var current = new Dictionary(8); + foreach (IdString currencyId in m_Bag.Wealth.List) + { + current[currencyId.Hash] = m_Bag.Wealth.Get(currencyId); + } + + return !DictionariesEqual(m_LastSyncedWealth, current); + } + + private Dictionary BuildCurrentPositionState() + { + var current = new Dictionary(m_RuntimeItemMap.Count); + foreach (Cell cell in m_Bag.Content.CellList) + { + if (cell == null || cell.Available) continue; + + Vector2Int position = m_Bag.Content.FindPosition(cell.RootRuntimeItemID); + foreach (IdString runtimeId in cell.List) + { + current[runtimeId.Hash] = position; + } + } + + return current; + } + + private NetworkCell[] BuildChangedCellDelta() + { + Dictionary currentPositions = BuildCurrentPositionState(); + var changedPositions = new HashSet(); + + foreach (KeyValuePair entry in currentPositions) + { + if (!m_LastSyncedPositions.TryGetValue(entry.Key, out Vector2Int previousPosition) || + previousPosition != entry.Value) + { + changedPositions.Add(entry.Value); + } + } + + foreach (KeyValuePair entry in m_LastSyncedPositions) + { + if (!currentPositions.ContainsKey(entry.Key)) + { + changedPositions.Add(entry.Value); + } + } + + if (changedPositions.Count == 0) return Array.Empty(); + + var orderedPositions = new List(changedPositions); + orderedPositions.Sort((left, right) => + { + int x = left.x.CompareTo(right.x); + return x != 0 ? x : left.y.CompareTo(right.y); + }); + + var changedCells = new List(orderedPositions.Count); + foreach (Vector2Int position in orderedPositions) + { + Cell cell = m_Bag.Content.GetContent(position); + if (cell == null || cell.Available) + { + changedCells.Add(new NetworkCell + { + Position = position, + ItemHash = 0, + StackCount = 0, + RootItem = default, + StackedRuntimeIds = Array.Empty(), + StackedRuntimeIdStrings = Array.Empty() + }); + continue; + } + + GetStackedRuntimeIdentity(cell, out long[] stackedRuntimeIds, out string[] stackedRuntimeIdStrings); + changedCells.Add(new NetworkCell + { + Position = position, + ItemHash = cell.Item.ID.Hash, + StackCount = cell.Count, + RootItem = ConvertToNetworkItem(cell.RootRuntimeItem), + StackedRuntimeIds = stackedRuntimeIds, + StackedRuntimeIdStrings = stackedRuntimeIdStrings + }); + } + + return changedCells.ToArray(); + } + + private NetworkEquipmentSlot[] BuildChangedEquipmentDelta() + { + var changedSlots = new List(Mathf.Max(1, m_Bag.Equipment.Count)); + for (int i = 0; i < m_Bag.Equipment.Count; i++) + { + IdString slotRuntimeId = m_Bag.Equipment.GetSlotRootRuntimeItemID(i); + long currentRuntimeHash = slotRuntimeId.Hash; + if (m_LastSyncedEquipment.TryGetValue(i, out long previousRuntimeHash) && + previousRuntimeHash == currentRuntimeHash) + { + continue; + } + + changedSlots.Add(new NetworkEquipmentSlot + { + SlotIndex = i, + BaseItemHash = m_Bag.Equipment.GetSlotBaseID(i).Hash, + IsOccupied = !string.IsNullOrEmpty(slotRuntimeId.String), + EquippedRuntimeIdHash = currentRuntimeHash + }); + } + + return changedSlots.ToArray(); + } + + private NetworkWealthEntry[] BuildChangedWealthDelta() + { + var changedEntries = new List(m_Bag.Wealth.List.Count); + var seenCurrencyHashes = new HashSet(); + + foreach (IdString currencyId in m_Bag.Wealth.List) + { + int hash = currencyId.Hash; + int amount = m_Bag.Wealth.Get(currencyId); + seenCurrencyHashes.Add(hash); + + if (m_LastSyncedWealth.TryGetValue(hash, out int previousAmount) && + previousAmount == amount) + { + continue; + } + + changedEntries.Add(new NetworkWealthEntry + { + CurrencyHash = hash, + Amount = amount + }); + } + + foreach (KeyValuePair entry in m_LastSyncedWealth) + { + if (seenCurrencyHashes.Contains(entry.Key)) continue; + + changedEntries.Add(new NetworkWealthEntry + { + CurrencyHash = entry.Key, + Amount = 0 + }); + } + + return changedEntries.ToArray(); + } + + private void CacheCurrentSyncState() + { + Dictionary currentPositions = BuildCurrentPositionState(); + m_LastSyncedPositions.Clear(); + foreach (KeyValuePair entry in currentPositions) + { + m_LastSyncedPositions[entry.Key] = entry.Value; + } + + m_LastSyncedEquipment.Clear(); + for (int i = 0; i < m_Bag.Equipment.Count; i++) + { + m_LastSyncedEquipment[i] = m_Bag.Equipment.GetSlotRootRuntimeItemID(i).Hash; + } + + m_LastSyncedWealth.Clear(); + foreach (IdString currencyId in m_Bag.Wealth.List) + { + m_LastSyncedWealth[currencyId.Hash] = m_Bag.Wealth.Get(currencyId); + } + } + + private static bool DictionariesEqual( + Dictionary left, + Dictionary right) + { + if (ReferenceEquals(left, right)) return true; + if (left == null || right == null) return false; + if (left.Count != right.Count) return false; + + var comparer = EqualityComparer.Default; + foreach (var entry in left) + { + if (!right.TryGetValue(entry.Key, out TValue value)) return false; + if (!comparer.Equals(entry.Value, value)) return false; + } + + return true; + } + + private bool TryResolveItem(int itemHash, string itemIdString, out Item item) + { + item = null; + InventoryRepository inventory = Settings.From(); + if (inventory == null) return false; + + if (string.IsNullOrWhiteSpace(itemIdString)) + { + return false; + } + + var itemId = new IdString(itemIdString); + if (itemId.Hash != itemHash) return false; + + item = inventory.Items.Get(itemId); + return item != null && item.ID.Hash == itemHash; + } + + private bool TryResolveCurrencyId(int currencyHash, string currencyIdString, out IdString currencyId) + { + currencyId = IdString.EMPTY; + if (string.IsNullOrWhiteSpace(currencyIdString)) return false; + + currencyId = new IdString(currencyIdString); + if (currencyId.Hash != currencyHash) return false; + + foreach (IdString entry in m_Bag.Wealth.List) + { + if (entry.Hash == currencyHash && entry == currencyId) + { + return true; + } + } + + return false; + } + + private bool TryResolveCurrencyIdByHash(int currencyHash, out IdString currencyId) + { + currencyId = IdString.EMPTY; + foreach (IdString entry in m_Bag.Wealth.List) + { + if (entry.Hash == currencyHash) + { + currencyId = entry; + return true; + } + } + + return false; + } + + private static bool TryResolveSocketId(RuntimeItem parentItem, int socketHash, string socketIdString, out IdString socketId) + { + socketId = IdString.EMPTY; + if (parentItem == null || parentItem.Item == null) return false; + if (string.IsNullOrWhiteSpace(socketIdString)) return false; + + socketId = new IdString(socketIdString); + if (socketId.Hash != socketHash) return false; + + var sockets = Sockets.FlattenHierarchy(parentItem.Item); + return sockets != null && sockets.ContainsKey(socketId); + } + + private NetworkRuntimeItem ConvertToNetworkItem(RuntimeItem runtimeItem) + { + if (runtimeItem == null) return default; + + var properties = new List(); + foreach (var prop in runtimeItem.Properties) + { + properties.Add(new NetworkRuntimeProperty + { + PropertyHash = prop.Key.Hash, + PropertyIdString = prop.Key.String, + Number = prop.Value.Number, + Text = prop.Value.Text + }); + } + + var sockets = new List(); + foreach (var socket in runtimeItem.Sockets) + { + sockets.Add(new NetworkRuntimeSocket + { + SocketHash = socket.Key.Hash, + SocketIdString = socket.Key.String, + HasAttachment = socket.Value.HasAttachment, + Attachment = socket.Value.HasAttachment ? ConvertToNetworkItem(socket.Value.Attachment) : default + }); + } + + return new NetworkRuntimeItem + { + ItemHash = runtimeItem.ItemID.Hash, + ItemIdString = runtimeItem.ItemID.String, + RuntimeIdHash = runtimeItem.RuntimeID.Hash, + RuntimeIdString = runtimeItem.RuntimeID.String, + Properties = properties.ToArray(), + Sockets = sockets.ToArray() + }; + } + + private RuntimeItem ReconstructRuntimeItem(NetworkRuntimeItem networkItem) + { + if (networkItem.ItemHash == 0) return null; + + if (!TryResolveItem(networkItem.ItemHash, networkItem.ItemIdString, out Item item)) + { + return null; + } + + var runtimeItem = new RuntimeItem(item); + TryApplyRuntimeId(runtimeItem, networkItem.RuntimeIdString, networkItem.RuntimeIdHash); + + if (networkItem.Properties != null) + { + foreach (NetworkRuntimeProperty property in networkItem.Properties) + { + if (!TryResolveRuntimePropertyId(runtimeItem, property.PropertyHash, property.PropertyIdString, out IdString propertyId)) + { + continue; + } + + if (!runtimeItem.Properties.TryGetValue(propertyId, out RuntimeProperty runtimeProperty)) + { + continue; + } + + runtimeProperty.Number = property.Number; + runtimeProperty.Text = property.Text; + } + } + + if (networkItem.Sockets != null && s_RuntimeSocketAttachmentField != null) + { + foreach (NetworkRuntimeSocket socket in networkItem.Sockets) + { + if (!TryResolveRuntimeSocketId(runtimeItem, socket.SocketHash, socket.SocketIdString, out IdString socketId) || + !runtimeItem.Sockets.TryGetValue(socketId, out RuntimeSocket runtimeSocket)) + { + continue; + } + + if (!socket.HasAttachment) + { + s_RuntimeSocketAttachmentField.SetValue(runtimeSocket, null); + continue; + } + + RuntimeItem attachment = ReconstructRuntimeItem(socket.Attachment); + if (attachment != null) + { + s_RuntimeSocketAttachmentField.SetValue(runtimeSocket, attachment); + } + } + } + + return runtimeItem; + } + + private void ApplyCellDelta(NetworkCell[] changedCells) + { + if (changedCells == null) return; + + foreach (NetworkCell cell in changedCells) + { + ClearCellAtPosition(cell.Position); + + bool isDeleteEntry = cell.ItemHash == 0 || cell.StackCount <= 0 || cell.RootItem.ItemHash == 0; + if (isDeleteEntry) + { + continue; + } + + RuntimeItem rootItem = ReconstructRuntimeItem(cell.RootItem); + if (rootItem == null) + { + continue; + } + + bool addedRoot = m_Bag.Content.Add(rootItem, cell.Position, true); + if (!addedRoot) + { + continue; + } + + TrackRuntimeItemRecursive(rootItem); + + int stackCount = Mathf.Max(1, cell.StackCount); + long[] stackedRuntimeIds = cell.StackedRuntimeIds; + string[] stackedRuntimeIdStrings = cell.StackedRuntimeIdStrings; + for (int i = 1; i < stackCount; i++) + { + RuntimeItem stackedItem = new RuntimeItem(rootItem, true); + int stackedIndex = i - 1; + if (stackedRuntimeIds != null && stackedIndex < stackedRuntimeIds.Length) + { + string runtimeIdString = stackedRuntimeIdStrings != null && stackedIndex < stackedRuntimeIdStrings.Length + ? stackedRuntimeIdStrings[stackedIndex] + : null; + TryApplyRuntimeId(stackedItem, runtimeIdString, stackedRuntimeIds[stackedIndex]); + } + + if (m_Bag.Content.Add(stackedItem, cell.Position, true)) + { + TrackRuntimeItemRecursive(stackedItem); + } + } + } + } + + private void ApplyEquipmentDelta(NetworkEquipmentSlot[] changedEquipment) + { + if (changedEquipment == null) return; + + foreach (NetworkEquipmentSlot slot in changedEquipment) + { + if (slot.SlotIndex < 0 || slot.SlotIndex >= m_Bag.Equipment.Count) + { + continue; + } + + _ = m_Bag.Equipment.UnequipFromIndex(slot.SlotIndex); + if (!slot.IsOccupied) + { + continue; + } + + if (m_RuntimeItemMap.TryGetValue(slot.EquippedRuntimeIdHash, out RuntimeItem runtimeItem)) + { + _ = m_Bag.Equipment.EquipToIndex(runtimeItem, slot.SlotIndex); + } + } + } + + private void ApplyWealthDelta(NetworkWealthEntry[] changedWealth) + { + if (changedWealth == null) return; + + foreach (NetworkWealthEntry wealthEntry in changedWealth) + { + if (TryResolveCurrencyIdByHash(wealthEntry.CurrencyHash, out IdString currencyId)) + { + m_Bag.Wealth.Set(currencyId, wealthEntry.Amount); + } + } + } + + private void ClearCellAtPosition(Vector2Int position) + { + int safety = 0; + while (safety++ < 256) + { + RuntimeItem removed = m_Bag.Content.Remove(position); + if (removed == null) + { + break; + } + + UntrackRuntimeItemRecursive(removed); + } + } + + private void ApplyFullSnapshot(NetworkInventorySnapshot snapshot) + { + // [LOCAL-EDIT] #PILFER-INVENTORY-SNAPSHOT-DEBUG + int localItemsBefore = m_Bag.Content.CountWithStack; + int trackedItemsBefore = m_RuntimeItemMap.Count; + int snapshotCells = snapshot.Cells?.Length ?? 0; + int snapshotStackItems = CountSnapshotStackItems(snapshot.Cells); + + LogPickupDebug( + $"{name}: applying full snapshot bag={NetworkId} snapshotBag={snapshot.BagNetworkId} world={IsWorldInventory} server={m_IsServer} local={m_IsLocalClient} remote={m_IsRemoteClient} localItemsBefore={localItemsBefore} trackedBefore={trackedItemsBefore} snapshotCells={snapshotCells} snapshotStackItems={snapshotStackItems} firstCells={DescribeSnapshotCells(snapshot.Cells, 5)}", + this); + + ClearCurrentInventoryState(); + + if (snapshot.Cells != null) + { + foreach (NetworkCell cell in snapshot.Cells) + { + RuntimeItem rootItem = ReconstructRuntimeItem(cell.RootItem); + if (rootItem == null) continue; + + bool addedRoot = m_Bag.Content.Add(rootItem, cell.Position, true); + if (!addedRoot) + { + continue; + } + + TrackRuntimeItemRecursive(rootItem); + + int stackCount = Mathf.Max(1, cell.StackCount); + long[] stackedRuntimeIds = cell.StackedRuntimeIds; + string[] stackedRuntimeIdStrings = cell.StackedRuntimeIdStrings; + for (int i = 1; i < stackCount; i++) + { + RuntimeItem stackedItem = new RuntimeItem(rootItem, true); + int stackedIndex = i - 1; + if (stackedRuntimeIds != null && stackedIndex < stackedRuntimeIds.Length) + { + string runtimeIdString = stackedRuntimeIdStrings != null && stackedIndex < stackedRuntimeIdStrings.Length + ? stackedRuntimeIdStrings[stackedIndex] + : null; + TryApplyRuntimeId(stackedItem, runtimeIdString, stackedRuntimeIds[stackedIndex]); + } + + if (m_Bag.Content.Add(stackedItem, cell.Position, true)) + { + TrackRuntimeItemRecursive(stackedItem); + } + } + } + } + + for (int i = 0; i < m_Bag.Equipment.Count; i++) + { + _ = m_Bag.Equipment.UnequipFromIndex(i); + } + + if (snapshot.Equipment != null) + { + foreach (NetworkEquipmentSlot slot in snapshot.Equipment) + { + if (!slot.IsOccupied) continue; + if (!m_RuntimeItemMap.TryGetValue(slot.EquippedRuntimeIdHash, out RuntimeItem runtimeItem)) continue; + _ = m_Bag.Equipment.EquipToIndex(runtimeItem, slot.SlotIndex); + } + } + + foreach (IdString currencyId in m_Bag.Wealth.List) + { + m_Bag.Wealth.Set(currencyId, 0); + } + + if (snapshot.Wealth != null) + { + foreach (NetworkWealthEntry wealthEntry in snapshot.Wealth) + { + if (TryResolveCurrencyIdByHash(wealthEntry.CurrencyHash, out IdString currencyId)) + { + m_Bag.Wealth.Set(currencyId, wealthEntry.Amount); + } + } + } + + CacheCurrentSyncState(); + + // [LOCAL-EDIT] #PILFER-INVENTORY-SNAPSHOT-DEBUG + LogPickupDebug( + $"{name}: applied full snapshot bag={NetworkId} localItemsAfter={m_Bag.Content.CountWithStack} trackedAfter={m_RuntimeItemMap.Count} snapshotCells={snapshotCells} snapshotStackItems={snapshotStackItems}", + this); + } + + // [LOCAL-EDIT] #PILFER-INVENTORY-SNAPSHOT-DEBUG + private static int CountSnapshotStackItems(NetworkCell[] cells) + { + if (cells == null) return 0; + + int count = 0; + for (int i = 0; i < cells.Length; i++) + { + count += Mathf.Max(0, cells[i].StackCount); + } + + return count; + } + + // [LOCAL-EDIT] #PILFER-INVENTORY-SNAPSHOT-DEBUG + private static string DescribeSnapshotCells(NetworkCell[] cells, int maxCells) + { + if (cells == null || cells.Length == 0) return "none"; + + int limit = Mathf.Min(cells.Length, Mathf.Max(0, maxCells)); + string description = string.Empty; + for (int i = 0; i < limit; i++) + { + NetworkCell cell = cells[i]; + if (i > 0) description += "; "; + description += $"#{i} pos={cell.Position} item={cell.RootItem.ItemIdString} runtime={cell.RootItem.RuntimeIdHash} stack={cell.StackCount}"; + } + + if (cells.Length > limit) + { + description += $"; +{cells.Length - limit} more"; + } + + return description; + } + + private void ClearCurrentInventoryState() + { + int safety = 0; + while (safety++ < 4096) + { + RuntimeItem itemToRemove = null; + foreach (Cell cell in m_Bag.Content.CellList) + { + if (cell == null || cell.Available) continue; + itemToRemove = cell.Peek(); + if (itemToRemove != null) break; + } + + if (itemToRemove == null) break; + m_Bag.Content.Remove(itemToRemove); + } + + m_RuntimeItemMap.Clear(); + } + + private void TrackRuntimeItemRecursive(RuntimeItem runtimeItem) + { + if (runtimeItem == null) return; + + m_RuntimeItemMap[runtimeItem.RuntimeID.Hash] = runtimeItem; + foreach (KeyValuePair socketEntry in runtimeItem.Sockets) + { + RuntimeSocket socket = socketEntry.Value; + if (socket == null || !socket.HasAttachment) continue; + TrackRuntimeItemRecursive(socket.Attachment); + } + } + + private void UntrackRuntimeItemRecursive(RuntimeItem runtimeItem) + { + if (runtimeItem == null) return; + + m_RuntimeItemMap.Remove(runtimeItem.RuntimeID.Hash); + foreach (KeyValuePair socketEntry in runtimeItem.Sockets) + { + RuntimeSocket socket = socketEntry.Value; + if (socket == null || !socket.HasAttachment) continue; + UntrackRuntimeItemRecursive(socket.Attachment); + } + } + + private bool ContainsRuntimeItemRecursive(long runtimeIdHash) + { + foreach (Cell cell in m_Bag.Content.CellList) + { + if (cell == null || cell.Available) continue; + + RuntimeItem rootItem = cell.RootRuntimeItem; + if (ContainsRuntimeItemRecursive(rootItem, runtimeIdHash)) return true; + + foreach (IdString stackedId in cell.List) + { + RuntimeItem stackedItem = m_Bag.Content.GetRuntimeItem(stackedId); + if (ContainsRuntimeItemRecursive(stackedItem, runtimeIdHash)) return true; + } + } + + return false; + } + + private static bool ContainsRuntimeItemRecursive(RuntimeItem runtimeItem, long runtimeIdHash) + { + if (runtimeItem == null) return false; + if (runtimeItem.RuntimeID.Hash == runtimeIdHash) return true; + + foreach (KeyValuePair socketEntry in runtimeItem.Sockets) + { + RuntimeSocket socket = socketEntry.Value; + if (socket == null || !socket.HasAttachment) continue; + if (ContainsRuntimeItemRecursive(socket.Attachment, runtimeIdHash)) return true; + } + + return false; + } + + private static void TryApplyRuntimeId(RuntimeItem runtimeItem, string runtimeIdString, long runtimeIdHash) + { + if (runtimeItem == null || s_RuntimeItemIdField == null) return; + if (string.IsNullOrWhiteSpace(runtimeIdString)) return; + + IdString runtimeId = new IdString(runtimeIdString); + if (runtimeIdHash != 0 && runtimeId.Hash != runtimeIdHash) return; + s_RuntimeItemIdField.SetValue(runtimeItem, runtimeId); + } + + private static bool TryResolveRuntimePropertyId(RuntimeItem runtimeItem, int propertyHash, string propertyIdString, out IdString propertyId) + { + propertyId = IdString.EMPTY; + if (runtimeItem == null) return false; + + if (!string.IsNullOrWhiteSpace(propertyIdString)) + { + IdString candidate = new IdString(propertyIdString); + if (candidate.Hash == propertyHash && runtimeItem.Properties.ContainsKey(candidate)) + { + propertyId = candidate; + return true; + } + } + + foreach (KeyValuePair entry in runtimeItem.Properties) + { + if (entry.Key.Hash != propertyHash) continue; + propertyId = entry.Key; + return true; + } + + return false; + } + + private static bool TryResolveRuntimeSocketId(RuntimeItem runtimeItem, int socketHash, string socketIdString, out IdString socketId) + { + socketId = IdString.EMPTY; + if (runtimeItem == null) return false; + + if (!string.IsNullOrWhiteSpace(socketIdString)) + { + IdString candidate = new IdString(socketIdString); + if (candidate.Hash == socketHash && runtimeItem.Sockets.ContainsKey(candidate)) + { + socketId = candidate; + return true; + } + } + + foreach (KeyValuePair entry in runtimeItem.Sockets) + { + if (entry.Key.Hash != socketHash) continue; + socketId = entry.Key; + return true; + } + + return false; + } + + private static void GetStackedRuntimeIdentity(Cell cell, out long[] runtimeIds, out string[] runtimeIdStrings) + { + var ids = new List(); + var idStrings = new List(); + foreach (var id in cell.List) + { + if (id.Hash == cell.RootRuntimeItemID.Hash) continue; + ids.Add(id.Hash); + idStrings.Add(id.String); + } + + runtimeIds = ids.ToArray(); + runtimeIdStrings = idStrings.ToArray(); + } + } +} +#endif diff --git a/NetworkInventoryController.Server.cs b/NetworkInventoryController.Server.cs new file mode 100644 index 0000000..fca1435 --- /dev/null +++ b/NetworkInventoryController.Server.cs @@ -0,0 +1,1766 @@ +#if GC2_INVENTORY +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; +using UnityEngine; +using GameCreator.Runtime.Common; +using GameCreator.Runtime.Inventory; + +namespace Arawn.GameCreator2.Networking.Inventory +{ + // ════════════════════════════════════════════════════════════════════════════════════════════ + // SERVER-SIDE — Request processing, broadcasting, and helper methods + // ════════════════════════════════════════════════════════════════════════════════════════════ + + public partial class NetworkInventoryController + { + private static readonly FieldInfo s_RuntimeItemIdField = typeof(RuntimeItem) + .GetField("m_RuntimeID", BindingFlags.Instance | BindingFlags.NonPublic); + + private static readonly FieldInfo s_RuntimeSocketAttachmentField = typeof(RuntimeSocket) + .GetField("m_AttachmentRuntimeItem", BindingFlags.Instance | BindingFlags.NonPublic); + + static NetworkInventoryController() + { + if (s_RuntimeItemIdField == null || s_RuntimeSocketAttachmentField == null) + { + Debug.LogWarning( + "[NetworkInventoryController] Reflection dependencies for RuntimeItem/RuntimeSocket could not be resolved. " + + "Inventory runtime reconstruction may degrade until patch signatures are updated for this GC2 version."); + } + } + + // ════════════════════════════════════════════════════════════════════════════════════════ + // SERVER-SIDE: PROCESS REQUESTS + // ════════════════════════════════════════════════════════════════════════════════════════ + + #region Server Processing + + /// + /// [Server] Process content add request. + /// + public NetworkContentAddResponse ProcessContentAddRequest(NetworkContentAddRequest request, uint clientNetworkId) + { + if (!m_IsServer) + { + return new NetworkContentAddResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.NotAuthorized + }; + } + + // Client-originated arbitrary runtime payloads are not allowed. + if (request.ItemHash == 0) + { + return new NetworkContentAddResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.SecurityViolation + }; + } + + if (!TryResolveItem(request.ItemHash, request.ItemIdString, out Item item)) + { + return new NetworkContentAddResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.IdentityMismatch + }; + } + + RuntimeItem runtimeItem = new RuntimeItem(item); + + // Try to add + Vector2Int resultPosition; + if (request.Position.x >= 0 && request.Position.y >= 0) + { + bool success = m_Bag.Content.Add(runtimeItem, request.Position, request.AllowStack); + resultPosition = success ? request.Position : TBagContent.INVALID; + } + else + { + resultPosition = m_Bag.Content.Add(runtimeItem, request.AllowStack); + } + + if (resultPosition == TBagContent.INVALID) + { + return new NetworkContentAddResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.InsufficientSpace + }; + } + + // Update map + TrackRuntimeItemRecursive(runtimeItem); + + // Broadcast + var broadcast = new NetworkItemAddedBroadcast + { + BagNetworkId = NetworkId, + Item = ConvertToNetworkItem(runtimeItem), + Position = resultPosition, + StackCount = m_Bag.Content.GetContent(resultPosition)?.Count ?? 1 + }; + + NetworkInventoryManager.Instance?.BroadcastItemAdded(broadcast); + OnItemAdded?.Invoke(broadcast); + + return new NetworkContentAddResponse + { + RequestId = request.RequestId, + Authorized = true, + RejectionReason = InventoryRejectionReason.None, + ResultPosition = resultPosition, + AssignedRuntimeId = runtimeItem.RuntimeID.Hash, + AssignedRuntimeIdString = runtimeItem.RuntimeID.String + }; + } + + /// + /// [Server] Process content remove request. + /// + public NetworkContentRemoveResponse ProcessContentRemoveRequest(NetworkContentRemoveRequest request, uint clientNetworkId) + { + if (!m_IsServer) + { + return new NetworkContentRemoveResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.NotAuthorized + }; + } + + RuntimeItem removed; + Vector2Int position; + + if (request.UsePosition) + { + position = request.Position; + removed = m_Bag.Content.Remove(position); + } + else + { + if (!m_RuntimeItemMap.TryGetValue(request.RuntimeIdHash, out var runtimeItem)) + { + return new NetworkContentRemoveResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.RuntimeItemNotFound + }; + } + + position = m_Bag.Content.FindPosition(runtimeItem.RuntimeID); + removed = m_Bag.Content.Remove(runtimeItem); + } + + if (removed == null) + { + return new NetworkContentRemoveResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.RuntimeItemNotFound + }; + } + + UntrackRuntimeItemRecursive(removed); + + // Broadcast + var cell = m_Bag.Content.GetContent(position); + var removeBroadcast = new NetworkItemRemovedBroadcast + { + BagNetworkId = NetworkId, + RuntimeIdHash = removed.RuntimeID.Hash, + Position = position, + RemainingStackCount = cell?.Count ?? 0 + }; + + NetworkInventoryManager.Instance?.BroadcastItemRemoved(removeBroadcast); + OnItemRemoved?.Invoke(removeBroadcast); + + return new NetworkContentRemoveResponse + { + RequestId = request.RequestId, + Authorized = true, + RejectionReason = InventoryRejectionReason.None, + RemovedItem = ConvertToNetworkItem(removed) + }; + } + + /// + /// [Server] Process content move request. + /// + public NetworkContentMoveResponse ProcessContentMoveRequest(NetworkContentMoveRequest request, uint clientNetworkId) + { + if (!m_IsServer) + { + return new NetworkContentMoveResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.NotAuthorized + }; + } + + if (!m_Bag.Content.CanMove(request.FromPosition, request.ToPosition, request.AllowStack)) + { + return new NetworkContentMoveResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.InvalidPosition + }; + } + + var moveCell = m_Bag.Content.GetContent(request.FromPosition); + long runtimeIdHash = moveCell?.RootRuntimeItemID.Hash ?? 0; + + bool moveSuccess = m_Bag.Content.Move(request.FromPosition, request.ToPosition, request.AllowStack); + + if (!moveSuccess) + { + return new NetworkContentMoveResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.InvalidOperation + }; + } + + // Broadcast + var moveBroadcast = new NetworkItemMovedBroadcast + { + BagNetworkId = NetworkId, + RuntimeIdHash = runtimeIdHash, + FromPosition = request.FromPosition, + ToPosition = request.ToPosition + }; + + NetworkInventoryManager.Instance?.BroadcastItemMoved(moveBroadcast); + OnItemMoved?.Invoke(moveBroadcast); + + return new NetworkContentMoveResponse + { + RequestId = request.RequestId, + Authorized = true, + RejectionReason = InventoryRejectionReason.None, + FinalPosition = request.ToPosition + }; + } + + /// + /// [Server] Process content use request. + /// + public NetworkContentUseResponse ProcessContentUseRequest(NetworkContentUseRequest request, uint clientNetworkId) + { + if (!m_IsServer) + { + return new NetworkContentUseResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.NotAuthorized + }; + } + + bool useSuccess; + bool wasConsumed = false; + + if (request.UsePosition) + { + var useCell = m_Bag.Content.GetContent(request.Position); + if (useCell == null || useCell.Available) + { + return new NetworkContentUseResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.RuntimeItemNotFound + }; + } + + var useItem = useCell.RootRuntimeItem; + wasConsumed = useItem.Item.Usage.ConsumeWhenUse; + useSuccess = m_Bag.Content.Use(request.Position); + } + else + { + if (!m_RuntimeItemMap.TryGetValue(request.RuntimeIdHash, out var useItem)) + { + return new NetworkContentUseResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.RuntimeItemNotFound + }; + } + + wasConsumed = useItem.Item.Usage.ConsumeWhenUse; + useSuccess = m_Bag.Content.Use(useItem); + + if (useSuccess && wasConsumed) + { + UntrackRuntimeItemRecursive(useItem); + } + } + + if (!useSuccess) + { + return new NetworkContentUseResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.CannotUse + }; + } + + // Broadcast + var useBroadcast = new NetworkItemUsedBroadcast + { + BagNetworkId = NetworkId, + RuntimeIdHash = request.RuntimeIdHash, + WasConsumed = wasConsumed + }; + + NetworkInventoryManager.Instance?.BroadcastItemUsed(useBroadcast); + OnItemUsed?.Invoke(useBroadcast); + + return new NetworkContentUseResponse + { + RequestId = request.RequestId, + Authorized = true, + RejectionReason = InventoryRejectionReason.None, + WasConsumed = wasConsumed + }; + } + + /// + /// [Server] Process content drop request. + /// + public NetworkContentDropResponse ProcessContentDropRequest(NetworkContentDropRequest request, uint clientNetworkId) + { + LogPickupDebug( + $"{name}: server drop request received req={request.RequestId} client={clientNetworkId} actor={request.ActorNetworkId} targetBag={request.TargetBagNetworkId} runtime={request.RuntimeIdHash} position={request.DropPosition} controllerBag={NetworkId} hasRuntime={m_RuntimeItemMap.ContainsKey(request.RuntimeIdHash)}", + this); + + if (!m_IsServer) + { + LogPickupWarning($"{name}: drop rejected not server req={request.RequestId} runtime={request.RuntimeIdHash}", this); + return new NetworkContentDropResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.NotAuthorized + }; + } + + if (!m_RuntimeItemMap.TryGetValue(request.RuntimeIdHash, out var dropItem)) + { + LogPickupWarning( + $"{name}: drop rejected runtime not found req={request.RequestId} runtime={request.RuntimeIdHash} trackedItems={m_RuntimeItemMap.Count}", + this); + return new NetworkContentDropResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.RuntimeItemNotFound + }; + } + + if (!dropItem.Item.CanDrop) + { + LogPickupWarning( + $"{name}: drop rejected item cannot drop req={request.RequestId} item={DescribeRuntimeItem(dropItem)}", + this); + return new NetworkContentDropResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.CannotDrop + }; + } + + Vector2Int sourcePosition = m_Bag.Content.FindPosition(dropItem.RuntimeID); + NetworkRuntimeItem droppedItem = ConvertToNetworkItem(dropItem); + GameObject dropped; + m_IsApplyingNetworkState = true; + try + { + dropped = m_Bag.Content.Drop(dropItem, request.DropPosition); + } + finally + { + m_IsApplyingNetworkState = false; + } + + if (dropped == null) + { + LogPickupWarning( + $"{name}: drop rejected GC2 Content.Drop returned null req={request.RequestId} item={DescribeRuntimeItem(dropItem)} position={request.DropPosition}", + this); + return new NetworkContentDropResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.CannotDrop + }; + } + + UntrackRuntimeItemRecursive(dropItem); + + var removeBroadcast = new NetworkItemRemovedBroadcast + { + BagNetworkId = NetworkId, + RuntimeIdHash = request.RuntimeIdHash, + Position = sourcePosition, + RemainingStackCount = m_Bag.Content.GetContent(sourcePosition)?.Count ?? 0 + }; + + NetworkInventoryManager.Instance?.BroadcastItemRemoved(removeBroadcast); + OnItemRemoved?.Invoke(removeBroadcast); + + var dropBroadcast = new NetworkItemDroppedBroadcast + { + SourceBagNetworkId = NetworkId, + Item = droppedItem, + Position = request.DropPosition + }; + + NetworkInventoryManager.Instance?.BroadcastItemDropped(dropBroadcast); + RememberDroppedItemInstance(request.RuntimeIdHash, dropped, NetworkId, droppedItem, request.DropPosition); + RememberServerDroppedWorldItem(request.RuntimeIdHash, NetworkId, droppedItem, request.DropPosition); + LogPickupDebug( + $"{name}: drop accepted req={request.RequestId} item={DescribeNetworkItem(droppedItem)} sourcePosition={sourcePosition} dropPosition={request.DropPosition} instance={(dropped != null ? dropped.name : "null")}", + this); + CacheCurrentSyncState(); + + return new NetworkContentDropResponse + { + RequestId = request.RequestId, + Authorized = true, + RejectionReason = InventoryRejectionReason.None, + DroppedCount = 1 + }; + } + + /// + /// [Server] Process transfer from this bag to another registered bag. + /// + public NetworkTransferResponse ProcessTransferRequest( + NetworkTransferRequest request, + NetworkInventoryController destination, + uint clientNetworkId) + { + LogPickupDebug( + $"{name}: server transfer request received req={request.RequestId} client={clientNetworkId} actor={request.ActorNetworkId} sourceBag={request.SourceBagNetworkId} destinationBag={request.DestinationBagNetworkId} runtime={request.RuntimeIdHash} destination={request.DestinationPosition} sourceControllerBag={NetworkId} destinationControllerBag={(destination != null ? destination.NetworkId : 0)} trackedItems={m_RuntimeItemMap.Count}", + this); + + if (!m_IsServer || destination == null || !destination.m_IsServer) + { + LogPickupWarning( + $"{name}: transfer rejected not server-authoritative req={request.RequestId} runtime={request.RuntimeIdHash} sourceServer={m_IsServer} destination={(destination != null ? destination.name : "null")} destinationServer={(destination != null && destination.m_IsServer)}", + this); + return new NetworkTransferResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.NotAuthorized + }; + } + + if (destination == this) + { + LogPickupWarning($"{name}: transfer rejected source and destination are the same req={request.RequestId}", this); + return new NetworkTransferResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.InvalidOperation + }; + } + + if (!m_RuntimeItemMap.TryGetValue(request.RuntimeIdHash, out RuntimeItem runtimeItem)) + { + LogPickupWarning( + $"{name}: transfer rejected runtime not found req={request.RequestId} runtime={request.RuntimeIdHash} trackedItems={m_RuntimeItemMap.Count}", + this); + return new NetworkTransferResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.RuntimeItemNotFound + }; + } + + Vector2Int sourcePosition = m_Bag.Content.FindPosition(runtimeItem.RuntimeID); + RuntimeItem removed = m_Bag.Content.Remove(runtimeItem); + if (removed == null) + { + LogPickupWarning( + $"{name}: transfer rejected GC2 Content.Remove returned null req={request.RequestId} item={DescribeRuntimeItem(runtimeItem)}", + this); + return new NetworkTransferResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.RuntimeItemNotFound + }; + } + + UntrackRuntimeItemRecursive(removed); + + Vector2Int finalPosition; + if (request.DestinationPosition.x >= 0 && request.DestinationPosition.y >= 0) + { + bool added = destination.m_Bag.Content.Add( + removed, + request.DestinationPosition, + request.AllowStack); + finalPosition = added ? request.DestinationPosition : TBagContent.INVALID; + } + else + { + finalPosition = destination.m_Bag.Content.Add(removed, request.AllowStack); + } + + if (finalPosition == TBagContent.INVALID) + { + LogPickupWarning( + $"{name}: transfer rejected destination has insufficient space req={request.RequestId} item={DescribeRuntimeItem(removed)} destinationBag={destination.NetworkId} requested={request.DestinationPosition}", + this); + + if (sourcePosition.x >= 0 && sourcePosition.y >= 0) + { + m_Bag.Content.Add(removed, sourcePosition, true); + } + else + { + m_Bag.Content.Add(removed, true); + } + + TrackRuntimeItemRecursive(removed); + + return new NetworkTransferResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.InsufficientSpace + }; + } + + destination.TrackRuntimeItemRecursive(removed); + CacheCurrentSyncState(); + destination.CacheCurrentSyncState(); + + NetworkInventoryManager manager = NetworkInventoryManager.Instance; + if (manager != null) + { + manager.BroadcastFullSnapshot(GetFullSnapshot()); + manager.BroadcastFullSnapshot(destination.GetFullSnapshot()); + } + + LogPickupDebug( + $"{name}: transfer accepted req={request.RequestId} item={DescribeRuntimeItem(removed)} sourcePosition={sourcePosition} destinationBag={destination.NetworkId} finalPosition={finalPosition}", + this); + + return new NetworkTransferResponse + { + RequestId = request.RequestId, + Authorized = true, + RejectionReason = InventoryRejectionReason.None, + FinalPosition = finalPosition + }; + } + + private void BroadcastServerDropFromLocalMutation(NetworkRuntimeItem droppedItem, long runtimeIdHash, Vector3 position) + { + if (!m_IsServer || droppedItem.ItemHash == 0) return; + + NetworkInventoryManager manager = NetworkInventoryManager.Instance; + if (manager == null) return; + + var removeBroadcast = new NetworkItemRemovedBroadcast + { + BagNetworkId = NetworkId, + RuntimeIdHash = runtimeIdHash, + Position = TBagContent.INVALID, + RemainingStackCount = 0 + }; + + manager.BroadcastItemRemoved(removeBroadcast); + + manager.BroadcastItemDropped(new NetworkItemDroppedBroadcast + { + SourceBagNetworkId = NetworkId, + Item = droppedItem, + Position = position + }); + + RememberServerDroppedWorldItem(runtimeIdHash, NetworkId, droppedItem, position); + CacheCurrentSyncState(); + } + + public NetworkPickupResponse ProcessPickupRequest(NetworkPickupRequest request, uint clientNetworkId) + { + LogPickupDebug( + $"{name}: server pickup request received req={request.RequestId} client={clientNetworkId} actor={request.ActorNetworkId} pickerBag={request.PickerBagNetworkId} sourceBag={request.SourceBagNetworkId} runtime={request.RuntimeIdHash} destination={request.DestinationPosition} controllerBag={NetworkId} knownDropped={s_ServerDroppedWorldItems.ContainsKey(request.RuntimeIdHash)} knownDropCount={s_ServerDroppedWorldItems.Count}", + this); + + if (!m_IsServer) + { + LogPickupWarning($"{name}: pickup rejected not server req={request.RequestId} runtime={request.RuntimeIdHash}", this); + return new NetworkPickupResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.NotAuthorized + }; + } + + if (request.PickerBagNetworkId != NetworkId) + { + LogPickupWarning( + $"{name}: pickup rejected bag mismatch req={request.RequestId} runtime={request.RuntimeIdHash} requestPickerBag={request.PickerBagNetworkId} controllerBag={NetworkId}", + this); + return new NetworkPickupResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.BagNotFound + }; + } + + if (!TryGetServerDroppedWorldItem(request.RuntimeIdHash, out ServerDroppedWorldItem droppedWorldItem)) + { + // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT + if (TryProcessWorldObjectPickupRequest(request, clientNetworkId, out NetworkPickupResponse worldObjectResponse)) + { + return worldObjectResponse; + } + + LogPickupWarning( + $"{name}: pickup rejected dropped runtime not found req={request.RequestId} runtime={request.RuntimeIdHash} knownDropCount={s_ServerDroppedWorldItems.Count}", + this); + return new NetworkPickupResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.RuntimeItemNotFound + }; + } + + if (request.SourceBagNetworkId != 0 && droppedWorldItem.SourceBagNetworkId != request.SourceBagNetworkId) + { + LogPickupWarning( + $"{name}: pickup rejected source mismatch req={request.RequestId} runtime={request.RuntimeIdHash} requestSource={request.SourceBagNetworkId} rememberedSource={droppedWorldItem.SourceBagNetworkId}", + this); + return new NetworkPickupResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.IdentityMismatch + }; + } + + RuntimeItem runtimeItem = ReconstructRuntimeItem(droppedWorldItem.Item); + if (runtimeItem == null) + { + LogPickupWarning( + $"{name}: pickup rejected failed reconstruct req={request.RequestId} droppedItem={DescribeNetworkItem(droppedWorldItem.Item)}", + this); + return new NetworkPickupResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.IdentityMismatch + }; + } + + Vector2Int finalPosition; + m_IsApplyingNetworkState = true; + try + { + if (request.DestinationPosition.x >= 0 && request.DestinationPosition.y >= 0) + { + bool added = m_Bag.Content.Add(runtimeItem, request.DestinationPosition, true); + finalPosition = added ? request.DestinationPosition : TBagContent.INVALID; + } + else + { + finalPosition = m_Bag.Content.Add(runtimeItem, true); + } + } + finally + { + m_IsApplyingNetworkState = false; + } + + if (finalPosition == TBagContent.INVALID) + { + LogPickupWarning( + $"{name}: pickup rejected insufficient space req={request.RequestId} item={DescribeRuntimeItem(runtimeItem)} requestedDestination={request.DestinationPosition}", + this); + return new NetworkPickupResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.InsufficientSpace + }; + } + + s_ServerDroppedWorldItems.Remove(request.RuntimeIdHash); + TrackRuntimeItemRecursive(runtimeItem); + + NetworkInventoryManager manager = NetworkInventoryManager.Instance; + if (manager != null) + { + var addBroadcast = new NetworkItemAddedBroadcast + { + BagNetworkId = NetworkId, + Item = ConvertToNetworkItem(runtimeItem), + Position = finalPosition, + StackCount = m_Bag.Content.GetContent(finalPosition)?.Count ?? 1 + }; + + manager.BroadcastItemAdded(addBroadcast); + OnItemAdded?.Invoke(addBroadcast); + + LogPickupDebug( + $"{name}: pickup accepted broadcasting item add req={request.RequestId} pickerBag={NetworkId} sourceBag={droppedWorldItem.SourceBagNetworkId} item={DescribeRuntimeItem(runtimeItem)} finalPosition={finalPosition}", + this); + + manager.BroadcastDroppedItemRemoved(new NetworkDroppedItemRemovedBroadcast + { + SourceBagNetworkId = droppedWorldItem.SourceBagNetworkId, + RuntimeIdHash = request.RuntimeIdHash, + Position = droppedWorldItem.Position + }); + } + + bool destroyedLocalDrop = TryDestroyDroppedItemInstance(request.RuntimeIdHash); + LogPickupDebug( + $"{name}: pickup completed req={request.RequestId} runtime={request.RuntimeIdHash} destroyedServerDropInstance={destroyedLocalDrop} remainingServerDrops={s_ServerDroppedWorldItems.Count}", + this); + CacheCurrentSyncState(); + + return new NetworkPickupResponse + { + RequestId = request.RequestId, + Authorized = true, + RejectionReason = InventoryRejectionReason.None, + PickedUpItem = ConvertToNetworkItem(runtimeItem), + PlacedPosition = finalPosition + }; + } + + // [LOCAL-EDIT] #PILFER-INVENTORY-SERVER-LOOT + public NetworkLootResponse ProcessLootRequest(NetworkLootRequest request, uint clientNetworkId) + { + Debug.Log( + $"[NetworkInventoryLootDebug] {name}: server loot request received req={request.RequestId} client={clientNetworkId} actor={request.ActorNetworkId} container={request.ContainerBagNetworkId} controllerBag={NetworkId} world={IsWorldInventory}"); + + if (!m_IsServer) + { + return BuildLootResponse(request, false, false, InventoryRejectionReason.NotAuthorized, NetworkLootFailure.None); + } + + if (request.ContainerBagNetworkId != NetworkId) + { + return BuildLootResponse(request, false, false, InventoryRejectionReason.BagNotFound, NetworkLootFailure.ContainerBagNotFound); + } + + if (!IsWorldInventory) + { + return BuildLootResponse(request, false, false, InventoryRejectionReason.InvalidOperation, NetworkLootFailure.ContainerIsNotWorldInventory); + } + + NetworkLootContainer lootContainer = GetComponent() ?? + GetComponentInParent() ?? + GetComponentInChildren(); + if (lootContainer == null) + { + return BuildLootResponse(request, false, false, InventoryRejectionReason.InvalidOperation, NetworkLootFailure.LootContainerMissing); + } + + if (lootContainer.LootTable == null) + { + return BuildLootResponse(request, false, false, InventoryRejectionReason.ItemNotFound, NetworkLootFailure.LootTableMissing); + } + + if (lootContainer.GenerateOnce && lootContainer.HasGenerated) + { + BroadcastFullState(); + return BuildLootResponse(request, true, false, InventoryRejectionReason.None, NetworkLootFailure.AlreadyGenerated); + } + + bool generated = lootContainer.LootTable.Run(m_Bag); + if (!generated) + { + return BuildLootResponse(request, false, false, InventoryRejectionReason.ItemNotFound, NetworkLootFailure.LootRollFailed); + } + + lootContainer.MarkGenerated(); + TrackAllCurrentRuntimeItemsForServerLoot(); + CacheCurrentSyncState(); + BroadcastFullState(); + + int cells = GetFullSnapshot().Cells?.Length ?? 0; + if (lootContainer.LogDiagnostics) + { + Debug.Log( + $"[NetworkInventoryLootDebug] {name}: generated server loot container={NetworkId} generated={generated} cells={cells}", + this); + } + + return BuildLootResponse(request, true, true, InventoryRejectionReason.None, NetworkLootFailure.None); + } + + // [LOCAL-EDIT] #PILFER-INVENTORY-SERVER-LOOT + private void TrackAllCurrentRuntimeItemsForServerLoot() + { + foreach (Cell cell in m_Bag.Content.CellList) + { + if (cell == null || cell.Available) continue; + + TrackRuntimeItemRecursive(cell.RootRuntimeItem); + foreach (IdString runtimeId in cell.List) + { + RuntimeItem runtimeItem = m_Bag.Content.GetRuntimeItem(runtimeId); + TrackRuntimeItemRecursive(runtimeItem); + } + } + } + + // [LOCAL-EDIT] #PILFER-INVENTORY-SERVER-LOOT + private static NetworkLootResponse BuildLootResponse( + NetworkLootRequest request, + bool authorized, + bool generated, + InventoryRejectionReason reason, + NetworkLootFailure failure) + { + return new NetworkLootResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + ContainerBagNetworkId = request.ContainerBagNetworkId, + Authorized = authorized, + Generated = generated, + RejectionReason = reason, + LootFailure = failure + }; + } + + private void BroadcastServerPickupFromDroppedItemIfNeeded(RuntimeItem item) + { + if (!m_IsServer || item == null) return; + long removedDropRuntimeHash = item.RuntimeID.Hash; + if (!TryTakeServerDroppedWorldItem(removedDropRuntimeHash, out ServerDroppedWorldItem droppedWorldItem)) + { + if (!TryTakeServerDroppedWorldItemForLocalPickup( + item, + transform.position, + out removedDropRuntimeHash, + out droppedWorldItem)) + { + LogPickupDebug( + $"{name}: server local add is not a remembered dropped item={DescribeRuntimeItem(item)} bag={NetworkId} knownDropCount={s_ServerDroppedWorldItems.Count}", + this); + return; + } + + LogPickupDebug( + $"{name}: server local pickup matched remembered drop by item/proximity item={DescribeRuntimeItem(item)} rememberedRuntime={removedDropRuntimeHash} sourceBag={droppedWorldItem.SourceBagNetworkId} dropPosition={droppedWorldItem.Position}", + this); + } + + TrackRuntimeItemRecursive(item); + + Vector2Int position = m_Bag.Content.FindPosition(item.RuntimeID); + NetworkInventoryManager manager = NetworkInventoryManager.Instance; + if (manager == null) return; + + var addBroadcast = new NetworkItemAddedBroadcast + { + BagNetworkId = NetworkId, + Item = ConvertToNetworkItem(item), + Position = position, + StackCount = position != TBagContent.INVALID + ? m_Bag.Content.GetContent(position)?.Count ?? 1 + : 1 + }; + + manager.BroadcastItemAdded(addBroadcast); + OnItemAdded?.Invoke(addBroadcast); + + manager.BroadcastDroppedItemRemoved(new NetworkDroppedItemRemovedBroadcast + { + SourceBagNetworkId = droppedWorldItem.SourceBagNetworkId, + RuntimeIdHash = removedDropRuntimeHash, + Position = droppedWorldItem.Position + }); + + bool destroyedLocalDrop = TryDestroyDroppedItemInstance(removedDropRuntimeHash); + LogPickupDebug( + $"{name}: server local pickup broadcast item={DescribeRuntimeItem(item)} removedDropRuntime={removedDropRuntimeHash} sourceBag={droppedWorldItem.SourceBagNetworkId} destinationBag={NetworkId} destroyedServerDropInstance={destroyedLocalDrop}", + this); + CacheCurrentSyncState(); + } + + private static void RememberServerDroppedWorldItem( + long runtimeIdHash, + uint sourceBagNetworkId, + NetworkRuntimeItem item, + Vector3 position) + { + if (runtimeIdHash == 0) return; + + PruneServerDroppedWorldItems(); + s_ServerDroppedWorldItems[runtimeIdHash] = new ServerDroppedWorldItem + { + SourceBagNetworkId = sourceBagNetworkId, + Item = item, + Position = position, + Time = Time.unscaledTime + }; + + LogPickupDebug( + $"remembered server dropped world item runtime={runtimeIdHash} sourceBag={sourceBagNetworkId} item={DescribeNetworkItem(item)} position={position} knownDropCount={s_ServerDroppedWorldItems.Count}"); + } + + private static bool TryGetServerDroppedWorldItem(long runtimeIdHash, out ServerDroppedWorldItem droppedWorldItem) + { + PruneServerDroppedWorldItems(); + return s_ServerDroppedWorldItems.TryGetValue(runtimeIdHash, out droppedWorldItem); + } + + private static bool TryTakeServerDroppedWorldItem(long runtimeIdHash, out ServerDroppedWorldItem droppedWorldItem) + { + PruneServerDroppedWorldItems(); + + if (s_ServerDroppedWorldItems.TryGetValue(runtimeIdHash, out droppedWorldItem)) + { + s_ServerDroppedWorldItems.Remove(runtimeIdHash); + return true; + } + + return false; + } + + private static bool TryTakeServerDroppedWorldItemForLocalPickup( + RuntimeItem localItem, + Vector3 pickerPosition, + out long runtimeIdHash, + out ServerDroppedWorldItem droppedWorldItem) + { + PruneServerDroppedWorldItems(); + runtimeIdHash = 0; + droppedWorldItem = default; + + if (localItem?.Item == null || s_ServerDroppedWorldItems.Count == 0) + { + return false; + } + + int itemHash = localItem.ItemID.Hash; + float bestDistance = float.MaxValue; + long bestRuntimeIdHash = 0; + ServerDroppedWorldItem bestItem = default; + + foreach (KeyValuePair entry in s_ServerDroppedWorldItems) + { + ServerDroppedWorldItem candidate = entry.Value; + if (candidate.Item.ItemHash != itemHash) continue; + + float distance = Vector3.SqrMagnitude(candidate.Position - pickerPosition); + if (distance >= bestDistance) continue; + + bestDistance = distance; + bestRuntimeIdHash = entry.Key; + bestItem = candidate; + } + + if (bestRuntimeIdHash == 0 || bestDistance > 16f) + { + return false; + } + + s_ServerDroppedWorldItems.Remove(bestRuntimeIdHash); + runtimeIdHash = bestRuntimeIdHash; + droppedWorldItem = bestItem; + return true; + } + + private static void PruneServerDroppedWorldItems() + { + if (s_ServerDroppedWorldItems.Count == 0) return; + + s_SharedRuntimeIdBuffer.Clear(); + float now = Time.unscaledTime; + foreach (KeyValuePair entry in s_ServerDroppedWorldItems) + { + if (now - entry.Value.Time <= 600f) continue; + s_SharedRuntimeIdBuffer.Add(entry.Key); + } + + for (int i = 0; i < s_SharedRuntimeIdBuffer.Count; i++) + { + s_ServerDroppedWorldItems.Remove(s_SharedRuntimeIdBuffer[i]); + } + } + + private void BroadcastServerSocketAttach(RuntimeItem parent, RuntimeItem attachment, IdString socketId) + { + if (!m_IsServer || parent == null || attachment == null) return; + + var broadcast = new NetworkSocketChangeBroadcast + { + BagNetworkId = NetworkId, + ParentRuntimeIdHash = parent.RuntimeID.Hash, + SocketHash = socketId.Hash, + HasAttachment = true, + Attachment = ConvertToNetworkItem(attachment) + }; + + NetworkInventoryManager.Instance?.BroadcastSocketChange(broadcast); + OnSocketChanged?.Invoke(broadcast); + CacheCurrentSyncState(); + } + + private void BroadcastServerSocketDetach(RuntimeItem parent) + { + if (!m_IsServer || parent == null) return; + + NetworkInventoryManager.Instance?.BroadcastFullSnapshot(GetFullSnapshot()); + CacheCurrentSyncState(); + } + + /// + /// [Server] Process equipment request. + /// + public async Task ProcessEquipmentRequest(NetworkEquipmentRequest request, uint clientNetworkId) + { + if (!m_IsServer) + { + return new NetworkEquipmentResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.NotAuthorized + }; + } + + bool equipSuccess = false; + int equippedIndex = -1; + + switch (request.Action) + { + case EquipmentAction.Equip: + case EquipmentAction.EquipToSlot: + case EquipmentAction.EquipToIndex: + { + if (!m_RuntimeItemMap.TryGetValue(request.RuntimeIdHash, out var equipItem)) + { + return new NetworkEquipmentResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.RuntimeItemNotFound + }; + } + + if (request.Action == EquipmentAction.EquipToIndex) + { + equipSuccess = await m_Bag.Equipment.EquipToIndex(equipItem, request.SlotOrIndex); + equippedIndex = request.SlotOrIndex; + } + else if (request.Action == EquipmentAction.EquipToSlot) + { + equipSuccess = await m_Bag.Equipment.Equip(equipItem, request.SlotOrIndex); + equippedIndex = m_Bag.Equipment.GetEquippedIndex(equipItem); + } + else + { + equipSuccess = await m_Bag.Equipment.Equip(equipItem); + equippedIndex = m_Bag.Equipment.GetEquippedIndex(equipItem); + } + + if (equipSuccess) + { + var equipBroadcast = new NetworkItemEquippedBroadcast + { + BagNetworkId = NetworkId, + RuntimeIdHash = request.RuntimeIdHash, + EquipmentIndex = equippedIndex + }; + NetworkInventoryManager.Instance?.BroadcastItemEquipped(equipBroadcast); + OnItemEquipped?.Invoke(equipBroadcast); + } + break; + } + + case EquipmentAction.Unequip: + { + if (!m_RuntimeItemMap.TryGetValue(request.RuntimeIdHash, out var unequipItem)) + { + return new NetworkEquipmentResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.RuntimeItemNotFound + }; + } + + equippedIndex = m_Bag.Equipment.GetEquippedIndex(unequipItem); + equipSuccess = await m_Bag.Equipment.Unequip(unequipItem); + + if (equipSuccess) + { + var unequipBroadcast = new NetworkItemUnequippedBroadcast + { + BagNetworkId = NetworkId, + RuntimeIdHash = request.RuntimeIdHash, + EquipmentIndex = equippedIndex + }; + NetworkInventoryManager.Instance?.BroadcastItemUnequipped(unequipBroadcast); + OnItemUnequipped?.Invoke(unequipBroadcast); + } + break; + } + + case EquipmentAction.UnequipFromIndex: + { + var slotId = m_Bag.Equipment.GetSlotRootRuntimeItemID(request.SlotOrIndex); + long runtimeIdHash = slotId.Hash; + + equipSuccess = await m_Bag.Equipment.UnequipFromIndex(request.SlotOrIndex); + equippedIndex = request.SlotOrIndex; + + if (equipSuccess) + { + var unequipIdxBroadcast = new NetworkItemUnequippedBroadcast + { + BagNetworkId = NetworkId, + RuntimeIdHash = runtimeIdHash, + EquipmentIndex = equippedIndex + }; + NetworkInventoryManager.Instance?.BroadcastItemUnequipped(unequipIdxBroadcast); + OnItemUnequipped?.Invoke(unequipIdxBroadcast); + } + break; + } + } + + return new NetworkEquipmentResponse + { + RequestId = request.RequestId, + Authorized = equipSuccess, + RejectionReason = equipSuccess ? InventoryRejectionReason.None : InventoryRejectionReason.CannotEquip, + EquippedIndex = equippedIndex + }; + } + + /// + /// [Server] Process socket request. + /// + public NetworkSocketResponse ProcessSocketRequest(NetworkSocketRequest request, uint clientNetworkId) + { + if (!m_IsServer) + { + return new NetworkSocketResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.NotAuthorized + }; + } + + if (!m_RuntimeItemMap.TryGetValue(request.ParentRuntimeIdHash, out var parentItem)) + { + return new NetworkSocketResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.RuntimeItemNotFound + }; + } + + bool socketSuccess = false; + NetworkRuntimeItem detachedItem = default; + NetworkRuntimeItem attachedItem = default; + int usedSocketHash = request.SocketHash; + IdString socketId = IdString.EMPTY; + + if (request.Action == SocketAction.AttachToSocket || request.Action == SocketAction.DetachFromSocket) + { + if (!TryResolveSocketId(parentItem, request.SocketHash, request.SocketIdString, out socketId)) + { + return new NetworkSocketResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.IdentityMismatch + }; + } + + usedSocketHash = socketId.Hash; + } + + switch (request.Action) + { + case SocketAction.Attach: + case SocketAction.AttachToSocket: + { + if (!m_RuntimeItemMap.TryGetValue(request.AttachmentRuntimeIdHash, out var attachment)) + { + return new NetworkSocketResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.RuntimeItemNotFound + }; + } + + if (request.Action == SocketAction.AttachToSocket) + { + socketSuccess = m_Bag.Equipment.AttachTo(parentItem, attachment, socketId); + } + else + { + socketSuccess = m_Bag.Equipment.AttachTo(parentItem, attachment); + } + + if (socketSuccess) + { + attachedItem = ConvertToNetworkItem(attachment); + } + break; + } + + case SocketAction.Detach: + case SocketAction.DetachFromSocket: + { + RuntimeItem detached; + if (request.Action == SocketAction.DetachFromSocket) + { + detached = m_Bag.Equipment.DetachFrom(parentItem, socketId); + } + else + { + if (!m_RuntimeItemMap.TryGetValue(request.AttachmentRuntimeIdHash, out var detachAttachment)) + { + return new NetworkSocketResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.RuntimeItemNotFound + }; + } + detached = m_Bag.Equipment.DetachFrom(parentItem, detachAttachment); + } + + socketSuccess = detached != null; + if (socketSuccess) + { + detachedItem = ConvertToNetworkItem(detached); + } + break; + } + } + + if (socketSuccess) + { + var socketBroadcast = new NetworkSocketChangeBroadcast + { + BagNetworkId = NetworkId, + ParentRuntimeIdHash = request.ParentRuntimeIdHash, + SocketHash = usedSocketHash, + HasAttachment = request.Action == SocketAction.Attach || request.Action == SocketAction.AttachToSocket, + Attachment = attachedItem + }; + NetworkInventoryManager.Instance?.BroadcastSocketChange(socketBroadcast); + OnSocketChanged?.Invoke(socketBroadcast); + } + + return new NetworkSocketResponse + { + RequestId = request.RequestId, + Authorized = socketSuccess, + RejectionReason = socketSuccess ? InventoryRejectionReason.None : InventoryRejectionReason.CannotAttach, + UsedSocketHash = usedSocketHash, + DetachedItem = detachedItem + }; + } + + /// + /// [Server] Process wealth request. + /// + public NetworkWealthResponse ProcessWealthRequest(NetworkWealthRequest request, uint clientNetworkId) + { + if (!m_IsServer) + { + return new NetworkWealthResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.NotAuthorized + }; + } + + if (!TryResolveCurrencyId(request.CurrencyHash, request.CurrencyIdString, out IdString currencyId)) + { + return new NetworkWealthResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.IdentityMismatch + }; + } + + int oldValue = m_Bag.Wealth.Get(currencyId); + int newValue; + + switch (request.Action) + { + case WealthAction.Set: + m_Bag.Wealth.Set(currencyId, request.Value); + newValue = request.Value; + break; + + case WealthAction.Add: + m_Bag.Wealth.Add(currencyId, request.Value); + newValue = oldValue + request.Value; + break; + + case WealthAction.Subtract: + if (oldValue < request.Value) + { + return new NetworkWealthResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.InsufficientFunds + }; + } + m_Bag.Wealth.Subtract(currencyId, request.Value); + newValue = oldValue - request.Value; + break; + + default: + return new NetworkWealthResponse + { + RequestId = request.RequestId, + Authorized = false, + RejectionReason = InventoryRejectionReason.InvalidOperation + }; + } + + // Broadcast + var wealthBroadcast = new NetworkWealthChangeBroadcast + { + BagNetworkId = NetworkId, + CurrencyHash = request.CurrencyHash, + NewValue = newValue, + Change = newValue - oldValue + }; + NetworkInventoryManager.Instance?.BroadcastWealthChange(wealthBroadcast); + OnWealthChanged?.Invoke(wealthBroadcast); + + return new NetworkWealthResponse + { + RequestId = request.RequestId, + Authorized = true, + RejectionReason = InventoryRejectionReason.None, + NewValue = newValue, + OldValue = oldValue + }; + } + + #endregion + + // ════════════════════════════════════════════════════════════════════════════════════════ + // BROADCAST RECEIVERS + // ════════════════════════════════════════════════════════════════════════════════════════ + + #region Broadcast Receivers + + public void ReceiveItemAddedBroadcast(NetworkItemAddedBroadcast broadcast) + { + if (m_IsServer) return; + + m_IsApplyingNetworkState = true; + try + { + if (broadcast.Item.RuntimeIdHash != 0 && + m_PendingPickupLocalRuntimeByServerRuntime.TryGetValue( + broadcast.Item.RuntimeIdHash, + out long provisionalRuntimeHash)) + { + m_PendingPickupLocalRuntimeByServerRuntime.Remove(broadcast.Item.RuntimeIdHash); + + if (provisionalRuntimeHash != broadcast.Item.RuntimeIdHash && + m_RuntimeItemMap.TryGetValue(provisionalRuntimeHash, out RuntimeItem provisionalItem)) + { + Vector2Int provisionalPosition = m_Bag.Content.FindPosition(provisionalItem.RuntimeID); + m_Bag.Content.Remove(provisionalItem); + UntrackRuntimeItemRecursive(provisionalItem); + + LogPickupDebug( + $"{name}: removed provisional local pickup item before authoritative add localRuntime={provisionalRuntimeHash} serverRuntime={broadcast.Item.RuntimeIdHash} provisionalPosition={provisionalPosition}", + this); + } + else + { + LogPickupDebug( + $"{name}: authoritative pickup add matched existing runtime serverRuntime={broadcast.Item.RuntimeIdHash}", + this); + } + } + + if (broadcast.Item.RuntimeIdHash != 0 && + (m_RuntimeItemMap.ContainsKey(broadcast.Item.RuntimeIdHash) || + m_Bag.Content.Contains(new IdString(broadcast.Item.RuntimeIdString)))) + { + LogPickupDebug( + $"{name}: item add broadcast skipped duplicate bag={broadcast.BagNetworkId} item={DescribeNetworkItem(broadcast.Item)} position={broadcast.Position}", + this); + return; + } + + // Reconstruct and apply + var runtimeItem = ReconstructRuntimeItem(broadcast.Item); + if (runtimeItem != null) + { + bool addedAtPosition = m_Bag.Content.Add(runtimeItem, broadcast.Position, true); + TrackRuntimeItemRecursive(runtimeItem); + LogPickupDebug( + $"{name}: item add broadcast applied bag={broadcast.BagNetworkId} item={DescribeRuntimeItem(runtimeItem)} requestedPosition={broadcast.Position} addedAtPosition={addedAtPosition} tracked={m_RuntimeItemMap.ContainsKey(runtimeItem.RuntimeID.Hash)}", + this); + } + else + { + LogPickupWarning( + $"{name}: item add broadcast failed reconstruct bag={broadcast.BagNetworkId} item={DescribeNetworkItem(broadcast.Item)}", + this); + } + } + finally + { + m_IsApplyingNetworkState = false; + } + + OnItemAdded?.Invoke(broadcast); + } + + public void ReceiveItemRemovedBroadcast(NetworkItemRemovedBroadcast broadcast) + { + if (m_IsServer) return; + + m_IsApplyingNetworkState = true; + try + { + if (m_RuntimeItemMap.TryGetValue(broadcast.RuntimeIdHash, out var runtimeItem)) + { + m_Bag.Content.Remove(runtimeItem); + UntrackRuntimeItemRecursive(runtimeItem); + } + } + finally + { + m_IsApplyingNetworkState = false; + } + + OnItemRemoved?.Invoke(broadcast); + } + + public void ReceiveItemMovedBroadcast(NetworkItemMovedBroadcast broadcast) + { + if (m_IsServer) return; + + m_IsApplyingNetworkState = true; + try + { + m_Bag.Content.Move(broadcast.FromPosition, broadcast.ToPosition, true); + } + finally + { + m_IsApplyingNetworkState = false; + } + + OnItemMoved?.Invoke(broadcast); + } + + public void ReceiveItemUsedBroadcast(NetworkItemUsedBroadcast broadcast) + { + if (m_IsServer) return; + + m_IsApplyingNetworkState = true; + try + { + if (broadcast.WasConsumed && m_RuntimeItemMap.TryGetValue(broadcast.RuntimeIdHash, out var runtimeItem)) + { + m_Bag.Content.Remove(runtimeItem); + UntrackRuntimeItemRecursive(runtimeItem); + } + } + finally + { + m_IsApplyingNetworkState = false; + } + + OnItemUsed?.Invoke(broadcast); + } + + public void ReceiveItemDroppedBroadcast(NetworkItemDroppedBroadcast broadcast) + { + if (m_IsServer) return; + if (TryAdoptPredictedDroppedItemInstance(broadcast)) + { + LogPickupDebug( + $"{name}: dropped item broadcast adopted local predicted drop sourceBag={broadcast.SourceBagNetworkId} item={DescribeNetworkItem(broadcast.Item)} position={broadcast.Position}", + this); + return; + } + + RuntimeItem runtimeItem = ReconstructRuntimeItem(broadcast.Item); + if (runtimeItem == null) + { + LogPickupWarning( + $"{name}: dropped item broadcast failed reconstruct sourceBag={broadcast.SourceBagNetworkId} item={DescribeNetworkItem(broadcast.Item)}", + this); + return; + } + + m_IsApplyingNetworkState = true; + try + { + GameObject instance = Item.Drop(runtimeItem, broadcast.Position, Quaternion.identity); + RememberDroppedItemInstance( + broadcast.Item.RuntimeIdHash, + instance, + broadcast.SourceBagNetworkId, + broadcast.Item, + broadcast.Position); + LogPickupDebug( + $"{name}: dropped item broadcast spawned instance sourceBag={broadcast.SourceBagNetworkId} item={DescribeRuntimeItem(runtimeItem)} position={broadcast.Position} instance={(instance != null ? instance.name : "null")}", + this); + } + finally + { + m_IsApplyingNetworkState = false; + } + } + + public void ReceiveDroppedItemRemovedBroadcast(NetworkDroppedItemRemovedBroadcast broadcast) + { + if (m_IsServer) return; + bool destroyed = TryDestroyDroppedItemInstance(broadcast, out long destroyedRuntimeIdHash); + LogPickupDebug( + $"{name}: dropped item remove broadcast sourceBag={broadcast.SourceBagNetworkId} runtime={broadcast.RuntimeIdHash} destroyed={destroyed} destroyedRuntime={destroyedRuntimeIdHash} position={broadcast.Position}", + this); + } + + public void ReceiveItemEquippedBroadcast(NetworkItemEquippedBroadcast broadcast) + { + if (m_IsServer) return; + + m_IsApplyingNetworkState = true; + try + { + if (m_RuntimeItemMap.TryGetValue(broadcast.RuntimeIdHash, out var runtimeItem)) + { + _ = m_Bag.Equipment.EquipToIndex(runtimeItem, broadcast.EquipmentIndex); + } + } + finally + { + m_IsApplyingNetworkState = false; + } + + OnItemEquipped?.Invoke(broadcast); + } + + public void ReceiveItemUnequippedBroadcast(NetworkItemUnequippedBroadcast broadcast) + { + if (m_IsServer) return; + + m_IsApplyingNetworkState = true; + try + { + _ = m_Bag.Equipment.UnequipFromIndex(broadcast.EquipmentIndex); + } + finally + { + m_IsApplyingNetworkState = false; + } + + OnItemUnequipped?.Invoke(broadcast); + } + + public void ReceiveSocketChangeBroadcast(NetworkSocketChangeBroadcast broadcast) + { + if (m_IsServer) return; + + m_IsApplyingNetworkState = true; + try + { + if (!m_RuntimeItemMap.TryGetValue(broadcast.ParentRuntimeIdHash, out RuntimeItem parentItem)) + { + OnSocketChanged?.Invoke(broadcast); + return; + } + + if (!TryResolveRuntimeSocketId(parentItem, broadcast.SocketHash, null, out IdString socketId) || + !parentItem.Sockets.TryGetValue(socketId, out RuntimeSocket socket)) + { + OnSocketChanged?.Invoke(broadcast); + return; + } + + RuntimeItem previousAttachment = socket.Attachment; + RuntimeItem nextAttachment = null; + if (broadcast.HasAttachment) + { + RuntimeItem attachment = ReconstructRuntimeItem(broadcast.Attachment); + if (attachment != null) + { + if (m_Bag.Content.Contains(attachment)) + { + m_Bag.Content.Remove(attachment); + } + + if (s_RuntimeSocketAttachmentField != null) + { + s_RuntimeSocketAttachmentField.SetValue(socket, attachment); + } + + TrackRuntimeItemRecursive(attachment); + nextAttachment = attachment; + } + } + else if (s_RuntimeSocketAttachmentField != null) + { + s_RuntimeSocketAttachmentField.SetValue(socket, null); + } + + if (previousAttachment != null && + (nextAttachment == null || previousAttachment.RuntimeID.Hash != nextAttachment.RuntimeID.Hash)) + { + UntrackRuntimeItemRecursive(previousAttachment); + } + } + finally + { + m_IsApplyingNetworkState = false; + } + + OnSocketChanged?.Invoke(broadcast); + } + + public void ReceiveWealthChangeBroadcast(NetworkWealthChangeBroadcast broadcast) + { + if (m_IsServer) return; + + m_IsApplyingNetworkState = true; + try + { + if (TryResolveCurrencyIdByHash(broadcast.CurrencyHash, out IdString currencyId)) + { + m_Bag.Wealth.Set(currencyId, broadcast.NewValue); + } + } + finally + { + m_IsApplyingNetworkState = false; + } + + OnWealthChanged?.Invoke(broadcast); + } + + public void ReceiveFullSnapshot(NetworkInventorySnapshot snapshot) + { + if (m_IsServer) return; + + if (snapshot.BagNetworkId != 0 && snapshot.BagNetworkId != NetworkId) + { + return; + } + + m_IsApplyingNetworkState = true; + try + { + ApplyFullSnapshot(snapshot); + } + finally + { + m_IsApplyingNetworkState = false; + } + + if (m_LogAllChanges) + { + Debug.Log($"[NetworkInventoryController] Received full snapshot: {snapshot.Cells?.Length ?? 0} cells"); + } + } + + public void ReceiveDelta(NetworkInventoryDelta delta) + { + if (m_IsServer) return; + + if (delta.BagNetworkId != 0 && delta.BagNetworkId != NetworkId) + { + return; + } + + const uint maskCells = 1u << 0; + const uint maskEquipment = 1u << 1; + const uint maskWealth = 1u << 2; + + m_IsApplyingNetworkState = true; + try + { + if ((delta.ChangeMask & maskCells) != 0 && delta.ChangedCells != null) + { + ApplyCellDelta(delta.ChangedCells); + } + + if ((delta.ChangeMask & maskEquipment) != 0 && delta.ChangedEquipment != null) + { + ApplyEquipmentDelta(delta.ChangedEquipment); + } + + if ((delta.ChangeMask & maskWealth) != 0 && delta.ChangedWealth != null) + { + ApplyWealthDelta(delta.ChangedWealth); + } + } + finally + { + m_IsApplyingNetworkState = false; + } + + CacheCurrentSyncState(); + + if (m_LogAllChanges) + { + Debug.Log( + $"[NetworkInventoryController] Applied partial delta (mask={delta.ChangeMask}) " + + $"cells={delta.ChangedCells?.Length ?? 0} " + + $"equipment={delta.ChangedEquipment?.Length ?? 0} " + + $"wealth={delta.ChangedWealth?.Length ?? 0}"); + } + } + + #endregion + } +} +#endif diff --git a/NetworkInventoryController.WorldObjectPickup.cs b/NetworkInventoryController.WorldObjectPickup.cs new file mode 100644 index 0000000..114aa91 --- /dev/null +++ b/NetworkInventoryController.WorldObjectPickup.cs @@ -0,0 +1,264 @@ +#if GC2_INVENTORY +using UnityEngine; +using GameCreator.Runtime.Inventory; + +namespace Arawn.GameCreator2.Networking.Inventory +{ + public partial class NetworkInventoryController + { + // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT + public void RequestWorldObjectPickup(NetworkWorldObject worldObject, Vector2Int destinationPosition) + { + if (worldObject == null) return; + if (m_IsRemoteClient) return; + if (!m_IsLocalClient && !m_IsServer) return; + + if (!worldObject.AllowPickup || worldObject.Item == null) + { + LogPickupWarning( + $"{name}: world object pickup skipped invalid source prop={worldObject.NetworkId} allow={worldObject.AllowPickup} item={(worldObject.Item != null ? worldObject.Item.ID.String : "null")}", + this); + return; + } + + if (!TryGetLocalActorNetworkId(out uint actorNetworkId)) + { + LogPickupWarning($"{name}: world object pickup skipped no local actor network id prop={worldObject.NetworkId}", this); + return; + } + + var request = new NetworkPickupRequest + { + RequestId = GetNextRequestId(), + ActorNetworkId = actorNetworkId, + CorrelationId = NetworkCorrelation.Compose(actorNetworkId, m_LastIssuedRequestId), + PickerBagNetworkId = NetworkId, + PropNetworkId = worldObject.NetworkId, + SourceBagNetworkId = 0, + RuntimeIdHash = 0, + DestinationPosition = destinationPosition + }; + + LogPickupDebug( + $"{name}: sending world object pickup request req={request.RequestId} actor={actorNetworkId} pickerBag={NetworkId} prop={request.PropNetworkId} item={worldObject.Item.ID.String} destination={destinationPosition} server={m_IsServer} local={m_IsLocalClient}", + this); + + if (m_IsServer) + { + NetworkPickupResponse response = ProcessPickupRequest(request, NetworkId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + if (!response.Authorized) + { + OnOperationRejected?.Invoke(response.RejectionReason, "World object pickup"); + } + return; + } + + NetworkInventoryManager.Instance?.SendPickupRequest(request); + } + + // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT + private bool TryProcessWorldObjectPickupRequest( + NetworkPickupRequest request, + uint clientNetworkId, + out NetworkPickupResponse response) + { + response = default; + + if (request.PropNetworkId == 0) return false; + + if (!NetworkWorldObjectRegistry.TryGet(request.PropNetworkId, out NetworkWorldObject worldObject)) + { + if (NetworkWorldObjectRegistry.IsConsumed(request.PropNetworkId)) + { + // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT-CONSUMED-REGISTRY + LogPickupWarning( + $"{name}: world object pickup rejected consumed missing instance req={request.RequestId} prop={request.PropNetworkId} client={clientNetworkId}", + this); + response = BuildWorldObjectPickupResponse( + request, + false, + InventoryRejectionReason.InvalidOperation, + NetworkPickupFailure.WorldObjectConsumed, + default, + TBagContent.INVALID); + return true; + } + + LogPickupWarning( + $"{name}: world object pickup rejected prop not found req={request.RequestId} prop={request.PropNetworkId} client={clientNetworkId}", + this); + response = BuildWorldObjectPickupResponse( + request, + false, + InventoryRejectionReason.RuntimeItemNotFound, + NetworkPickupFailure.WorldObjectNotFound, + default, + TBagContent.INVALID); + return true; + } + + if (!worldObject.AllowPickup) + { + LogPickupWarning( + $"{name}: world object pickup rejected disabled req={request.RequestId} prop={request.PropNetworkId} allow={worldObject.AllowPickup} consumed={worldObject.IsConsumed} item={(worldObject.Item != null ? worldObject.Item.ID.String : "null")}", + worldObject); + response = BuildWorldObjectPickupResponse( + request, + false, + InventoryRejectionReason.InvalidOperation, + NetworkPickupFailure.WorldObjectPickupDisabled, + default, + TBagContent.INVALID); + return true; + } + + if (worldObject.Item == null) + { + LogPickupWarning( + $"{name}: world object pickup rejected missing item req={request.RequestId} prop={request.PropNetworkId} allow={worldObject.AllowPickup} consumed={worldObject.IsConsumed}", + worldObject); + response = BuildWorldObjectPickupResponse( + request, + false, + InventoryRejectionReason.ItemNotFound, + NetworkPickupFailure.WorldObjectItemMissing, + default, + TBagContent.INVALID); + return true; + } + + if (worldObject.IsConsumed) + { + LogPickupWarning( + $"{name}: world object pickup rejected consumed req={request.RequestId} prop={request.PropNetworkId} item={worldObject.Item.ID.String}", + worldObject); + response = BuildWorldObjectPickupResponse( + request, + false, + InventoryRejectionReason.InvalidOperation, + NetworkPickupFailure.WorldObjectConsumed, + default, + TBagContent.INVALID); + return true; + } + + float pickupDistance3D = worldObject.GetDistanceTo(transform.position); + float pickupHorizontalDistance = worldObject.GetHorizontalDistanceTo(transform.position); + + if (!worldObject.CanPickupFrom(transform.position)) + { + // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT-REJECT-DIAGNOSTICS + LogPickupWarning( + $"{name}: world object pickup rejected out of range req={request.RequestId} prop={request.PropNetworkId} pickerPosition={transform.position} propPosition={worldObject.transform.position} distance3D={pickupDistance3D} horizontalDistance={pickupHorizontalDistance} radius={worldObject.PickupRadius}", + worldObject); + response = BuildWorldObjectPickupResponse( + request, + false, + InventoryRejectionReason.InvalidPosition, + NetworkPickupFailure.WorldObjectOutOfRange, + default, + TBagContent.INVALID); + return true; + } + + RuntimeItem runtimeItem = worldObject.CreatePickupRuntimeItem(); + if (runtimeItem == null) + { + response = BuildWorldObjectPickupResponse( + request, + false, + InventoryRejectionReason.IdentityMismatch, + NetworkPickupFailure.WorldObjectRuntimeItemFailed, + default, + TBagContent.INVALID); + return true; + } + + Vector2Int finalPosition; + m_IsApplyingNetworkState = true; + try + { + if (request.DestinationPosition.x >= 0 && request.DestinationPosition.y >= 0) + { + bool added = m_Bag.Content.Add(runtimeItem, request.DestinationPosition, true); + finalPosition = added ? request.DestinationPosition : TBagContent.INVALID; + } + else + { + finalPosition = m_Bag.Content.Add(runtimeItem, true); + } + } + finally + { + m_IsApplyingNetworkState = false; + } + + if (finalPosition == TBagContent.INVALID) + { + response = BuildWorldObjectPickupResponse( + request, + false, + InventoryRejectionReason.InsufficientSpace, + NetworkPickupFailure.None, + default, + TBagContent.INVALID); + return true; + } + + worldObject.MarkPickedUp(); + TrackRuntimeItemRecursive(runtimeItem); + + NetworkRuntimeItem networkItem = ConvertToNetworkItem(runtimeItem); + var addBroadcast = new NetworkItemAddedBroadcast + { + BagNetworkId = NetworkId, + Item = networkItem, + Position = finalPosition, + StackCount = m_Bag.Content.GetContent(finalPosition)?.Count ?? 1 + }; + + NetworkInventoryManager.Instance?.BroadcastItemAdded(addBroadcast); + OnItemAdded?.Invoke(addBroadcast); + CacheCurrentSyncState(); + + LogPickupDebug( + $"{name}: world object pickup accepted req={request.RequestId} prop={request.PropNetworkId} item={DescribeRuntimeItem(runtimeItem)} finalPosition={finalPosition} distance3D={pickupDistance3D} horizontalDistance={pickupHorizontalDistance} radius={worldObject.PickupRadius}", + this); + + response = BuildWorldObjectPickupResponse( + request, + true, + InventoryRejectionReason.None, + NetworkPickupFailure.None, + networkItem, + finalPosition); + return true; + } + + // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT + private static NetworkPickupResponse BuildWorldObjectPickupResponse( + NetworkPickupRequest request, + bool authorized, + InventoryRejectionReason reason, + NetworkPickupFailure pickupFailure, + NetworkRuntimeItem item, + Vector2Int position) + { + return new NetworkPickupResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + PropNetworkId = request.PropNetworkId, + PickupFailure = pickupFailure, + Authorized = authorized, + RejectionReason = reason, + PickedUpItem = item, + PlacedPosition = position + }; + } + } +} +#endif diff --git a/NetworkInventoryController.cs b/NetworkInventoryController.cs new file mode 100644 index 0000000..4e4ea6d --- /dev/null +++ b/NetworkInventoryController.cs @@ -0,0 +1,536 @@ +#if GC2_INVENTORY +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using UnityEngine; +using GameCreator.Runtime.Common; +using GameCreator.Runtime.Inventory; + +namespace Arawn.GameCreator2.Networking.Inventory +{ + /// + /// Server-authoritative inventory controller for GC2 Bag. + /// Intercepts all inventory operations and routes through server validation. + /// + /// + /// + /// Purpose: + /// In competitive multiplayer, inventory operations MUST be server-authoritative + /// to prevent item duplication, gold exploits, and illegal crafting. + /// + /// + /// Architecture: + /// - Clients send operation requests to server + /// - Server validates and applies changes + /// - Server broadcasts confirmed changes to all clients + /// + /// + [RequireComponent(typeof(Bag))] + [AddComponentMenu("Game Creator/Network/Inventory/Network Inventory Controller")] + [DefaultExecutionOrder(ApplicationManager.EXECUTION_ORDER_DEFAULT + 5)] + public partial class NetworkInventoryController : MonoBehaviour + { + // ════════════════════════════════════════════════════════════════════════════════════════ + // INSPECTOR + // ════════════════════════════════════════════════════════════════════════════════════════ + + [Header("Network Settings")] + [Tooltip("Apply changes optimistically before server confirmation (items only).")] + [SerializeField] private bool m_OptimisticUpdates = false; + + [Tooltip("Rollback optimistic updates if server rejects.")] + [SerializeField] private bool m_RollbackOnReject = true; + + [Tooltip("Optional stable network id for scene/world bags. Leave 0 to derive one from the scene hierarchy.")] + [SerializeField] private uint m_StaticNetworkIdOverride = 0; + + [Header("Sync Settings")] + [Tooltip("Send full state sync at this interval (seconds). 0 = never.")] + [SerializeField] private float m_FullSyncInterval = 10f; + + [Tooltip("Send delta updates at this interval (seconds).")] + [SerializeField] private float m_DeltaSyncInterval = 0.2f; + + [Header("Validation")] + [Tooltip("Log rejected operations for debugging.")] + [SerializeField] private bool m_LogRejections = false; + + [Header("Debug")] + [SerializeField] private bool m_LogAllChanges = false; + + // ════════════════════════════════════════════════════════════════════════════════════════ + // EVENTS + // ════════════════════════════════════════════════════════════════════════════════════════ + + // Content events + public event Action OnContentAddRequested; + public event Action OnItemAdded; + public event Action OnContentRemoveRequested; + public event Action OnItemRemoved; + public event Action OnContentMoveRequested; + public event Action OnItemMoved; + public event Action OnContentUseRequested; + public event Action OnItemUsed; + + // Equipment events + public event Action OnEquipmentRequested; + public event Action OnItemEquipped; + public event Action OnItemUnequipped; + + // Socket events + public event Action OnSocketRequested; + public event Action OnSocketChanged; + + // Wealth events + public event Action OnWealthRequested; + public event Action OnWealthChanged; + + // Rejection event + public event Action OnOperationRejected; + + // ════════════════════════════════════════════════════════════════════════════════════════ + // PRIVATE FIELDS + // ════════════════════════════════════════════════════════════════════════════════════════ + + private Bag m_Bag; + private NetworkCharacter m_NetworkCharacter; + private uint m_CachedStaticNetworkId; + private bool m_IsApplyingNetworkState; + + // Network role + private bool m_IsServer; + private bool m_IsLocalClient; + private bool m_IsRemoteClient; + + // Request tracking + private ushort m_NextRequestId = 1; + private ushort m_LastIssuedRequestId = 1; + private static readonly List s_SharedKeyBuffer = new(16); + private static readonly List s_SharedRuntimeIdBuffer = new(16); + private readonly Dictionary m_PendingAdds = new(16); + private readonly Dictionary m_PendingRemoves = new(16); + private readonly Dictionary m_PendingMoves = new(16); + private readonly Dictionary m_PendingEquipment = new(8); + private readonly Dictionary m_PendingWealth = new(8); + private readonly Dictionary m_PendingPickupLocalRuntimeByServerRuntime = new(8); + + // State tracking for delta sync + private readonly Dictionary m_LastSyncedPositions = new(32); + private readonly Dictionary m_LastSyncedWealth = new(8); + private readonly Dictionary m_LastSyncedEquipment = new(8); + private float m_LastFullSync; + private float m_LastDeltaSync; + + // RuntimeItem ID mapping (for server-assigned IDs) + private readonly Dictionary m_RuntimeItemMap = new(64); + + private static readonly List s_Controllers = new(64); + private static readonly List s_PendingLocalRemovals = new(32); + private static readonly HashSet s_LocalDropRuntimeIds = new(); + private static readonly Dictionary s_DroppedItemInstances = new(); + private static readonly Dictionary s_ServerDroppedWorldItems = new(); + private static bool s_StaticHooksInstalled; + private static NetworkInventoryController s_LocalPlayerController; + + // ════════════════════════════════════════════════════════════════════════════════════════ + // STRUCTS + // ════════════════════════════════════════════════════════════════════════════════════════ + + private struct PendingContentAdd : ITimedPendingRequest + { + public NetworkContentAddRequest Request; + public float SentTime; + public float PendingSentTime => SentTime; + } + + private struct PendingContentRemove : ITimedPendingRequest + { + public NetworkContentRemoveRequest Request; + public RuntimeItem RemovedItem; // For rollback + public float SentTime; + public float PendingSentTime => SentTime; + } + + private struct PendingContentMove : ITimedPendingRequest + { + public NetworkContentMoveRequest Request; + public float SentTime; + public float PendingSentTime => SentTime; + } + + private struct PendingEquipment : ITimedPendingRequest + { + public NetworkEquipmentRequest Request; + public float SentTime; + public float PendingSentTime => SentTime; + } + + private struct PendingWealth : ITimedPendingRequest + { + public NetworkWealthRequest Request; + public int OriginalValue; + public float SentTime; + public float PendingSentTime => SentTime; + } + + private struct PendingLocalRemoval + { + public NetworkInventoryController SourceController; + public NetworkRuntimeItem Item; + public long RuntimeIdHash; + public float Time; + } + + private struct DroppedItemInstance + { + public GameObject Instance; + public uint SourceBagNetworkId; + public NetworkRuntimeItem Item; + public Vector3 Position; + } + + private struct ServerDroppedWorldItem + { + public uint SourceBagNetworkId; + public NetworkRuntimeItem Item; + public Vector3 Position; + public float Time; + } + + private static void LogPickupDebug(string message, UnityEngine.Object context = null) + { + if (context != null) Debug.Log($"[NetworkInventoryPickupDebug] {message}", context); + else Debug.Log($"[NetworkInventoryPickupDebug] {message}"); + } + + private static void LogPickupWarning(string message, UnityEngine.Object context = null) + { + if (context != null) Debug.LogWarning($"[NetworkInventoryPickupDebug] {message}", context); + else Debug.LogWarning($"[NetworkInventoryPickupDebug] {message}"); + } + + private static string DescribeRuntimeItem(RuntimeItem item) + { + if (item == null) return "null"; + return $"{item.ItemID.String} runtime={item.RuntimeID.String} hash={item.RuntimeID.Hash}"; + } + + private static string DescribeNetworkItem(NetworkRuntimeItem item) + { + return $"{item.ItemIdString} runtime={item.RuntimeIdString} hash={item.RuntimeIdHash} itemHash={item.ItemHash}"; + } + + // ════════════════════════════════════════════════════════════════════════════════════════ + // PROPERTIES + // ════════════════════════════════════════════════════════════════════════════════════════ + + /// The underlying GC2 Bag component. + public Bag Bag => m_Bag; + + /// Network ID of this bag's owner. + public uint NetworkId => m_NetworkCharacter != null + ? m_NetworkCharacter.NetworkId + : GetStaticNetworkId(); + + /// Whether this inventory is backed by a spawned NetworkCharacter. + public bool UsesNetworkCharacterId => m_NetworkCharacter != null; + + /// Whether this inventory is a scene/world bag such as a chest. + public bool IsWorldInventory => m_NetworkCharacter == null; + + /// Whether this is running on the server. + public bool IsServer => m_IsServer; + + /// Whether this is the local player's inventory. + public bool IsLocalClient => m_IsLocalClient; + + public bool OptimisticUpdates => m_OptimisticUpdates; + + public bool RollbackOnReject => m_RollbackOnReject; + + // ════════════════════════════════════════════════════════════════════════════════════════ + // UNITY LIFECYCLE + // ════════════════════════════════════════════════════════════════════════════════════════ + + private void Awake() + { + m_Bag = GetComponent(); + m_NetworkCharacter = GetComponent(); + } + + private void Start() + { + // Subscribe to GC2 events for local change detection + SubscribeToBagEvents(); + } + + private void OnDestroy() + { + UnsubscribeFromBagEvents(); + } + + private void Update() + { + if (!m_IsServer && !m_IsLocalClient) return; + + float currentTime = Time.time; + + // Server-side sync + if (m_IsServer) + { + if (m_FullSyncInterval > 0 && currentTime - m_LastFullSync > m_FullSyncInterval) + { + BroadcastFullState(); + m_LastFullSync = currentTime; + } + + if (m_DeltaSyncInterval > 0 && currentTime - m_LastDeltaSync > m_DeltaSyncInterval) + { + BroadcastDeltaState(); + m_LastDeltaSync = currentTime; + } + } + + CleanupPendingRequests(); + } + + // ════════════════════════════════════════════════════════════════════════════════════════ + // INITIALIZATION + // ════════════════════════════════════════════════════════════════════════════════════════ + + /// + /// Initialize the network inventory controller with role information. + /// + public void Initialize(bool isServer, bool isLocalClient) + { + m_IsServer = isServer; + m_IsLocalClient = isLocalClient; + m_IsRemoteClient = !isServer && !isLocalClient; + + if (m_IsLocalClient && UsesNetworkCharacterId && NetworkId != 0) + { + s_LocalPlayerController = this; + } + + InitializeStateTracking(); + + if (IsWorldInventory) + { + LogPickupDebug( + $"{name}: world inventory initialized networkId={NetworkId} path={BuildStableScenePath(transform)} server={m_IsServer} local={m_IsLocalClient} remote={m_IsRemoteClient} trackedItems={m_RuntimeItemMap.Count}", + this); + } + + if (m_LogAllChanges) + { + string role = m_IsServer ? "Server" : (m_IsLocalClient ? "LocalClient" : "RemoteClient"); + Debug.Log($"[NetworkInventoryController] {gameObject.name} initialized as {role}"); + } + } + + private void InitializeStateTracking() + { + // Build RuntimeItem map + m_RuntimeItemMap.Clear(); + foreach (var cell in m_Bag.Content.CellList) + { + if (cell == null || cell.Available) continue; + + foreach (var runtimeIdEntry in cell.List) + { + var runtimeItem = m_Bag.Content.GetRuntimeItem(runtimeIdEntry); + if (runtimeItem != null) + { + TrackRuntimeItemRecursive(runtimeItem); + } + } + } + + // Cache initial wealth + foreach (var currencyId in m_Bag.Wealth.List) + { + m_LastSyncedWealth[currencyId.Hash] = m_Bag.Wealth.Get(currencyId); + } + + CacheCurrentSyncState(); + } + + private void SubscribeToBagEvents() + { + if (!s_Controllers.Contains(this)) + { + s_Controllers.Add(this); + } + + InstallStaticInventoryHooks(); + + if (m_Bag.Content != null) + { + m_Bag.Content.EventAdd += OnLocalItemAdded; + m_Bag.Content.EventRemove += OnLocalItemRemoved; + m_Bag.Content.EventUse += OnLocalItemUsed; + } + + if (m_Bag.Equipment != null) + { + m_Bag.Equipment.EventEquip += OnLocalItemEquipped; + m_Bag.Equipment.EventUnequip += OnLocalItemUnequipped; + } + + if (m_Bag.Wealth != null) + { + m_Bag.Wealth.EventChange += OnLocalWealthChanged; + } + } + + private void UnsubscribeFromBagEvents() + { + s_Controllers.Remove(this); + if (s_LocalPlayerController == this) + { + s_LocalPlayerController = null; + } + + UninstallStaticInventoryHooksIfUnused(); + + if (m_Bag != null) + { + if (m_Bag.Content != null) + { + m_Bag.Content.EventAdd -= OnLocalItemAdded; + m_Bag.Content.EventRemove -= OnLocalItemRemoved; + m_Bag.Content.EventUse -= OnLocalItemUsed; + } + + if (m_Bag.Equipment != null) + { + m_Bag.Equipment.EventEquip -= OnLocalItemEquipped; + m_Bag.Equipment.EventUnequip -= OnLocalItemUnequipped; + } + + if (m_Bag.Wealth != null) + { + m_Bag.Wealth.EventChange -= OnLocalWealthChanged; + } + } + } + + private ushort GetNextRequestId() + { + if (m_NextRequestId == 0) + { + m_NextRequestId = 1; + } + + ushort requestId = m_NextRequestId; + m_NextRequestId++; + if (m_NextRequestId == 0) + { + m_NextRequestId = 1; + } + + m_LastIssuedRequestId = requestId; + return requestId; + } + + private static ulong GetPendingKey(uint actorNetworkId, uint correlationId, ushort requestId) + { + uint pendingCorrelation = correlationId != 0 ? correlationId : requestId; + return ((ulong)actorNetworkId << 32) | pendingCorrelation; + } + + private uint GetStaticNetworkId() + { + if (m_StaticNetworkIdOverride != 0) return m_StaticNetworkIdOverride; + if (m_CachedStaticNetworkId != 0) return m_CachedStaticNetworkId; + + string path = BuildStableScenePath(transform); + uint hash = 2166136261u; + for (int i = 0; i < path.Length; i++) + { + hash ^= path[i]; + hash *= 16777619u; + } + + m_CachedStaticNetworkId = 0x80000000u | (hash & 0x7FFFFFFFu); + if (m_CachedStaticNetworkId == 0) m_CachedStaticNetworkId = 0x80000001u; + return m_CachedStaticNetworkId; + } + + private static string BuildStableScenePath(Transform target) + { + if (target == null) return string.Empty; + + string scenePath = target.gameObject.scene.path; + if (string.IsNullOrEmpty(scenePath)) scenePath = target.gameObject.scene.name; + + string path = BuildStableScenePathSegment(target); + Transform current = target; + while (current.parent != null) + { + current = current.parent; + path = $"{BuildStableScenePathSegment(current)}/{path}"; + } + + return $"{scenePath}:{path}"; + } + + private static string BuildStableScenePathSegment(Transform target) + { + int sameNameIndex = 0; + Transform parent = target.parent; + if (parent != null) + { + for (int i = 0; i < parent.childCount; i++) + { + Transform sibling = parent.GetChild(i); + if (sibling == target) break; + if (sibling != null && sibling.name == target.name) + { + sameNameIndex++; + } + } + } + else if (target.gameObject.scene.IsValid()) + { + GameObject[] roots = target.gameObject.scene.GetRootGameObjects(); + for (int i = 0; i < roots.Length; i++) + { + GameObject root = roots[i]; + if (root == null) continue; + if (root.transform == target) break; + if (root.name == target.name) + { + sameNameIndex++; + } + } + } + + return $"{target.name}[{sameNameIndex}]"; + } + + private static void InstallStaticInventoryHooks() + { + if (s_StaticHooksInstalled) return; + + RuntimeSockets.EventAttachRuntimeItem -= HandleGlobalSocketAttached; + RuntimeSockets.EventAttachRuntimeItem += HandleGlobalSocketAttached; + RuntimeSockets.EventDetachRuntimeItem -= HandleGlobalSocketDetached; + RuntimeSockets.EventDetachRuntimeItem += HandleGlobalSocketDetached; + Item.EventInstantiate -= HandleGlobalItemInstantiated; + Item.EventInstantiate += HandleGlobalItemInstantiated; + s_StaticHooksInstalled = true; + } + + private static void UninstallStaticInventoryHooksIfUnused() + { + if (!s_StaticHooksInstalled || s_Controllers.Count > 0) return; + + RuntimeSockets.EventAttachRuntimeItem -= HandleGlobalSocketAttached; + RuntimeSockets.EventDetachRuntimeItem -= HandleGlobalSocketDetached; + Item.EventInstantiate -= HandleGlobalItemInstantiated; + s_StaticHooksInstalled = false; + } + } +} +#endif diff --git a/NetworkInventoryManager.cs b/NetworkInventoryManager.cs new file mode 100644 index 0000000..e2468f5 --- /dev/null +++ b/NetworkInventoryManager.cs @@ -0,0 +1,1815 @@ +#if GC2_INVENTORY +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using UnityEngine; +using Arawn.GameCreator2.Networking; +using Arawn.GameCreator2.Networking.Security; + +namespace Arawn.GameCreator2.Networking.Inventory +{ + /// + /// Global manager for inventory network communication. + /// Transport-agnostic - wire up delegates to your networking solution. + /// + [AddComponentMenu("Game Creator/Network/Inventory/Network Inventory Manager")] + public class NetworkInventoryManager : NetworkSingleton + { + // ════════════════════════════════════════════════════════════════════════════════════════ + // SINGLETON (lazy-find override) + // ════════════════════════════════════════════════════════════════════════════════════════ + + /// Singleton instance. Falls back to FindFirstObjectByType if not yet assigned. + public new static NetworkInventoryManager Instance + { + get + { + if (s_Instance == null) + s_Instance = FindFirstObjectByType(); + return s_Instance; + } + } + + // ════════════════════════════════════════════════════════════════════════════════════════ + // TRANSPORT DELEGATES - Wire to your networking solution + // ════════════════════════════════════════════════════════════════════════════════════════ + + // ───────────────────────────────────────────────────────────────────────────────────────── + // CLIENT → SERVER: Content Operations + // ───────────────────────────────────────────────────────────────────────────────────────── + + public Action OnSendContentAddRequest; + public Action OnSendContentRemoveRequest; + public Action OnSendContentMoveRequest; + public Action OnSendContentUseRequest; + public Action OnSendContentDropRequest; + + // ───────────────────────────────────────────────────────────────────────────────────────── + // CLIENT → SERVER: Equipment Operations + // ───────────────────────────────────────────────────────────────────────────────────────── + + public Action OnSendEquipmentRequest; + + // ───────────────────────────────────────────────────────────────────────────────────────── + // CLIENT → SERVER: Socket Operations + // ───────────────────────────────────────────────────────────────────────────────────────── + + public Action OnSendSocketRequest; + + // ───────────────────────────────────────────────────────────────────────────────────────── + // CLIENT → SERVER: Wealth Operations + // ───────────────────────────────────────────────────────────────────────────────────────── + + public Action OnSendWealthRequest; + + // ───────────────────────────────────────────────────────────────────────────────────────── + // CLIENT → SERVER: Merchant Operations + // ───────────────────────────────────────────────────────────────────────────────────────── + + public Action OnSendMerchantRequest; + + // ───────────────────────────────────────────────────────────────────────────────────────── + // CLIENT → SERVER: Crafting Operations + // ───────────────────────────────────────────────────────────────────────────────────────── + + public Action OnSendCraftingRequest; + + // ───────────────────────────────────────────────────────────────────────────────────────── + // CLIENT → SERVER: Transfer Operations + // ───────────────────────────────────────────────────────────────────────────────────────── + + public Action OnSendTransferRequest; + public Action OnSendPickupRequest; + // [LOCAL-EDIT] #PILFER-INVENTORY-SERVER-LOOT + public Action OnSendLootRequest; + public Action OnSendCombineRequest; + + // ───────────────────────────────────────────────────────────────────────────────────────── + // SERVER → CLIENT: Responses (Single target) + // ───────────────────────────────────────────────────────────────────────────────────────── + + public Action OnSendContentAddResponse; + public Action OnSendContentRemoveResponse; + public Action OnSendContentMoveResponse; + public Action OnSendContentUseResponse; + public Action OnSendContentDropResponse; + public Action OnSendEquipmentResponse; + public Action OnSendSocketResponse; + public Action OnSendWealthResponse; + public Action OnSendMerchantResponse; + public Action OnSendCraftingResponse; + public Action OnSendTransferResponse; + public Action OnSendPickupResponse; + // [LOCAL-EDIT] #PILFER-INVENTORY-SERVER-LOOT + public Action OnSendLootResponse; + public Action OnSendCombineResponse; + + // ───────────────────────────────────────────────────────────────────────────────────────── + // SERVER → ALL CLIENTS: Broadcasts + // ───────────────────────────────────────────────────────────────────────────────────────── + + public Action OnBroadcastItemAdded; + public Action OnBroadcastItemRemoved; + public Action OnBroadcastItemDropped; + public Action OnBroadcastDroppedItemRemoved; + public Action OnBroadcastItemMoved; + public Action OnBroadcastItemUsed; + public Action OnBroadcastItemEquipped; + public Action OnBroadcastItemUnequipped; + public Action OnBroadcastSocketChange; + public Action OnBroadcastWealthChange; + public Action OnBroadcastPropertyChange; + public Action OnBroadcastFullSnapshot; + public Action OnBroadcastDelta; + + // ───────────────────────────────────────────────────────────────────────────────────────── + // SERVER → SINGLE CLIENT: Targeted + // ───────────────────────────────────────────────────────────────────────────────────────── + + public Action OnSendSnapshotToClient; + + // ════════════════════════════════════════════════════════════════════════════════════════ + // INSPECTOR + // ════════════════════════════════════════════════════════════════════════════════════════ + + [Header("Settings")] + [SerializeField] private bool m_IsServer; + + [Header("Validation")] + [SerializeField] private int m_MaxPendingRequestsPerPlayer = 50; + [SerializeField] private float m_RequestTimeout = 5f; + + [Header("Debug")] + [SerializeField] private bool m_LogNetworkMessages = false; + + // ════════════════════════════════════════════════════════════════════════════════════════ + // PRIVATE FIELDS + // ════════════════════════════════════════════════════════════════════════════════════════ + + private readonly Dictionary m_Controllers = new(32); + private readonly Dictionary m_PendingRequestCounts = new(32); + private NetworkInventoryPatchHooks m_PatchHooks; + + // Merchant controllers (separate from player bags) + private readonly Dictionary m_MerchantControllers = new(8); + + // ════════════════════════════════════════════════════════════════════════════════════════ + // PROPERTIES + // ════════════════════════════════════════════════════════════════════════════════════════ + + public bool IsServer + { + get => m_IsServer; + set + { + m_IsServer = value; + SecurityIntegration.SetModuleServerContext("Inventory", m_IsServer); + SecurityIntegration.EnsureSecurityManagerInitialized(m_IsServer, ResolveSecurityTimeProvider); + SyncPatchHooks(); + if (m_IsServer) RefreshOwnedEntityMappings(); + } + } + + public int ControllerCount => m_Controllers.Count; + + public float RequestTimeoutSeconds => m_RequestTimeout; + + // ════════════════════════════════════════════════════════════════════════════════════════ + // UNITY LIFECYCLE + // ════════════════════════════════════════════════════════════════════════════════════════ + private void OnEnable() + { + SecurityIntegration.SetModuleServerContext("Inventory", m_IsServer); + SecurityIntegration.EnsureSecurityManagerInitialized(m_IsServer, ResolveSecurityTimeProvider); + SyncPatchHooks(); + } + + private void OnDisable() + { + SecurityIntegration.SetModuleServerContext("Inventory", false); + if (m_PatchHooks != null) + { + m_PatchHooks.Initialize(false); + } + } + + + // ════════════════════════════════════════════════════════════════════════════════════════ + // REGISTRATION + // ════════════════════════════════════════════════════════════════════════════════════════ + + public void RegisterController(uint networkId, NetworkInventoryController controller) + { + if (controller == null) return; + m_Controllers[networkId] = controller; + RegisterOwnedEntityMapping(networkId); + + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Registered inventory controller: NetworkId={networkId}"); + } + + public void UnregisterController(uint networkId) + { + bool removed = m_Controllers.Remove(networkId); + if (removed) + { + SecurityIntegration.UnregisterEntity(networkId); + } + + if (removed && m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Unregistered inventory controller: NetworkId={networkId}"); + } + + public NetworkInventoryController GetController(uint networkId) + { + return m_Controllers.TryGetValue(networkId, out var controller) ? controller : null; + } + + private NetworkInventoryController GetControllerOrFallback(uint networkId, string operation) + { + NetworkInventoryController controller = GetController(networkId); + if (controller != null) return controller; + + foreach (var entry in m_Controllers) + { + if (entry.Value == null) continue; + + Debug.LogWarning( + $"[NetworkInventoryPickupDebug][Manager] {operation} using fallback controller because bag={networkId} is not registered locally. fallbackBag={entry.Key}"); + return entry.Value; + } + + Debug.LogWarning( + $"[NetworkInventoryPickupDebug][Manager] {operation} ignored because bag={networkId} is not registered locally and no fallback controller exists"); + return null; + } + + public void RegisterMerchantController(uint networkId, NetworkMerchantController controller) + { + if (controller == null) return; + m_MerchantControllers[networkId] = controller; + } + + public void UnregisterMerchantController(uint networkId) + { + m_MerchantControllers.Remove(networkId); + } + + public NetworkMerchantController GetMerchantController(uint networkId) + { + return m_MerchantControllers.TryGetValue(networkId, out var controller) ? controller : null; + } + + // ════════════════════════════════════════════════════════════════════════════════════════ + // CLIENT → SERVER: SENDING REQUESTS + // ════════════════════════════════════════════════════════════════════════════════════════ + + #region Send Requests + + public void SendContentAddRequest(NetworkContentAddRequest request) + { + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Sending add request: RequestId={request.RequestId}"); + OnSendContentAddRequest?.Invoke(request); + } + + public void SendContentRemoveRequest(NetworkContentRemoveRequest request) + { + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Sending remove request: RequestId={request.RequestId}"); + OnSendContentRemoveRequest?.Invoke(request); + } + + public void SendContentMoveRequest(NetworkContentMoveRequest request) + { + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Sending move request: RequestId={request.RequestId}"); + OnSendContentMoveRequest?.Invoke(request); + } + + public void SendContentUseRequest(NetworkContentUseRequest request) + { + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Sending use request: RequestId={request.RequestId}"); + OnSendContentUseRequest?.Invoke(request); + } + + public void SendContentDropRequest(NetworkContentDropRequest request) + { + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Sending drop request: RequestId={request.RequestId}"); + OnSendContentDropRequest?.Invoke(request); + } + + public void SendEquipmentRequest(NetworkEquipmentRequest request) + { + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Sending equipment request: RequestId={request.RequestId}, Action={request.Action}"); + OnSendEquipmentRequest?.Invoke(request); + } + + public void SendSocketRequest(NetworkSocketRequest request) + { + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Sending socket request: RequestId={request.RequestId}, Action={request.Action}"); + OnSendSocketRequest?.Invoke(request); + } + + public void SendWealthRequest(NetworkWealthRequest request) + { + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Sending wealth request: RequestId={request.RequestId}, Action={request.Action}"); + OnSendWealthRequest?.Invoke(request); + } + + public void SendMerchantRequest(NetworkMerchantRequest request) + { + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Sending merchant request: RequestId={request.RequestId}, Action={request.Action}"); + OnSendMerchantRequest?.Invoke(request); + } + + public void SendCraftingRequest(NetworkCraftingRequest request) + { + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Sending crafting request: RequestId={request.RequestId}, Action={request.Action}"); + OnSendCraftingRequest?.Invoke(request); + } + + public void SendTransferRequest(NetworkTransferRequest request) + { + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Sending transfer request: RequestId={request.RequestId}"); + OnSendTransferRequest?.Invoke(request); + } + + public void SendPickupRequest(NetworkPickupRequest request) + { + Debug.Log( + $"[NetworkInventoryPickupDebug][Manager] send pickup request req={request.RequestId} actor={request.ActorNetworkId} pickerBag={request.PickerBagNetworkId} sourceBag={request.SourceBagNetworkId} runtime={request.RuntimeIdHash} destination={request.DestinationPosition}"); + OnSendPickupRequest?.Invoke(request); + } + + // [LOCAL-EDIT] #PILFER-INVENTORY-SERVER-LOOT + public void SendLootRequest(NetworkLootRequest request) + { + Debug.Log( + $"[NetworkInventoryLootDebug][Manager] send loot request req={request.RequestId} actor={request.ActorNetworkId} container={request.ContainerBagNetworkId}"); + OnSendLootRequest?.Invoke(request); + } + + public void SendCombineRequest(NetworkCombineRequest request) + { + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Sending combine request: RequestId={request.RequestId}"); + OnSendCombineRequest?.Invoke(request); + } + + #endregion + + // ════════════════════════════════════════════════════════════════════════════════════════ + // SERVER: RECEIVING REQUESTS + // ════════════════════════════════════════════════════════════════════════════════════════ + + #region Receive Requests (Server) + + private static uint GetSenderClientId(ulong clientId) + { + return NetworkTransportBridge.TryConvertSenderClientId(clientId, out uint senderClientId) + ? senderClientId + : NetworkTransportBridge.InvalidClientId; + } + + private static NetworkRequestContext BuildContext(uint actorNetworkId, uint correlationId) + { + return NetworkRequestContext.Create(actorNetworkId, correlationId); + } + + private static InventoryRejectionReason GetSecurityRejection(uint actorNetworkId, uint correlationId) + { + return SecurityIntegration.IsProtocolContextMismatch(actorNetworkId, correlationId) + ? InventoryRejectionReason.ProtocolMismatch + : InventoryRejectionReason.SecurityViolation; + } + + private void RegisterOwnedEntityMapping(uint entityNetworkId) + { + if (!m_IsServer || entityNetworkId == 0) return; + + SecurityIntegration.RegisterEntityActor(entityNetworkId, entityNetworkId); + + var bridge = NetworkTransportBridge.Active; + if (bridge != null && + bridge.TryGetCharacterOwner(entityNetworkId, out uint ownerClientId) && + NetworkTransportBridge.IsValidClientId(ownerClientId)) + { + SecurityIntegration.RegisterEntityOwner(entityNetworkId, ownerClientId); + } + } + + private void RefreshOwnedEntityMappings() + { + foreach (var kvp in m_Controllers) + { + RegisterOwnedEntityMapping(kvp.Key); + } + } + + private bool ValidateTargetOwnership(uint senderClientId, uint actorNetworkId, uint targetBagNetworkId, string requestType) + { + NetworkInventoryController targetController = GetController(targetBagNetworkId); + if (targetController != null && targetController.IsWorldInventory) + { + return true; + } + + return SecurityIntegration.ValidateTargetEntityOwnership( + senderClientId, + actorNetworkId, + targetBagNetworkId, + "Inventory", + requestType); + } + + private static float ResolveSecurityTimeProvider() + { + var bridge = NetworkTransportBridge.Active; + return bridge != null && bridge.IsServer ? bridge.ServerTime : Time.time; + } + + private void SyncPatchHooks() + { + if (!m_IsServer) + { + if (m_PatchHooks != null) m_PatchHooks.Initialize(false); + return; + } + + if (m_PatchHooks == null) + { + m_PatchHooks = GetComponent(); + if (m_PatchHooks == null) + { + m_PatchHooks = gameObject.AddComponent(); + } + } + + m_PatchHooks.Initialize(true); + } + + public void ReceiveContentAddRequest(NetworkContentAddRequest request, ulong clientId) + { + if (!m_IsServer) return; + uint senderClientId = GetSenderClientId(clientId); + if (!SecurityIntegration.ValidateModuleRequest( + senderClientId, + BuildContext(request.ActorNetworkId, request.CorrelationId), + "Inventory", + nameof(NetworkContentAddRequest))) + { + SendContentAddResponse(senderClientId, new NetworkContentAddResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = GetSecurityRejection(request.ActorNetworkId, request.CorrelationId) + }); + return; + } + if (!ValidateTargetOwnership(senderClientId, request.ActorNetworkId, request.TargetBagNetworkId, nameof(NetworkContentAddRequest))) + { + SendContentAddResponse(senderClientId, new NetworkContentAddResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.SecurityViolation + }); + return; + } + if (!CheckRateLimit(clientId)) + { + SendContentAddResponse(senderClientId, new NetworkContentAddResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.RateLimitExceeded + }); + return; + } + + try + { + var controller = GetController(request.TargetBagNetworkId); + if (controller == null) + { + SendContentAddResponse(senderClientId, new NetworkContentAddResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.BagNotFound + }); + return; + } + + var response = controller.ProcessContentAddRequest(request, senderClientId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + SendContentAddResponse(senderClientId, response); + } + finally + { + DecrementPendingRequests(clientId); + } + } + + public void ReceiveContentRemoveRequest(NetworkContentRemoveRequest request, ulong clientId) + { + if (!m_IsServer) return; + uint senderClientId = GetSenderClientId(clientId); + if (!SecurityIntegration.ValidateModuleRequest( + senderClientId, + BuildContext(request.ActorNetworkId, request.CorrelationId), + "Inventory", + nameof(NetworkContentRemoveRequest))) + { + SendContentRemoveResponse(senderClientId, new NetworkContentRemoveResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = GetSecurityRejection(request.ActorNetworkId, request.CorrelationId) + }); + return; + } + if (!ValidateTargetOwnership(senderClientId, request.ActorNetworkId, request.TargetBagNetworkId, nameof(NetworkContentRemoveRequest))) + { + SendContentRemoveResponse(senderClientId, new NetworkContentRemoveResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.SecurityViolation + }); + return; + } + if (!CheckRateLimit(clientId)) + { + SendContentRemoveResponse(senderClientId, new NetworkContentRemoveResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.RateLimitExceeded + }); + return; + } + + try + { + var controller = GetController(request.TargetBagNetworkId); + if (controller == null) + { + SendContentRemoveResponse(senderClientId, new NetworkContentRemoveResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.BagNotFound + }); + return; + } + + var response = controller.ProcessContentRemoveRequest(request, senderClientId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + SendContentRemoveResponse(senderClientId, response); + } + finally + { + DecrementPendingRequests(clientId); + } + } + + public void ReceiveContentMoveRequest(NetworkContentMoveRequest request, ulong clientId) + { + if (!m_IsServer) return; + uint senderClientId = GetSenderClientId(clientId); + if (!SecurityIntegration.ValidateModuleRequest( + senderClientId, + BuildContext(request.ActorNetworkId, request.CorrelationId), + "Inventory", + nameof(NetworkContentMoveRequest))) + { + SendContentMoveResponse(senderClientId, new NetworkContentMoveResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = GetSecurityRejection(request.ActorNetworkId, request.CorrelationId) + }); + return; + } + if (!ValidateTargetOwnership(senderClientId, request.ActorNetworkId, request.TargetBagNetworkId, nameof(NetworkContentMoveRequest))) + { + SendContentMoveResponse(senderClientId, new NetworkContentMoveResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.SecurityViolation + }); + return; + } + if (!CheckRateLimit(clientId)) + { + SendContentMoveResponse(senderClientId, new NetworkContentMoveResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.RateLimitExceeded + }); + return; + } + + try + { + var controller = GetController(request.TargetBagNetworkId); + if (controller == null) + { + SendContentMoveResponse(senderClientId, new NetworkContentMoveResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.BagNotFound + }); + return; + } + + var response = controller.ProcessContentMoveRequest(request, senderClientId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + SendContentMoveResponse(senderClientId, response); + } + finally + { + DecrementPendingRequests(clientId); + } + } + + public void ReceiveContentUseRequest(NetworkContentUseRequest request, ulong clientId) + { + if (!m_IsServer) return; + uint senderClientId = GetSenderClientId(clientId); + if (!SecurityIntegration.ValidateModuleRequest( + senderClientId, + BuildContext(request.ActorNetworkId, request.CorrelationId), + "Inventory", + nameof(NetworkContentUseRequest))) + { + SendContentUseResponse(senderClientId, new NetworkContentUseResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = GetSecurityRejection(request.ActorNetworkId, request.CorrelationId) + }); + return; + } + if (!ValidateTargetOwnership(senderClientId, request.ActorNetworkId, request.TargetBagNetworkId, nameof(NetworkContentUseRequest))) + { + SendContentUseResponse(senderClientId, new NetworkContentUseResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.SecurityViolation + }); + return; + } + if (!CheckRateLimit(clientId)) + { + SendContentUseResponse(senderClientId, new NetworkContentUseResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.RateLimitExceeded + }); + return; + } + + try + { + var controller = GetController(request.TargetBagNetworkId); + if (controller == null) + { + SendContentUseResponse(senderClientId, new NetworkContentUseResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.BagNotFound + }); + return; + } + + var response = controller.ProcessContentUseRequest(request, senderClientId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + SendContentUseResponse(senderClientId, response); + } + finally + { + DecrementPendingRequests(clientId); + } + } + + public void ReceiveContentDropRequest(NetworkContentDropRequest request, ulong clientId) + { + if (!m_IsServer) return; + uint senderClientId = GetSenderClientId(clientId); + if (!SecurityIntegration.ValidateModuleRequest( + senderClientId, + BuildContext(request.ActorNetworkId, request.CorrelationId), + "Inventory", + nameof(NetworkContentDropRequest))) + { + SendContentDropResponse(senderClientId, new NetworkContentDropResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = GetSecurityRejection(request.ActorNetworkId, request.CorrelationId) + }); + return; + } + if (!ValidateTargetOwnership(senderClientId, request.ActorNetworkId, request.TargetBagNetworkId, nameof(NetworkContentDropRequest))) + { + SendContentDropResponse(senderClientId, new NetworkContentDropResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.SecurityViolation + }); + return; + } + if (!CheckRateLimit(clientId)) + { + SendContentDropResponse(senderClientId, new NetworkContentDropResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.RateLimitExceeded + }); + return; + } + + try + { + var controller = GetController(request.TargetBagNetworkId); + if (controller == null) + { + SendContentDropResponse(senderClientId, new NetworkContentDropResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.BagNotFound + }); + return; + } + + var response = controller.ProcessContentDropRequest(request, senderClientId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + SendContentDropResponse(senderClientId, response); + } + finally + { + DecrementPendingRequests(clientId); + } + } + + public async Task ReceiveEquipmentRequest(NetworkEquipmentRequest request, ulong clientId) + { + if (!m_IsServer) return; + uint senderClientId = GetSenderClientId(clientId); + if (!SecurityIntegration.ValidateModuleRequest( + senderClientId, + BuildContext(request.ActorNetworkId, request.CorrelationId), + "Inventory", + nameof(NetworkEquipmentRequest))) + { + SendEquipmentResponse(senderClientId, new NetworkEquipmentResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = GetSecurityRejection(request.ActorNetworkId, request.CorrelationId) + }); + return; + } + if (!ValidateTargetOwnership(senderClientId, request.ActorNetworkId, request.TargetBagNetworkId, nameof(NetworkEquipmentRequest))) + { + SendEquipmentResponse(senderClientId, new NetworkEquipmentResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.SecurityViolation + }); + return; + } + if (!CheckRateLimit(clientId)) + { + SendEquipmentResponse(senderClientId, new NetworkEquipmentResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.RateLimitExceeded + }); + return; + } + + try + { + var controller = GetController(request.TargetBagNetworkId); + if (controller == null) + { + SendEquipmentResponse(senderClientId, new NetworkEquipmentResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.BagNotFound + }); + return; + } + + try + { + var response = await controller.ProcessEquipmentRequest(request, senderClientId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + SendEquipmentResponse(senderClientId, response); + } + catch (Exception ex) + { + Debug.LogError($"[NetworkInventory] ReceiveEquipmentRequest failed: {ex.Message}\n{ex.StackTrace}"); + SendEquipmentResponse(senderClientId, new NetworkEquipmentResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.InternalError + }); + } + } + finally + { + DecrementPendingRequests(clientId); + } + } + + public void ReceiveSocketRequest(NetworkSocketRequest request, ulong clientId) + { + if (!m_IsServer) return; + uint senderClientId = GetSenderClientId(clientId); + if (!SecurityIntegration.ValidateModuleRequest( + senderClientId, + BuildContext(request.ActorNetworkId, request.CorrelationId), + "Inventory", + nameof(NetworkSocketRequest))) + { + SendSocketResponse(senderClientId, new NetworkSocketResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = GetSecurityRejection(request.ActorNetworkId, request.CorrelationId) + }); + return; + } + if (!ValidateTargetOwnership(senderClientId, request.ActorNetworkId, request.TargetBagNetworkId, nameof(NetworkSocketRequest))) + { + SendSocketResponse(senderClientId, new NetworkSocketResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.SecurityViolation + }); + return; + } + if (!CheckRateLimit(clientId)) + { + SendSocketResponse(senderClientId, new NetworkSocketResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.RateLimitExceeded + }); + return; + } + + try + { + var controller = GetController(request.TargetBagNetworkId); + if (controller == null) + { + SendSocketResponse(senderClientId, new NetworkSocketResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.BagNotFound + }); + return; + } + + var response = controller.ProcessSocketRequest(request, senderClientId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + SendSocketResponse(senderClientId, response); + } + finally + { + DecrementPendingRequests(clientId); + } + } + + public void ReceiveWealthRequest(NetworkWealthRequest request, ulong clientId) + { + if (!m_IsServer) return; + uint senderClientId = GetSenderClientId(clientId); + if (!SecurityIntegration.ValidateModuleRequest( + senderClientId, + BuildContext(request.ActorNetworkId, request.CorrelationId), + "Inventory", + nameof(NetworkWealthRequest))) + { + SendWealthResponse(senderClientId, new NetworkWealthResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = GetSecurityRejection(request.ActorNetworkId, request.CorrelationId) + }); + return; + } + if (!ValidateTargetOwnership(senderClientId, request.ActorNetworkId, request.TargetBagNetworkId, nameof(NetworkWealthRequest))) + { + SendWealthResponse(senderClientId, new NetworkWealthResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.SecurityViolation + }); + return; + } + if (!CheckRateLimit(clientId)) + { + SendWealthResponse(senderClientId, new NetworkWealthResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.RateLimitExceeded + }); + return; + } + + try + { + var controller = GetController(request.TargetBagNetworkId); + if (controller == null) + { + SendWealthResponse(senderClientId, new NetworkWealthResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.BagNotFound + }); + return; + } + + var response = controller.ProcessWealthRequest(request, senderClientId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + SendWealthResponse(senderClientId, response); + } + finally + { + DecrementPendingRequests(clientId); + } + } + + public void ReceiveTransferRequest(NetworkTransferRequest request, ulong clientId) + { + if (!m_IsServer) return; + uint senderClientId = GetSenderClientId(clientId); + NetworkInventoryController sourceController = GetController(request.SourceBagNetworkId); + NetworkInventoryController destinationController = GetController(request.DestinationBagNetworkId); + Debug.Log( + $"[NetworkInventoryPickupDebug][Manager] receive transfer request req={request.RequestId} senderConnection={clientId} senderClient={senderClientId} actor={request.ActorNetworkId} sourceBag={request.SourceBagNetworkId} sourceFound={sourceController != null} sourceWorld={(sourceController != null && sourceController.IsWorldInventory)} destinationBag={request.DestinationBagNetworkId} destinationFound={destinationController != null} destinationWorld={(destinationController != null && destinationController.IsWorldInventory)} runtime={request.RuntimeIdHash} destination={request.DestinationPosition}"); + + if (!SecurityIntegration.ValidateModuleRequest( + senderClientId, + BuildContext(request.ActorNetworkId, request.CorrelationId), + "Inventory", + nameof(NetworkTransferRequest))) + { + SendTransferResponse(senderClientId, new NetworkTransferResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = GetSecurityRejection(request.ActorNetworkId, request.CorrelationId) + }); + return; + } + + bool sourceAuthorized = ValidateTargetOwnership( + senderClientId, + request.ActorNetworkId, + request.SourceBagNetworkId, + nameof(NetworkTransferRequest)); + + bool destinationAuthorized = ValidateTargetOwnership( + senderClientId, + request.ActorNetworkId, + request.DestinationBagNetworkId, + nameof(NetworkTransferRequest)); + + if (!sourceAuthorized || !destinationAuthorized) + { + Debug.LogWarning( + $"[NetworkInventoryPickupDebug][Manager] transfer rejected by ownership req={request.RequestId} senderClient={senderClientId} actor={request.ActorNetworkId} sourceBag={request.SourceBagNetworkId} sourceAuthorized={sourceAuthorized} destinationBag={request.DestinationBagNetworkId} destinationAuthorized={destinationAuthorized}"); + + SendTransferResponse(senderClientId, new NetworkTransferResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.SecurityViolation + }); + return; + } + + // TODO: Support authorized world/container-to-world/container reorganization once container access locks and concurrent looting rules are defined. + if (sourceController != null && destinationController != null && + sourceController.IsWorldInventory && destinationController.IsWorldInventory) + { + Debug.LogWarning( + $"[NetworkInventoryPickupDebug][Manager] transfer rejected world-to-world deferred req={request.RequestId} sourceBag={request.SourceBagNetworkId} destinationBag={request.DestinationBagNetworkId}"); + + SendTransferResponse(senderClientId, new NetworkTransferResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.InvalidOperation + }); + return; + } + + if (!CheckRateLimit(clientId)) + { + SendTransferResponse(senderClientId, new NetworkTransferResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.RateLimitExceeded + }); + return; + } + + try + { + NetworkInventoryController source = GetController(request.SourceBagNetworkId); + NetworkInventoryController destination = GetController(request.DestinationBagNetworkId); + if (source == null || destination == null) + { + Debug.LogWarning( + $"[NetworkInventoryPickupDebug][Manager] transfer rejected bag not found req={request.RequestId} sourceBag={request.SourceBagNetworkId} sourceFound={source != null} destinationBag={request.DestinationBagNetworkId} destinationFound={destination != null}"); + + SendTransferResponse(senderClientId, new NetworkTransferResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.BagNotFound + }); + return; + } + + NetworkTransferResponse response = source.ProcessTransferRequest(request, destination, senderClientId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + SendTransferResponse(senderClientId, response); + } + finally + { + DecrementPendingRequests(clientId); + } + } + + public void ReceivePickupRequest(NetworkPickupRequest request, ulong clientId) + { + if (!m_IsServer) return; + uint senderClientId = GetSenderClientId(clientId); + Debug.Log( + $"[NetworkInventoryPickupDebug][Manager] receive pickup request req={request.RequestId} senderConnection={clientId} senderClient={senderClientId} actor={request.ActorNetworkId} pickerBag={request.PickerBagNetworkId} sourceBag={request.SourceBagNetworkId} runtime={request.RuntimeIdHash}"); + if (!SecurityIntegration.ValidateModuleRequest( + senderClientId, + BuildContext(request.ActorNetworkId, request.CorrelationId), + "Inventory", + nameof(NetworkPickupRequest))) + { + Debug.LogWarning( + $"[NetworkInventoryPickupDebug][Manager] pickup rejected by security req={request.RequestId} senderClient={senderClientId} actor={request.ActorNetworkId} reason={GetSecurityRejection(request.ActorNetworkId, request.CorrelationId)}"); + SendPickupResponse(senderClientId, new NetworkPickupResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = GetSecurityRejection(request.ActorNetworkId, request.CorrelationId) + }); + return; + } + + if (!ValidateTargetOwnership(senderClientId, request.ActorNetworkId, request.PickerBagNetworkId, nameof(NetworkPickupRequest))) + { + Debug.LogWarning( + $"[NetworkInventoryPickupDebug][Manager] pickup rejected by ownership req={request.RequestId} senderClient={senderClientId} actor={request.ActorNetworkId} pickerBag={request.PickerBagNetworkId}"); + SendPickupResponse(senderClientId, new NetworkPickupResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.SecurityViolation + }); + return; + } + + if (!CheckRateLimit(clientId)) + { + Debug.LogWarning( + $"[NetworkInventoryPickupDebug][Manager] pickup rejected by rate limit req={request.RequestId} senderConnection={clientId}"); + SendPickupResponse(senderClientId, new NetworkPickupResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.RateLimitExceeded + }); + return; + } + + try + { + NetworkInventoryController picker = GetController(request.PickerBagNetworkId); + if (picker == null) + { + Debug.LogWarning( + $"[NetworkInventoryPickupDebug][Manager] pickup rejected picker bag not found req={request.RequestId} pickerBag={request.PickerBagNetworkId}"); + SendPickupResponse(senderClientId, new NetworkPickupResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + Authorized = false, + RejectionReason = InventoryRejectionReason.BagNotFound + }); + return; + } + + NetworkPickupResponse response = picker.ProcessPickupRequest(request, senderClientId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + Debug.Log( + $"[NetworkInventoryPickupDebug][Manager] pickup processed req={request.RequestId} authorized={response.Authorized} reason={response.RejectionReason} pickupFailure={response.PickupFailure} prop={response.PropNetworkId} senderClient={senderClientId} placed={response.PlacedPosition}"); + SendPickupResponse(senderClientId, response); + } + finally + { + DecrementPendingRequests(clientId); + } + } + + // [LOCAL-EDIT] #PILFER-INVENTORY-SERVER-LOOT + public void ReceiveLootRequest(NetworkLootRequest request, ulong clientId) + { + if (!m_IsServer) return; + uint senderClientId = GetSenderClientId(clientId); + Debug.Log( + $"[NetworkInventoryLootDebug][Manager] receive loot request req={request.RequestId} senderConnection={clientId} senderClient={senderClientId} actor={request.ActorNetworkId} container={request.ContainerBagNetworkId}"); + + if (!SecurityIntegration.ValidateModuleRequest( + senderClientId, + BuildContext(request.ActorNetworkId, request.CorrelationId), + "Inventory", + nameof(NetworkLootRequest))) + { + SendLootResponse(senderClientId, new NetworkLootResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + ContainerBagNetworkId = request.ContainerBagNetworkId, + Authorized = false, + RejectionReason = GetSecurityRejection(request.ActorNetworkId, request.CorrelationId), + LootFailure = NetworkLootFailure.None + }); + return; + } + + if (!ValidateTargetOwnership(senderClientId, request.ActorNetworkId, request.ActorNetworkId, nameof(NetworkLootRequest))) + { + SendLootResponse(senderClientId, new NetworkLootResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + ContainerBagNetworkId = request.ContainerBagNetworkId, + Authorized = false, + RejectionReason = InventoryRejectionReason.SecurityViolation, + LootFailure = NetworkLootFailure.None + }); + return; + } + + if (!CheckRateLimit(clientId)) + { + SendLootResponse(senderClientId, new NetworkLootResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + ContainerBagNetworkId = request.ContainerBagNetworkId, + Authorized = false, + RejectionReason = InventoryRejectionReason.RateLimitExceeded, + LootFailure = NetworkLootFailure.None + }); + return; + } + + try + { + NetworkInventoryController container = GetController(request.ContainerBagNetworkId); + if (container == null) + { + SendLootResponse(senderClientId, new NetworkLootResponse + { + RequestId = request.RequestId, + ActorNetworkId = request.ActorNetworkId, + CorrelationId = request.CorrelationId, + ContainerBagNetworkId = request.ContainerBagNetworkId, + Authorized = false, + RejectionReason = InventoryRejectionReason.BagNotFound, + LootFailure = NetworkLootFailure.ContainerBagNotFound + }); + return; + } + + NetworkLootResponse response = container.ProcessLootRequest(request, senderClientId); + response.ActorNetworkId = request.ActorNetworkId; + response.CorrelationId = request.CorrelationId; + Debug.Log( + $"[NetworkInventoryLootDebug][Manager] loot processed req={request.RequestId} authorized={response.Authorized} generated={response.Generated} reason={response.RejectionReason} lootFailure={response.LootFailure} senderClient={senderClientId} container={response.ContainerBagNetworkId}"); + SendLootResponse(senderClientId, response); + } + finally + { + DecrementPendingRequests(clientId); + } + } + + #endregion + + // ════════════════════════════════════════════════════════════════════════════════════════ + // SERVER: SEND RESPONSES + // ════════════════════════════════════════════════════════════════════════════════════════ + + #region Send Responses (Server) + + private void SendContentAddResponse(uint targetNetworkId, NetworkContentAddResponse response) + { + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Sending add response: RequestId={response.RequestId}, Authorized={response.Authorized}"); + OnSendContentAddResponse?.Invoke(targetNetworkId, response); + } + + private void SendContentRemoveResponse(uint targetNetworkId, NetworkContentRemoveResponse response) + { + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Sending remove response: RequestId={response.RequestId}, Authorized={response.Authorized}"); + OnSendContentRemoveResponse?.Invoke(targetNetworkId, response); + } + + private void SendContentMoveResponse(uint targetNetworkId, NetworkContentMoveResponse response) + { + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Sending move response: RequestId={response.RequestId}, Authorized={response.Authorized}"); + OnSendContentMoveResponse?.Invoke(targetNetworkId, response); + } + + private void SendContentUseResponse(uint targetNetworkId, NetworkContentUseResponse response) + { + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Sending use response: RequestId={response.RequestId}, Authorized={response.Authorized}"); + OnSendContentUseResponse?.Invoke(targetNetworkId, response); + } + + private void SendContentDropResponse(uint targetNetworkId, NetworkContentDropResponse response) + { + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Sending drop response: RequestId={response.RequestId}, Authorized={response.Authorized}"); + OnSendContentDropResponse?.Invoke(targetNetworkId, response); + } + + private void SendEquipmentResponse(uint targetNetworkId, NetworkEquipmentResponse response) + { + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Sending equipment response: RequestId={response.RequestId}, Authorized={response.Authorized}"); + OnSendEquipmentResponse?.Invoke(targetNetworkId, response); + } + + private void SendSocketResponse(uint targetNetworkId, NetworkSocketResponse response) + { + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Sending socket response: RequestId={response.RequestId}, Authorized={response.Authorized}"); + OnSendSocketResponse?.Invoke(targetNetworkId, response); + } + + private void SendWealthResponse(uint targetNetworkId, NetworkWealthResponse response) + { + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Sending wealth response: RequestId={response.RequestId}, Authorized={response.Authorized}"); + OnSendWealthResponse?.Invoke(targetNetworkId, response); + } + + private void SendTransferResponse(uint targetNetworkId, NetworkTransferResponse response) + { + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Sending transfer response: RequestId={response.RequestId}, Authorized={response.Authorized}"); + OnSendTransferResponse?.Invoke(targetNetworkId, response); + } + + private void SendPickupResponse(uint targetNetworkId, NetworkPickupResponse response) + { + Debug.Log( + $"[NetworkInventoryPickupDebug][Manager] send pickup response req={response.RequestId} target={targetNetworkId} authorized={response.Authorized} reason={response.RejectionReason} pickupFailure={response.PickupFailure} prop={response.PropNetworkId} placed={response.PlacedPosition}"); + OnSendPickupResponse?.Invoke(targetNetworkId, response); + } + + // [LOCAL-EDIT] #PILFER-INVENTORY-SERVER-LOOT + private void SendLootResponse(uint targetNetworkId, NetworkLootResponse response) + { + Debug.Log( + $"[NetworkInventoryLootDebug][Manager] send loot response req={response.RequestId} target={targetNetworkId} authorized={response.Authorized} generated={response.Generated} reason={response.RejectionReason} lootFailure={response.LootFailure} container={response.ContainerBagNetworkId}"); + OnSendLootResponse?.Invoke(targetNetworkId, response); + } + + #endregion + + // ════════════════════════════════════════════════════════════════════════════════════════ + // SERVER: BROADCASTING + // ════════════════════════════════════════════════════════════════════════════════════════ + + #region Broadcasting (Server) + + public void BroadcastItemAdded(NetworkItemAddedBroadcast broadcast) + { + if (!m_IsServer) return; + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Broadcasting item added: BagId={broadcast.BagNetworkId}"); + OnBroadcastItemAdded?.Invoke(broadcast); + } + + public void BroadcastItemRemoved(NetworkItemRemovedBroadcast broadcast) + { + if (!m_IsServer) return; + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Broadcasting item removed: BagId={broadcast.BagNetworkId}"); + OnBroadcastItemRemoved?.Invoke(broadcast); + } + + public void BroadcastItemDropped(NetworkItemDroppedBroadcast broadcast) + { + if (!m_IsServer) return; + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Broadcasting item dropped: BagId={broadcast.SourceBagNetworkId}"); + OnBroadcastItemDropped?.Invoke(broadcast); + } + + public void BroadcastDroppedItemRemoved(NetworkDroppedItemRemovedBroadcast broadcast) + { + if (!m_IsServer) return; + Debug.Log( + $"[NetworkInventoryPickupDebug][Manager] broadcast dropped item removed sourceBag={broadcast.SourceBagNetworkId} runtime={broadcast.RuntimeIdHash} position={broadcast.Position}"); + OnBroadcastDroppedItemRemoved?.Invoke(broadcast); + } + + public void BroadcastItemMoved(NetworkItemMovedBroadcast broadcast) + { + if (!m_IsServer) return; + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Broadcasting item moved: BagId={broadcast.BagNetworkId}"); + OnBroadcastItemMoved?.Invoke(broadcast); + } + + public void BroadcastItemUsed(NetworkItemUsedBroadcast broadcast) + { + if (!m_IsServer) return; + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Broadcasting item used: BagId={broadcast.BagNetworkId}"); + OnBroadcastItemUsed?.Invoke(broadcast); + } + + public void BroadcastItemEquipped(NetworkItemEquippedBroadcast broadcast) + { + if (!m_IsServer) return; + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Broadcasting item equipped: BagId={broadcast.BagNetworkId}, Index={broadcast.EquipmentIndex}"); + OnBroadcastItemEquipped?.Invoke(broadcast); + } + + public void BroadcastItemUnequipped(NetworkItemUnequippedBroadcast broadcast) + { + if (!m_IsServer) return; + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Broadcasting item unequipped: BagId={broadcast.BagNetworkId}, Index={broadcast.EquipmentIndex}"); + OnBroadcastItemUnequipped?.Invoke(broadcast); + } + + public void BroadcastSocketChange(NetworkSocketChangeBroadcast broadcast) + { + if (!m_IsServer) return; + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Broadcasting socket change: BagId={broadcast.BagNetworkId}"); + OnBroadcastSocketChange?.Invoke(broadcast); + } + + public void BroadcastWealthChange(NetworkWealthChangeBroadcast broadcast) + { + if (!m_IsServer) return; + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Broadcasting wealth change: BagId={broadcast.BagNetworkId}, Change={broadcast.Change}"); + OnBroadcastWealthChange?.Invoke(broadcast); + } + + public void BroadcastPropertyChange(NetworkPropertyChangeBroadcast broadcast) + { + if (!m_IsServer) return; + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Broadcasting property change: BagId={broadcast.BagNetworkId}"); + OnBroadcastPropertyChange?.Invoke(broadcast); + } + + public void BroadcastFullSnapshot(NetworkInventorySnapshot snapshot) + { + if (!m_IsServer) return; + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Broadcasting full snapshot: BagId={snapshot.BagNetworkId}, Cells={snapshot.Cells?.Length ?? 0}"); + OnBroadcastFullSnapshot?.Invoke(snapshot); + } + + public void BroadcastDelta(NetworkInventoryDelta delta) + { + if (!m_IsServer) return; + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Broadcasting delta: BagId={delta.BagNetworkId}"); + OnBroadcastDelta?.Invoke(delta); + } + + public void SendSnapshotToClient(ulong clientId, NetworkInventorySnapshot snapshot) + { + if (!m_IsServer) return; + if (m_LogNetworkMessages) + Debug.Log($"[NetworkInventoryManager] Sending snapshot to client {clientId}: BagId={snapshot.BagNetworkId}"); + OnSendSnapshotToClient?.Invoke(clientId, snapshot); + } + + #endregion + + // ════════════════════════════════════════════════════════════════════════════════════════ + // CLIENT: RECEIVING BROADCASTS + // ════════════════════════════════════════════════════════════════════════════════════════ + + #region Receive Broadcasts (Client) + + public void ReceiveItemAddedBroadcast(NetworkItemAddedBroadcast broadcast) + { + var controller = GetController(broadcast.BagNetworkId); + controller?.ReceiveItemAddedBroadcast(broadcast); + } + + public void ReceiveItemRemovedBroadcast(NetworkItemRemovedBroadcast broadcast) + { + var controller = GetController(broadcast.BagNetworkId); + controller?.ReceiveItemRemovedBroadcast(broadcast); + } + + public void ReceiveItemDroppedBroadcast(NetworkItemDroppedBroadcast broadcast) + { + var controller = GetControllerOrFallback(broadcast.SourceBagNetworkId, "receive dropped item broadcast"); + controller?.ReceiveItemDroppedBroadcast(broadcast); + } + + public void ReceiveDroppedItemRemovedBroadcast(NetworkDroppedItemRemovedBroadcast broadcast) + { + var controller = GetControllerOrFallback(broadcast.SourceBagNetworkId, "receive dropped item removed broadcast"); + controller?.ReceiveDroppedItemRemovedBroadcast(broadcast); + } + + public void ReceiveItemMovedBroadcast(NetworkItemMovedBroadcast broadcast) + { + var controller = GetController(broadcast.BagNetworkId); + controller?.ReceiveItemMovedBroadcast(broadcast); + } + + public void ReceiveItemUsedBroadcast(NetworkItemUsedBroadcast broadcast) + { + var controller = GetController(broadcast.BagNetworkId); + controller?.ReceiveItemUsedBroadcast(broadcast); + } + + public void ReceiveItemEquippedBroadcast(NetworkItemEquippedBroadcast broadcast) + { + var controller = GetController(broadcast.BagNetworkId); + controller?.ReceiveItemEquippedBroadcast(broadcast); + } + + public void ReceiveItemUnequippedBroadcast(NetworkItemUnequippedBroadcast broadcast) + { + var controller = GetController(broadcast.BagNetworkId); + controller?.ReceiveItemUnequippedBroadcast(broadcast); + } + + public void ReceiveSocketChangeBroadcast(NetworkSocketChangeBroadcast broadcast) + { + var controller = GetController(broadcast.BagNetworkId); + controller?.ReceiveSocketChangeBroadcast(broadcast); + } + + public void ReceiveWealthChangeBroadcast(NetworkWealthChangeBroadcast broadcast) + { + var controller = GetController(broadcast.BagNetworkId); + controller?.ReceiveWealthChangeBroadcast(broadcast); + } + + public void ReceiveFullSnapshot(NetworkInventorySnapshot snapshot) + { + var controller = GetController(snapshot.BagNetworkId); + if (controller == null) + { + Debug.LogWarning( + $"[NetworkInventoryPickupDebug][Manager] full snapshot ignored because no controller is registered for bag={snapshot.BagNetworkId} registeredControllers={m_Controllers.Count}"); + return; + } + + controller.ReceiveFullSnapshot(snapshot); + } + + public void ReceiveDelta(NetworkInventoryDelta delta) + { + var controller = GetController(delta.BagNetworkId); + controller?.ReceiveDelta(delta); + } + + #endregion + + // ════════════════════════════════════════════════════════════════════════════════════════ + // CLIENT: RECEIVING RESPONSES + // ════════════════════════════════════════════════════════════════════════════════════════ + + #region Receive Responses (Client) + + public void ReceiveContentAddResponse(NetworkContentAddResponse response, uint targetNetworkId) + { + uint actorId = response.ActorNetworkId != 0 ? response.ActorNetworkId : targetNetworkId; + var controller = GetController(actorId); + controller?.ReceiveContentAddResponse(response); + } + + public void ReceiveContentRemoveResponse(NetworkContentRemoveResponse response, uint targetNetworkId) + { + uint actorId = response.ActorNetworkId != 0 ? response.ActorNetworkId : targetNetworkId; + var controller = GetController(actorId); + controller?.ReceiveContentRemoveResponse(response); + } + + public void ReceiveContentMoveResponse(NetworkContentMoveResponse response, uint targetNetworkId) + { + uint actorId = response.ActorNetworkId != 0 ? response.ActorNetworkId : targetNetworkId; + var controller = GetController(actorId); + controller?.ReceiveContentMoveResponse(response); + } + + public void ReceiveContentUseResponse(NetworkContentUseResponse response, uint targetNetworkId) + { + uint actorId = response.ActorNetworkId != 0 ? response.ActorNetworkId : targetNetworkId; + var controller = GetController(actorId); + controller?.ReceiveContentUseResponse(response); + } + + public void ReceiveContentDropResponse(NetworkContentDropResponse response, uint targetNetworkId) + { + uint actorId = response.ActorNetworkId != 0 ? response.ActorNetworkId : targetNetworkId; + var controller = GetController(actorId); + controller?.ReceiveContentDropResponse(response); + } + + public void ReceiveEquipmentResponse(NetworkEquipmentResponse response, uint targetNetworkId) + { + uint actorId = response.ActorNetworkId != 0 ? response.ActorNetworkId : targetNetworkId; + var controller = GetController(actorId); + controller?.ReceiveEquipmentResponse(response); + } + + public void ReceiveSocketResponse(NetworkSocketResponse response, uint targetNetworkId) + { + uint actorId = response.ActorNetworkId != 0 ? response.ActorNetworkId : targetNetworkId; + var controller = GetController(actorId); + controller?.ReceiveSocketResponse(response); + } + + public void ReceiveWealthResponse(NetworkWealthResponse response, uint targetNetworkId) + { + uint actorId = response.ActorNetworkId != 0 ? response.ActorNetworkId : targetNetworkId; + var controller = GetController(actorId); + controller?.ReceiveWealthResponse(response); + } + + public void ReceiveTransferResponse(NetworkTransferResponse response, uint targetNetworkId) + { + if (!response.Authorized && m_LogNetworkMessages) + { + Debug.LogWarning($"[NetworkInventoryManager] Transfer rejected: {response.RejectionReason}"); + } + } + + public void ReceivePickupResponse(NetworkPickupResponse response, uint targetNetworkId) + { + Debug.Log( + $"[NetworkInventoryPickupDebug][Manager] receive pickup response target={targetNetworkId} req={response.RequestId} authorized={response.Authorized} reason={response.RejectionReason} pickupFailure={response.PickupFailure} prop={response.PropNetworkId} placed={response.PlacedPosition}"); + + if (!response.Authorized) + { + // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT-REJECT-DIAGNOSTICS + Debug.LogWarning($"[NetworkInventoryManager] Pickup rejected: {response.RejectionReason} pickupFailure={response.PickupFailure} prop={response.PropNetworkId}"); + } + } + + // [LOCAL-EDIT] #PILFER-INVENTORY-SERVER-LOOT + public void ReceiveLootResponse(NetworkLootResponse response, uint targetNetworkId) + { + Debug.Log( + $"[NetworkInventoryLootDebug][Manager] receive loot response target={targetNetworkId} req={response.RequestId} authorized={response.Authorized} generated={response.Generated} reason={response.RejectionReason} lootFailure={response.LootFailure} container={response.ContainerBagNetworkId}"); + + if (!response.Authorized) + { + Debug.LogWarning($"[NetworkInventoryManager] Loot rejected: {response.RejectionReason} lootFailure={response.LootFailure} container={response.ContainerBagNetworkId}"); + } + } + + #endregion + + // ════════════════════════════════════════════════════════════════════════════════════════ + // CUSTOM VALIDATION EXTENSION POINTS + // ════════════════════════════════════════════════════════════════════════════════════════ + + /// Custom validator for add operations. + public Func CustomAddValidator; + + /// Custom validator for remove operations. + public Func CustomRemoveValidator; + + /// Custom validator for merchant operations. + public Func CustomMerchantValidator; + + /// Custom validator for crafting operations. + public Func CustomCraftingValidator; + + // ════════════════════════════════════════════════════════════════════════════════════════ + // HELPERS + // ════════════════════════════════════════════════════════════════════════════════════════ + + private bool CheckRateLimit(ulong clientId) + { + if (!m_PendingRequestCounts.TryGetValue(clientId, out int count)) + count = 0; + + if (count >= m_MaxPendingRequestsPerPlayer) + { + Debug.LogWarning($"[NetworkInventoryManager] Client {clientId} exceeded rate limit"); + return false; + } + + m_PendingRequestCounts[clientId] = count + 1; + return true; + } + + private void DecrementPendingRequests(ulong clientId) + { + if (m_PendingRequestCounts.TryGetValue(clientId, out int count)) + { + m_PendingRequestCounts[clientId] = Math.Max(0, count - 1); + } + } + + public IEnumerable GetRegisteredNetworkIds() => m_Controllers.Keys; + + public void SendInitialState(ulong clientId) + { + if (!m_IsServer) return; + foreach (var kvp in m_Controllers) + { + var snapshot = kvp.Value.GetFullSnapshot(); + SendSnapshotToClient(clientId, snapshot); + } + } + + public void ForceFullSync() + { + if (!m_IsServer) return; + foreach (var kvp in m_Controllers) + { + var snapshot = kvp.Value.GetFullSnapshot(); + BroadcastFullSnapshot(snapshot); + } + } + + public void ClearControllers() + { + m_Controllers.Clear(); + m_MerchantControllers.Clear(); + if (m_LogNetworkMessages) + Debug.Log("[NetworkInventoryManager] All controllers cleared"); + } + } + + /// + /// Placeholder for merchant-specific network controller. + /// + public class NetworkMerchantController : MonoBehaviour + { + // Would contain merchant-specific networking logic + // Similar to NetworkInventoryController but for merchant operations + } +} +#endif diff --git a/NetworkInventoryPatchHooks.cs b/NetworkInventoryPatchHooks.cs new file mode 100644 index 0000000..f6a1257 --- /dev/null +++ b/NetworkInventoryPatchHooks.cs @@ -0,0 +1,153 @@ +#if GC2_INVENTORY +using System; +using System.Reflection; +using UnityEngine; +using GameCreator.Runtime.Common; +using GameCreator.Runtime.Inventory; + +namespace Arawn.GameCreator2.Networking.Inventory +{ + /// + /// Runtime installer for Inventory patch delegates. Enables patched-mode validation on server. + /// + public class NetworkInventoryPatchHooks : NetworkSingleton + { + private bool m_IsServer; + private bool m_Installed; + + public bool IsPatchActive => m_Installed && IsInventoryPatched(); + + public void Initialize(bool isServer) + { + m_IsServer = isServer; + if (m_IsServer) InstallHooks(); + else UninstallHooks(); + } + + protected override void OnSingletonCleanup() + { + UninstallHooks(); + } + + public static bool IsInventoryPatched() + { + return + HasPublicStaticField( + typeof(TBagContent), + "NetworkAddValidator", + typeof(Func)) && + HasPublicStaticField( + typeof(TBagContent), + "NetworkRemoveValidator", + typeof(Func)) && + HasPublicStaticField( + typeof(TBagContent), + "NetworkMoveValidator", + typeof(Func)) && + HasPublicStaticField( + typeof(TBagContent), + "NetworkDropValidator", + typeof(Func)) && + HasPublicStaticField( + typeof(TBagContent), + "NetworkUseValidator", + typeof(Func)) && + HasPublicStaticField( + typeof(BagWealth), + "NetworkAddValidator", + typeof(Func)) && + HasPublicStaticField( + typeof(BagWealth), + "NetworkSetValidator", + typeof(Func)) && + HasPublicStaticProperty(typeof(TBagContent), "IsNetworkingActive", typeof(bool)) && + HasPublicStaticProperty(typeof(BagWealth), "IsNetworkingActive", typeof(bool)) && + HasInstanceMethod(typeof(TBagContent), "UseDirect", typeof(RuntimeItem)) && + HasInstanceMethod(typeof(TBagContent), "DropDirect", typeof(RuntimeItem), typeof(Vector3)) && + HasInstanceMethod(typeof(BagWealth), "SetDirect", typeof(IdString), typeof(int)) && + HasInstanceMethod(typeof(BagWealth), "AddDirect", typeof(IdString), typeof(int)); + } + + private void InstallHooks() + { + if (m_Installed) return; + if (!IsInventoryPatched()) + { + Debug.LogWarning("[NetworkInventoryPatchHooks] Inventory runtime patch markers were not detected. Falling back to interception mode."); + return; + } + + SetStaticField(typeof(TBagContent), "NetworkAddValidator", new Func(ValidateAdd)); + SetStaticField(typeof(TBagContent), "NetworkRemoveValidator", new Func(ValidateRemove)); + SetStaticField(typeof(TBagContent), "NetworkMoveValidator", new Func(ValidateMove)); + SetStaticField(typeof(TBagContent), "NetworkDropValidator", new Func(ValidateDrop)); + SetStaticField(typeof(TBagContent), "NetworkUseValidator", new Func(ValidateUse)); + + SetStaticField(typeof(BagWealth), "NetworkAddValidator", new Func(ValidateWealthAdd)); + SetStaticField(typeof(BagWealth), "NetworkSetValidator", new Func(ValidateWealthSet)); + + m_Installed = true; + } + + private void UninstallHooks() + { + if (!m_Installed) return; + + SetStaticField(typeof(TBagContent), "NetworkAddValidator", null); + SetStaticField(typeof(TBagContent), "NetworkRemoveValidator", null); + SetStaticField(typeof(TBagContent), "NetworkMoveValidator", null); + SetStaticField(typeof(TBagContent), "NetworkDropValidator", null); + SetStaticField(typeof(TBagContent), "NetworkUseValidator", null); + + SetStaticField(typeof(BagWealth), "NetworkAddValidator", null); + SetStaticField(typeof(BagWealth), "NetworkSetValidator", null); + + m_Installed = false; + } + + private bool ValidateAdd(TBagContent _, RuntimeItem __, Vector2Int ___, bool ____) => m_IsServer; + private bool ValidateRemove(TBagContent _, RuntimeItem __) => m_IsServer; + private bool ValidateMove(TBagContent _, Vector2Int __, Vector2Int ___, bool ____) => m_IsServer; + private bool ValidateDrop(TBagContent _, RuntimeItem __, Vector3 ___) => m_IsServer; + private bool ValidateUse(TBagContent _, RuntimeItem __) => m_IsServer; + private bool ValidateWealthAdd(BagWealth _, IdString __, int ___) => m_IsServer; + private bool ValidateWealthSet(BagWealth _, IdString __, int ___) => m_IsServer; + + private static void SetStaticField(Type type, string fieldName, object value) + { + FieldInfo field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.Static); + if (field == null) + { + Debug.LogWarning($"[NetworkInventoryPatchHooks] Missing patched field {type.Name}.{fieldName}. GC2 update likely changed signatures."); + return; + } + + field.SetValue(null, value); + } + + private static bool HasPublicStaticField(Type type, string fieldName, Type expectedFieldType) + { + FieldInfo field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.Static); + return field != null && expectedFieldType.IsAssignableFrom(field.FieldType); + } + + private static bool HasPublicStaticProperty(Type type, string propertyName, Type expectedPropertyType) + { + PropertyInfo property = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Static); + return property != null && expectedPropertyType.IsAssignableFrom(property.PropertyType); + } + + private static bool HasInstanceMethod(Type type, string methodName, params Type[] parameterTypes) + { + MethodInfo method = type.GetMethod( + methodName, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, + null, + parameterTypes, + null); + + return method != null; + } + } +} +#endif diff --git a/NetworkInventoryTypes.cs b/NetworkInventoryTypes.cs new file mode 100644 index 0000000..bbc46fa --- /dev/null +++ b/NetworkInventoryTypes.cs @@ -0,0 +1,917 @@ +#if GC2_INVENTORY +using System; +using UnityEngine; + +namespace Arawn.GameCreator2.Networking.Inventory +{ + // ════════════════════════════════════════════════════════════════════════════════════════════ + // ENUMS + // ════════════════════════════════════════════════════════════════════════════════════════════ + + /// + /// Types of inventory content operations. + /// + public enum InventoryContentAction : byte + { + Add = 0, + AddAtPosition = 1, + Remove = 2, + RemoveAtPosition = 3, + Move = 4, + Use = 5, + Drop = 6, + Sort = 7 + } + + /// + /// Types of equipment operations. + /// + public enum EquipmentAction : byte + { + Equip = 0, + EquipToSlot = 1, + EquipToIndex = 2, + Unequip = 3, + UnequipFromIndex = 4 + } + + /// + /// Types of socket operations. + /// + public enum SocketAction : byte + { + Attach = 0, + AttachToSocket = 1, + Detach = 2, + DetachFromSocket = 3 + } + + /// + /// Types of wealth operations. + /// + public enum WealthAction : byte + { + Set = 0, + Add = 1, + Subtract = 2 + } + + /// + /// Types of merchant operations. + /// + public enum MerchantAction : byte + { + BuyFromMerchant = 0, + SellToMerchant = 1 + } + + /// + /// Types of crafting operations. + /// + public enum CraftingAction : byte + { + Craft = 0, + Dismantle = 1, + Combine = 2 + } + + /// + /// Reasons for inventory operation rejection. + /// + public enum InventoryRejectionReason : byte + { + None = 0, + NotAuthorized = 1, + BagNotFound = 2, + ItemNotFound = 3, + RuntimeItemNotFound = 4, + InsufficientSpace = 5, + InvalidPosition = 6, + CannotStack = 7, + ItemEquipped = 8, + CannotEquip = 9, + CannotUnequip = 10, + InsufficientFunds = 11, + MerchantNotFound = 12, + CannotBuy = 13, + CannotSell = 14, + InsufficientIngredients = 15, + CannotCraft = 16, + CannotDismantle = 17, + SocketNotFound = 18, + CannotAttach = 19, + CannotDetach = 20, + CooldownActive = 21, + CannotUse = 22, + CannotDrop = 23, + RateLimitExceeded = 24, + InvalidOperation = 25, + NotOwner = 26, + ProtocolMismatch = 27, + SecurityViolation = 28, + IdentityMismatch = 29, + InternalError = 30, + RequestTimeout = 31 + } + + // [LOCAL-EDIT] #INVENTORY-WORLD-OBJECT-REJECT-DIAGNOSTICS + public enum NetworkPickupFailure : byte + { + None = 0, + WorldObjectNotFound = 1, + WorldObjectPickupDisabled = 2, + WorldObjectItemMissing = 3, + WorldObjectConsumed = 4, + WorldObjectOutOfRange = 5, + WorldObjectRuntimeItemFailed = 6 + } + + // [LOCAL-EDIT] #INVENTORY-SERVER-LOOT + public enum NetworkLootFailure : byte + { + None = 0, + ContainerBagNotFound = 1, + ContainerIsNotWorldInventory = 2, + LootContainerMissing = 3, + LootTableMissing = 4, + AlreadyGenerated = 5, + LootRollFailed = 6 + } + + /// + /// Source of inventory modification (for auditing/validation). + /// + public enum InventoryModificationSource : byte + { + Direct = 0, + Pickup = 1, + Loot = 2, + Trade = 3, + Merchant = 4, + Craft = 5, + Quest = 6, + Ability = 7, + StatusEffect = 8, + Admin = 9 + } + + // ════════════════════════════════════════════════════════════════════════════════════════════ + // NETWORK RUNTIME ITEM REPRESENTATION + // ════════════════════════════════════════════════════════════════════════════════════════════ + + /// + /// Minimal network representation of a RuntimeProperty. + /// ~16 bytes + /// + [Serializable] + public struct NetworkRuntimeProperty + { + public int PropertyHash; // 4 bytes - IdString hash + public string PropertyIdString; // Variable - Deterministic property ID + public float Number; // 4 bytes + public string Text; // Variable (null for most properties) + + public static NetworkRuntimeProperty FromProperty(int hash, float number, string text) + { + return new NetworkRuntimeProperty + { + PropertyHash = hash, + Number = number, + Text = text + }; + } + } + + /// + /// Minimal network representation of a RuntimeSocket. + /// ~12 bytes without attachment + /// + [Serializable] + public struct NetworkRuntimeSocket + { + public int SocketHash; // 4 bytes + public string SocketIdString; // Variable - Deterministic socket ID + public bool HasAttachment; // 1 byte + public NetworkRuntimeItem Attachment; // Variable (null if no attachment) + } + + /// + /// Network representation of a RuntimeItem. + /// This is the core data structure for syncing items. + /// + [Serializable] + public struct NetworkRuntimeItem + { + public int ItemHash; // 4 bytes - Item.ID hash + public string ItemIdString; // Variable - Deterministic item ID string + public long RuntimeIdHash; // 8 bytes - RuntimeItem.RuntimeID hash (use long for uniqueness) + public string RuntimeIdString; // Variable - Full RuntimeID string for reconstruction + public NetworkRuntimeProperty[] Properties; // Variable + public NetworkRuntimeSocket[] Sockets; // Variable + + /// + /// Estimated serialization size in bytes. + /// + public int EstimatedSize + { + get + { + int size = 12; // Base fields + size += (ItemIdString?.Length ?? 0) * 2; + size += (RuntimeIdString?.Length ?? 0) * 2; + size += (Properties?.Length ?? 0) * 16; + size += (Sockets?.Length ?? 0) * 12; + return size; + } + } + } + + /// + /// Network representation of a Cell (inventory slot with stacked items). + /// + [Serializable] + public struct NetworkCell + { + public Vector2Int Position; // 8 bytes + public int ItemHash; // 4 bytes - Item type + public int StackCount; // 4 bytes + public NetworkRuntimeItem RootItem; // Variable - The root item of the stack + public long[] StackedRuntimeIds; // Variable - RuntimeIDs of stacked items + public string[] StackedRuntimeIdStrings; // Variable - RuntimeID strings of stacked items + } + + // ════════════════════════════════════════════════════════════════════════════════════════════ + // CONTENT REQUESTS / RESPONSES + // ════════════════════════════════════════════════════════════════════════════════════════════ + + /// + /// Request to add item to bag content. + /// ~40 bytes + item data + /// + [Serializable] + public struct NetworkContentAddRequest + { + public ushort RequestId; // 2 bytes + public uint ActorNetworkId; // 4 bytes + public uint CorrelationId; // 4 bytes + public uint TargetBagNetworkId; // 4 bytes + public int ItemHash; // 4 bytes - Item type to create, or 0 if providing RuntimeItem + public string ItemIdString; // Variable - Deterministic item ID string + public NetworkRuntimeItem RuntimeItem; // Variable - If adding existing runtime item + public Vector2Int Position; // 8 bytes - (-1,-1) for auto-placement + public bool AllowStack; // 1 byte + public InventoryModificationSource Source; // 1 byte + public int SourceHash; // 4 bytes + } + + /// + /// Response to content add request. + /// ~24 bytes + /// + [Serializable] + public struct NetworkContentAddResponse + { + public ushort RequestId; // 2 bytes + public uint ActorNetworkId; // 4 bytes + public uint CorrelationId; // 4 bytes + public bool Authorized; // 1 byte + public InventoryRejectionReason RejectionReason; // 1 byte + public Vector2Int ResultPosition; // 8 bytes - Where item was placed + public long AssignedRuntimeId; // 8 bytes - Server-assigned RuntimeID hash + public string AssignedRuntimeIdString; // Variable + } + + /// + /// Request to remove item from bag. + /// ~24 bytes + /// + [Serializable] + public struct NetworkContentRemoveRequest + { + public ushort RequestId; // 2 bytes + public uint ActorNetworkId; // 4 bytes + public uint CorrelationId; // 4 bytes + public uint TargetBagNetworkId; // 4 bytes + public long RuntimeIdHash; // 8 bytes - RuntimeItem to remove + public Vector2Int Position; // 8 bytes - Or position to remove from + public bool UsePosition; // 1 byte - Whether to use position instead of RuntimeID + public InventoryModificationSource Source; // 1 byte + } + + /// + /// Response to content remove request. + /// ~20 bytes + /// + [Serializable] + public struct NetworkContentRemoveResponse + { + public ushort RequestId; // 2 bytes + public uint ActorNetworkId; // 4 bytes + public uint CorrelationId; // 4 bytes + public bool Authorized; // 1 byte + public InventoryRejectionReason RejectionReason; // 1 byte + public NetworkRuntimeItem RemovedItem; // Variable - The item that was removed + } + + /// + /// Request to move item within bag. + /// ~24 bytes + /// + [Serializable] + public struct NetworkContentMoveRequest + { + public ushort RequestId; // 2 bytes + public uint ActorNetworkId; // 4 bytes + public uint CorrelationId; // 4 bytes + public uint TargetBagNetworkId; // 4 bytes + public Vector2Int FromPosition; // 8 bytes + public Vector2Int ToPosition; // 8 bytes + public bool AllowStack; // 1 byte + } + + /// + /// Response to content move request. + /// ~8 bytes + /// + [Serializable] + public struct NetworkContentMoveResponse + { + public ushort RequestId; // 2 bytes + public uint ActorNetworkId; // 4 bytes + public uint CorrelationId; // 4 bytes + public bool Authorized; // 1 byte + public InventoryRejectionReason RejectionReason; // 1 byte + public Vector2Int FinalPosition; // 8 bytes + } + + /// + /// Request to use an item. + /// ~20 bytes + /// + [Serializable] + public struct NetworkContentUseRequest + { + public ushort RequestId; // 2 bytes + public uint ActorNetworkId; // 4 bytes + public uint CorrelationId; // 4 bytes + public uint TargetBagNetworkId; // 4 bytes + public long RuntimeIdHash; // 8 bytes + public Vector2Int Position; // 8 bytes - Alternative to RuntimeID + public bool UsePosition; // 1 byte + } + + /// + /// Response to use request. + /// ~8 bytes + /// + [Serializable] + public struct NetworkContentUseResponse + { + public ushort RequestId; // 2 bytes + public uint ActorNetworkId; // 4 bytes + public uint CorrelationId; // 4 bytes + public bool Authorized; // 1 byte + public InventoryRejectionReason RejectionReason; // 1 byte + public bool WasConsumed; // 1 byte + } + + /// + /// Request to drop an item. + /// ~32 bytes + /// + [Serializable] + public struct NetworkContentDropRequest + { + public ushort RequestId; // 2 bytes + public uint ActorNetworkId; // 4 bytes + public uint CorrelationId; // 4 bytes + public uint TargetBagNetworkId; // 4 bytes + public long RuntimeIdHash; // 8 bytes + public Vector3 DropPosition; // 12 bytes + public int MaxAmount; // 4 bytes - For dropping from stack + } + + /// + /// Response to drop request. + /// ~8 bytes + /// + [Serializable] + public struct NetworkContentDropResponse + { + public ushort RequestId; // 2 bytes + public uint ActorNetworkId; // 4 bytes + public uint CorrelationId; // 4 bytes + public bool Authorized; // 1 byte + public InventoryRejectionReason RejectionReason; // 1 byte + public int DroppedCount; // 4 bytes + // Prop spawning handled separately via NetworkObject spawn + } + + // ════════════════════════════════════════════════════════════════════════════════════════════ + // EQUIPMENT REQUESTS / RESPONSES + // ════════════════════════════════════════════════════════════════════════════════════════════ + + /// + /// Request to equip/unequip item. + /// ~20 bytes + /// + [Serializable] + public struct NetworkEquipmentRequest + { + public ushort RequestId; // 2 bytes + public uint ActorNetworkId; // 4 bytes + public uint CorrelationId; // 4 bytes + public uint TargetBagNetworkId; // 4 bytes + public long RuntimeIdHash; // 8 bytes + public EquipmentAction Action; // 1 byte + public int SlotOrIndex; // 4 bytes - Slot number or equipment index + } + + /// + /// Response to equipment request. + /// ~8 bytes + /// + [Serializable] + public struct NetworkEquipmentResponse + { + public ushort RequestId; // 2 bytes + public uint ActorNetworkId; // 4 bytes + public uint CorrelationId; // 4 bytes + public bool Authorized; // 1 byte + public InventoryRejectionReason RejectionReason; // 1 byte + public int EquippedIndex; // 4 bytes - Final equipment index + } + + // ════════════════════════════════════════════════════════════════════════════════════════════ + // SOCKET REQUESTS / RESPONSES + // ════════════════════════════════════════════════════════════════════════════════════════════ + + /// + /// Request to attach/detach socket. + /// ~28 bytes + /// + [Serializable] + public struct NetworkSocketRequest + { + public ushort RequestId; // 2 bytes + public uint ActorNetworkId; // 4 bytes + public uint CorrelationId; // 4 bytes + public uint TargetBagNetworkId; // 4 bytes + public long ParentRuntimeIdHash; // 8 bytes - Parent item + public long AttachmentRuntimeIdHash; // 8 bytes - Attachment item (for attach) or socket contents (for detach) + public int SocketHash; // 4 bytes - Specific socket (0 for auto) + public string SocketIdString; // Variable - Deterministic socket ID string + public SocketAction Action; // 1 byte + } + + /// + /// Response to socket request. + /// ~16 bytes + /// + [Serializable] + public struct NetworkSocketResponse + { + public ushort RequestId; // 2 bytes + public uint ActorNetworkId; // 4 bytes + public uint CorrelationId; // 4 bytes + public bool Authorized; // 1 byte + public InventoryRejectionReason RejectionReason; // 1 byte + public int UsedSocketHash; // 4 bytes - Which socket was used + public NetworkRuntimeItem DetachedItem; // Variable - If detaching + } + + // ════════════════════════════════════════════════════════════════════════════════════════════ + // WEALTH REQUESTS / RESPONSES + // ════════════════════════════════════════════════════════════════════════════════════════════ + + /// + /// Request to modify wealth. + /// ~20 bytes + /// + [Serializable] + public struct NetworkWealthRequest + { + public ushort RequestId; // 2 bytes + public uint ActorNetworkId; // 4 bytes + public uint CorrelationId; // 4 bytes + public uint TargetBagNetworkId; // 4 bytes + public int CurrencyHash; // 4 bytes + public string CurrencyIdString; // Variable - Deterministic currency ID string + public int Value; // 4 bytes + public WealthAction Action; // 1 byte + public InventoryModificationSource Source; // 1 byte + public int SourceHash; // 4 bytes + } + + /// + /// Response to wealth request. + /// ~12 bytes + /// + [Serializable] + public struct NetworkWealthResponse + { + public ushort RequestId; // 2 bytes + public uint ActorNetworkId; // 4 bytes + public uint CorrelationId; // 4 bytes + public bool Authorized; // 1 byte + public InventoryRejectionReason RejectionReason; // 1 byte + public int NewValue; // 4 bytes + public int OldValue; // 4 bytes + } + + // ════════════════════════════════════════════════════════════════════════════════════════════ + // MERCHANT REQUESTS / RESPONSES + // ════════════════════════════════════════════════════════════════════════════════════════════ + + /// + /// Request to buy/sell from merchant. + /// ~24 bytes + /// + [Serializable] + public struct NetworkMerchantRequest + { + public ushort RequestId; // 2 bytes + public uint ActorNetworkId; // 4 bytes + public uint CorrelationId; // 4 bytes + public uint ClientBagNetworkId; // 4 bytes + public uint MerchantNetworkId; // 4 bytes - NetworkId of merchant's Bag + public long RuntimeIdHash; // 8 bytes - Item to buy/sell + public MerchantAction Action; // 1 byte + public int Amount; // 4 bytes - For stacked purchases + } + + /// + /// Response to merchant request. + /// ~16 bytes + /// + [Serializable] + public struct NetworkMerchantResponse + { + public ushort RequestId; // 2 bytes + public uint ActorNetworkId; // 4 bytes + public uint CorrelationId; // 4 bytes + public bool Authorized; // 1 byte + public InventoryRejectionReason RejectionReason; // 1 byte + public int TotalPrice; // 4 bytes + public int NewClientWealth; // 4 bytes + public int NewMerchantWealth; // 4 bytes + } + + // ════════════════════════════════════════════════════════════════════════════════════════════ + // CRAFTING REQUESTS / RESPONSES + // ════════════════════════════════════════════════════════════════════════════════════════════ + + /// + /// Request to craft/dismantle. + /// ~20 bytes + /// + [Serializable] + public struct NetworkCraftingRequest + { + public ushort RequestId; // 2 bytes + public uint ActorNetworkId; // 4 bytes + public uint CorrelationId; // 4 bytes + public uint InputBagNetworkId; // 4 bytes + public uint OutputBagNetworkId; // 4 bytes + public int ItemHash; // 4 bytes - Item to craft (for Craft action) + public string ItemIdString; // Variable - Deterministic crafted item ID string + public long RuntimeIdHash; // 8 bytes - RuntimeItem to dismantle (for Dismantle) + public CraftingAction Action; // 1 byte + } + + /// + /// Response to crafting request. + /// ~12 bytes + created item + /// + [Serializable] + public struct NetworkCraftingResponse + { + public ushort RequestId; // 2 bytes + public uint ActorNetworkId; // 4 bytes + public uint CorrelationId; // 4 bytes + public bool Authorized; // 1 byte + public InventoryRejectionReason RejectionReason; // 1 byte + public NetworkRuntimeItem CreatedItem; // Variable - The crafted item + public NetworkRuntimeItem[] ReturnedItems; // Variable - Dismantle returns + } + + // ════════════════════════════════════════════════════════════════════════════════════════════ + // BROADCASTS + // ════════════════════════════════════════════════════════════════════════════════════════════ + + /// + /// Broadcast when item is added to bag. + /// + [Serializable] + public struct NetworkItemAddedBroadcast + { + public uint BagNetworkId; + public NetworkRuntimeItem Item; + public Vector2Int Position; + public int StackCount; + } + + /// + /// Broadcast when item is removed from bag. + /// + [Serializable] + public struct NetworkItemRemovedBroadcast + { + public uint BagNetworkId; + public long RuntimeIdHash; + public Vector2Int Position; + public int RemainingStackCount; + } + + /// + /// Broadcast when an item is dropped into the world. + /// + [Serializable] + public struct NetworkItemDroppedBroadcast + { + public uint SourceBagNetworkId; + public NetworkRuntimeItem Item; + public Vector3 Position; + } + + /// + /// Broadcast when a previously dropped world item is picked up or otherwise removed. + /// + [Serializable] + public struct NetworkDroppedItemRemovedBroadcast + { + public uint SourceBagNetworkId; + public long RuntimeIdHash; + public Vector3 Position; + } + + /// + /// Broadcast when item is moved within bag. + /// + [Serializable] + public struct NetworkItemMovedBroadcast + { + public uint BagNetworkId; + public long RuntimeIdHash; + public Vector2Int FromPosition; + public Vector2Int ToPosition; + } + + /// + /// Broadcast when item is used. + /// + [Serializable] + public struct NetworkItemUsedBroadcast + { + public uint BagNetworkId; + public long RuntimeIdHash; + public bool WasConsumed; + } + + /// + /// Broadcast when item is equipped. + /// + [Serializable] + public struct NetworkItemEquippedBroadcast + { + public uint BagNetworkId; + public long RuntimeIdHash; + public int EquipmentIndex; + } + + /// + /// Broadcast when item is unequipped. + /// + [Serializable] + public struct NetworkItemUnequippedBroadcast + { + public uint BagNetworkId; + public long RuntimeIdHash; + public int EquipmentIndex; + } + + /// + /// Broadcast when socket attachment changes. + /// + [Serializable] + public struct NetworkSocketChangeBroadcast + { + public uint BagNetworkId; + public long ParentRuntimeIdHash; + public int SocketHash; + public bool HasAttachment; + public NetworkRuntimeItem Attachment; // If attached + } + + /// + /// Broadcast when wealth changes. + /// + [Serializable] + public struct NetworkWealthChangeBroadcast + { + public uint BagNetworkId; + public int CurrencyHash; + public int NewValue; + public int Change; + } + + /// + /// Broadcast when property value changes. + /// + [Serializable] + public struct NetworkPropertyChangeBroadcast + { + public uint BagNetworkId; + public long RuntimeIdHash; + public int PropertyHash; + public float NewNumber; + public string NewText; + } + + // ════════════════════════════════════════════════════════════════════════════════════════════ + // FULL STATE SYNC + // ════════════════════════════════════════════════════════════════════════════════════════════ + + /// + /// Full inventory snapshot for initial sync or reconnection. + /// + [Serializable] + public struct NetworkInventorySnapshot + { + public uint BagNetworkId; + public float Timestamp; + public int BagType; // Grid vs List + public Vector2Int BagSize; // For grid bags + public int MaxWeight; + public NetworkCell[] Cells; // All occupied cells + public NetworkEquipmentSlot[] Equipment; // All equipment slots + public NetworkWealthEntry[] Wealth; // All currencies + } + + /// + /// Equipment slot state for snapshot. + /// + [Serializable] + public struct NetworkEquipmentSlot + { + public int SlotIndex; + public int BaseItemHash; // What type of item can go here + public bool IsOccupied; + public long EquippedRuntimeIdHash; + } + + /// + /// Wealth entry for snapshot. + /// + [Serializable] + public struct NetworkWealthEntry + { + public int CurrencyHash; + public int Amount; + } + + /// + /// Delta update for efficient sync. + /// + [Serializable] + public struct NetworkInventoryDelta + { + public uint BagNetworkId; + public float Timestamp; + public uint ChangeMask; // Bit flags for what changed + public NetworkCell[] ChangedCells; + public NetworkEquipmentSlot[] ChangedEquipment; + public NetworkWealthEntry[] ChangedWealth; + } + + // ════════════════════════════════════════════════════════════════════════════════════════════ + // TRANSFER BETWEEN BAGS + // ════════════════════════════════════════════════════════════════════════════════════════════ + + /// + /// Request to transfer item between two bags (trade, loot, etc.). + /// + [Serializable] + public struct NetworkTransferRequest + { + public ushort RequestId; + public uint ActorNetworkId; + public uint CorrelationId; + public uint SourceBagNetworkId; + public uint DestinationBagNetworkId; + public long RuntimeIdHash; + public Vector2Int DestinationPosition; // (-1,-1) for auto + public bool AllowStack; + public InventoryModificationSource Source; + } + + /// + /// Response to transfer request. + /// + [Serializable] + public struct NetworkTransferResponse + { + public ushort RequestId; + public uint ActorNetworkId; + public uint CorrelationId; + public bool Authorized; + public InventoryRejectionReason RejectionReason; + public Vector2Int FinalPosition; + } + + // ════════════════════════════════════════════════════════════════════════════════════════════ + // LOOT / PICKUP + // ════════════════════════════════════════════════════════════════════════════════════════════ + + /// + /// Request to pick up a dropped Prop. + /// + [Serializable] + public struct NetworkPickupRequest + { + public ushort RequestId; + public uint ActorNetworkId; + public uint CorrelationId; + public uint PickerBagNetworkId; + public uint PropNetworkId; // NetworkId of the Prop object + public uint SourceBagNetworkId; // Bag that originally dropped/spawned the prop + public long RuntimeIdHash; // Runtime item represented by the dropped prop + public Vector2Int DestinationPosition; + } + + /// + /// Response to pickup request. + /// + [Serializable] + public struct NetworkPickupResponse + { + public ushort RequestId; + public uint ActorNetworkId; + public uint CorrelationId; + // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT-REJECT-DIAGNOSTICS + public uint PropNetworkId; + public NetworkPickupFailure PickupFailure; + public bool Authorized; + public InventoryRejectionReason RejectionReason; + public NetworkRuntimeItem PickedUpItem; + public Vector2Int PlacedPosition; + } + + // [LOCAL-EDIT] #INVENTORY-SERVER-LOOT + [Serializable] + public struct NetworkLootRequest + { + public ushort RequestId; + public uint ActorNetworkId; + public uint CorrelationId; + public uint ContainerBagNetworkId; + } + + // [LOCAL-EDIT] #INVENTORY-SERVER-LOOT + [Serializable] + public struct NetworkLootResponse + { + public ushort RequestId; + public uint ActorNetworkId; + public uint CorrelationId; + public uint ContainerBagNetworkId; + public bool Authorized; + public bool Generated; + public InventoryRejectionReason RejectionReason; + public NetworkLootFailure LootFailure; + } + + // ════════════════════════════════════════════════════════════════════════════════════════════ + // COMBINE (Two items into one) + // ════════════════════════════════════════════════════════════════════════════════════════════ + + /// + /// Request to combine two items (if crafting.AllowToCombine). + /// + [Serializable] + public struct NetworkCombineRequest + { + public ushort RequestId; + public uint ActorNetworkId; + public uint CorrelationId; + public uint BagNetworkId; + public Vector2Int PositionA; + public Vector2Int PositionB; + } + + /// + /// Response to combine request. + /// + [Serializable] + public struct NetworkCombineResponse + { + public ushort RequestId; + public uint ActorNetworkId; + public uint CorrelationId; + public bool Authorized; + public InventoryRejectionReason RejectionReason; + public NetworkRuntimeItem ResultItem; + public Vector2Int ResultPosition; + } +} +#endif diff --git a/NetworkLootContainer.cs b/NetworkLootContainer.cs new file mode 100644 index 0000000..9ced1e3 --- /dev/null +++ b/NetworkLootContainer.cs @@ -0,0 +1,41 @@ +#if GC2_INVENTORY +using GameCreator.Runtime.Inventory; +using UnityEngine; + +namespace Arawn.GameCreator2.Networking.Inventory +{ + // [LOCAL-EDIT] #INVENTORY-SERVER-LOOT + // Adds server-authoritative GC2 loot-container generation on top of Arawn's inventory networking layer. + // Must be attached to any GameObject that will send requests for Loot Table generation. + // Works alongside InstructionNetworkLootRequest.cs + [AddComponentMenu("Game Creator/Network/Inventory/Network Loot Container")] + [DisallowMultipleComponent] + public sealed class NetworkLootContainer : MonoBehaviour + { + [Header("Loot")] + [SerializeField] private LootTable m_LootTable; + [SerializeField] private bool m_GenerateOnce = true; + + [Header("Debug")] + [SerializeField] private bool m_LogDiagnostics; + + private bool m_HasGenerated; + + public LootTable LootTable => m_LootTable; + public bool GenerateOnce => m_GenerateOnce; + public bool HasGenerated => m_HasGenerated; + public bool LogDiagnostics => m_LogDiagnostics; + + public bool CanGenerate() + { + if (m_LootTable == null) return false; + return !m_GenerateOnce || !m_HasGenerated; + } + + public void MarkGenerated() + { + m_HasGenerated = true; + } + } +} +#endif diff --git a/NetworkWorldObject.cs b/NetworkWorldObject.cs new file mode 100644 index 0000000..fb4f303 --- /dev/null +++ b/NetworkWorldObject.cs @@ -0,0 +1,229 @@ +#if GC2_INVENTORY +using System.Collections.Generic; +using GameCreator.Runtime.Inventory; +using UnityEngine; + +namespace Arawn.GameCreator2.Networking.Inventory +{ + public enum NetworkWorldObjectKind + { + Generic = 0, + PickupItem = 1, + // For future expansion + Door = 2, + Lever = 3, + Trap = 4, + Portal = 5, + Shrine = 6 + } + + /// + /// Stable network identity for scene-authored world objects. Pickup behavior is the first + /// implemented use; future object interactions can reuse the same identity and registry. + /// + [AddComponentMenu("Game Creator/Network/Inventory/Network World Object")] + [DisallowMultipleComponent] + public sealed class NetworkWorldObject : MonoBehaviour + { + // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT + [Header("Network Id")] + [SerializeField] private bool m_UseAutomaticNetworkId = true; + [SerializeField] private uint m_ManualNetworkId; + [SerializeField] private string m_NetworkIdSalt = string.Empty; + + [Header("World Object")] + [SerializeField] private NetworkWorldObjectKind m_Kind = NetworkWorldObjectKind.PickupItem; + + [Header("Pickup")] + [SerializeField] private bool m_AllowPickup = true; + [SerializeField] private Item m_Item; + [SerializeField] private float m_PickupRadius = 2f; + [SerializeField] private bool m_DisableOnPickup = true; + [SerializeField] private bool m_DestroyOnPickup; + + [Header("Debug")] + [SerializeField] private bool m_LogDiagnostics; + + private uint m_CachedNetworkId; + private bool m_IsConsumed; + + public uint NetworkId => ResolveNetworkId(); + public NetworkWorldObjectKind Kind => m_Kind; + public bool AllowPickup => m_AllowPickup; + public Item Item => m_Item; + public float PickupRadius => Mathf.Max(0f, m_PickupRadius); + public bool IsConsumed => m_IsConsumed; + + private void OnEnable() + { + NetworkWorldObjectRegistry.Register(this); + } + + private void OnDisable() + { + NetworkWorldObjectRegistry.Unregister(this); + } + + public bool CanPickupFrom(Vector3 pickerPosition) + { + if (!m_AllowPickup || m_IsConsumed || m_Item == null) return false; + if (m_Kind != NetworkWorldObjectKind.PickupItem) return false; + + float radius = PickupRadius; + if (radius <= 0f) return true; + + // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT-RANGE-DIAGNOSTICS + return GetHorizontalDistanceTo(pickerPosition) <= radius; + } + + public float GetDistanceTo(Vector3 pickerPosition) + { + // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT-RANGE-DIAGNOSTICS + return Vector3.Distance(transform.position, pickerPosition); + } + + public float GetHorizontalDistanceTo(Vector3 pickerPosition) + { + // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT-RANGE-DIAGNOSTICS + Vector3 position = transform.position; + float deltaX = position.x - pickerPosition.x; + float deltaZ = position.z - pickerPosition.z; + return Mathf.Sqrt(deltaX * deltaX + deltaZ * deltaZ); + } + + public RuntimeItem CreatePickupRuntimeItem() + { + return m_Item != null ? new RuntimeItem(m_Item) : null; + } + + public void MarkPickedUp() + { + if (m_IsConsumed) return; + m_IsConsumed = true; + NetworkWorldObjectRegistry.MarkConsumed(NetworkId); + + if (m_LogDiagnostics) + { + Debug.Log( + $"[NetworkWorldObject] picked up object={name} networkId={NetworkId} item={m_Item?.ID.String}", + this); + } + + if (m_DestroyOnPickup) + { + Destroy(gameObject); + return; + } + + if (m_DisableOnPickup) + { + gameObject.SetActive(false); + } + } + + private uint ResolveNetworkId() + { + if (!m_UseAutomaticNetworkId && m_ManualNetworkId != 0) return m_ManualNetworkId; + if (m_CachedNetworkId != 0) return m_CachedNetworkId; + + string path = BuildStableScenePath(transform); + if (!string.IsNullOrEmpty(m_NetworkIdSalt)) path = $"{path}:{m_NetworkIdSalt}"; + + uint hash = 2166136261u; + for (int i = 0; i < path.Length; i++) + { + hash ^= path[i]; + hash *= 16777619u; + } + + m_CachedNetworkId = hash != 0 ? hash : 1u; + return m_CachedNetworkId; + } + + private static string BuildStableScenePath(Transform target) + { + if (target == null) return string.Empty; + + string scenePath = target.gameObject.scene.path; + if (string.IsNullOrEmpty(scenePath)) scenePath = target.gameObject.scene.name; + + string path = BuildStableScenePathSegment(target); + Transform current = target; + while (current.parent != null) + { + current = current.parent; + path = $"{BuildStableScenePathSegment(current)}/{path}"; + } + + return $"{scenePath}:{path}"; + } + + private static string BuildStableScenePathSegment(Transform target) + { + int sameNameIndex = 0; + Transform parent = target.parent; + if (parent != null) + { + for (int i = 0; i < parent.childCount; i++) + { + Transform sibling = parent.GetChild(i); + if (sibling == target) break; + if (sibling != null && sibling.name == target.name) sameNameIndex++; + } + } + else if (target.gameObject.scene.IsValid()) + { + GameObject[] roots = target.gameObject.scene.GetRootGameObjects(); + for (int i = 0; i < roots.Length; i++) + { + GameObject root = roots[i]; + if (root == null) continue; + if (root.transform == target) break; + if (root.name == target.name) sameNameIndex++; + } + } + + return $"{target.name}[{sameNameIndex}]"; + } + } + + public static class NetworkWorldObjectRegistry + { + // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT + private static readonly Dictionary s_Objects = new(128); + // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT-CONSUMED-REGISTRY + private static readonly HashSet s_ConsumedObjectIds = new(128); + + public static void Register(NetworkWorldObject worldObject) + { + if (worldObject == null || worldObject.NetworkId == 0) return; + s_Objects[worldObject.NetworkId] = worldObject; + } + + public static void Unregister(NetworkWorldObject worldObject) + { + if (worldObject == null || worldObject.NetworkId == 0) return; + if (!s_Objects.TryGetValue(worldObject.NetworkId, out NetworkWorldObject existing)) return; + if (existing == worldObject) s_Objects.Remove(worldObject.NetworkId); + } + + public static void MarkConsumed(uint networkId) + { + // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT-CONSUMED-REGISTRY + if (networkId != 0) s_ConsumedObjectIds.Add(networkId); + } + + public static bool IsConsumed(uint networkId) + { + // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT-CONSUMED-REGISTRY + return networkId != 0 && s_ConsumedObjectIds.Contains(networkId); + } + + public static bool TryGet(uint networkId, out NetworkWorldObject worldObject) + { + worldObject = null; + return networkId != 0 && s_Objects.TryGetValue(networkId, out worldObject) && worldObject != null; + } + } +} +#endif From 316041c63e167d7f8df28fae21431ef99dfc8969 Mon Sep 17 00:00:00 2001 From: I am creating a game Date: Fri, 31 Jul 2026 17:13:00 -0700 Subject: [PATCH 2/2] Remove misplaced root-level inventory file --- InstructionNetworkLootRequest.cs | 96 - InstructionNetworkPickupRequest.cs | 96 - NetworkInventoryController.Client.cs | 1456 ------------- ...ventoryController.Server.SyncAndHelpers.cs | 924 --------- NetworkInventoryController.Server.cs | 1766 ---------------- ...rkInventoryController.WorldObjectPickup.cs | 264 --- NetworkInventoryController.cs | 536 ----- NetworkInventoryManager.cs | 1815 ----------------- NetworkInventoryPatchHooks.cs | 153 -- NetworkInventoryTypes.cs | 917 --------- NetworkLootContainer.cs | 41 - NetworkWorldObject.cs | 229 --- 12 files changed, 8293 deletions(-) delete mode 100644 InstructionNetworkLootRequest.cs delete mode 100644 InstructionNetworkPickupRequest.cs delete mode 100644 NetworkInventoryController.Client.cs delete mode 100644 NetworkInventoryController.Server.SyncAndHelpers.cs delete mode 100644 NetworkInventoryController.Server.cs delete mode 100644 NetworkInventoryController.WorldObjectPickup.cs delete mode 100644 NetworkInventoryController.cs delete mode 100644 NetworkInventoryManager.cs delete mode 100644 NetworkInventoryPatchHooks.cs delete mode 100644 NetworkInventoryTypes.cs delete mode 100644 NetworkLootContainer.cs delete mode 100644 NetworkWorldObject.cs diff --git a/InstructionNetworkLootRequest.cs b/InstructionNetworkLootRequest.cs deleted file mode 100644 index 7363b6d..0000000 --- a/InstructionNetworkLootRequest.cs +++ /dev/null @@ -1,96 +0,0 @@ -#if GC2_INVENTORY -using System; -using System.Threading.Tasks; -using GameCreator.Runtime.Characters; -using GameCreator.Runtime.Common; -using GameCreator.Runtime.VisualScripting; -using UnityEngine; - -namespace Arawn.GameCreator2.Networking.Inventory -{ - // [LOCAL-EDIT] #INVENTORY-SERVER-LOOT - // Adds server-authoritative GC2 loot-container generation on top of Arawn's inventory networking layer. - [Title("Network Loot Request")] - [Description("Requests server-authoritative loot generation for a Network Loot Container")] - [Category("Network/Inventory/Network Loot Request")] - [Parameter("Loot Container", "GameObject with the NetworkInventoryController and NetworkLootContainer. Usually Self.")] - [Parameter("Actor", "Player or character GameObject with the NetworkInventoryController that owns the request.")] - [Parameter("Log Diagnostics", "Print diagnostic logs when this instruction sends or rejects a loot request.")] - [Keywords("Network", "Inventory", "Loot", "Container", "Server")] - [Serializable] - public sealed class InstructionNetworkLootRequest : Instruction - { - [Header("Loot Container")] - [SerializeField] - [Tooltip("GameObject with the NetworkInventoryController and NetworkLootContainer. Usually Self.")] - private PropertyGetGameObject m_LootContainer = GetGameObjectSelf.Create(); - - [Header("Actor")] - [SerializeField] - [Tooltip("Player or character GameObject with the NetworkInventoryController that owns the request.")] - private PropertyGetGameObject m_Actor = GetGameObjectPlayer.Create(); - - [Header("Debug")] - [SerializeField] - [Tooltip("Print diagnostic logs when this instruction sends or rejects a loot request.")] - private bool m_LogDiagnostics; - - public override string Title => $"Network Loot {m_LootContainer}"; - - protected override Task Run(Args args) - { - GameObject containerObject = m_LootContainer.Get(args); - GameObject actorObject = m_Actor.Get(args); - - if (containerObject == null) - { - LogWarning("No loot container resolved."); - return DefaultResult; - } - - if (actorObject == null) - { - LogWarning("No actor resolved."); - return DefaultResult; - } - - NetworkInventoryController containerInventory = - containerObject.GetComponentInParent() ?? - containerObject.GetComponentInChildren(); - - NetworkInventoryController actorInventory = - actorObject.GetComponentInParent() ?? - actorObject.GetComponentInChildren(); - - if (containerInventory == null) - { - LogWarning($"Loot container '{containerObject.name}' has no NetworkInventoryController."); - return DefaultResult; - } - - if (actorInventory == null) - { - LogWarning($"Actor '{actorObject.name}' has no NetworkInventoryController."); - return DefaultResult; - } - - actorInventory.RequestLootGeneration(containerInventory); - Log($"sent loot request actor={actorObject.name} actorBag={actorInventory.NetworkId} container={containerObject.name} containerBag={containerInventory.NetworkId}"); - - return DefaultResult; - } - - private void Log(string message) - { - if (!m_LogDiagnostics) return; - Debug.Log($"[InstructionNetworkLootRequest] {message}"); - } - - private void LogWarning(string message) - { - if (!m_LogDiagnostics) return; - Debug.LogWarning($"[InstructionNetworkLootRequest] {message}"); - } - } -} -#endif diff --git a/InstructionNetworkPickupRequest.cs b/InstructionNetworkPickupRequest.cs deleted file mode 100644 index 9c81804..0000000 --- a/InstructionNetworkPickupRequest.cs +++ /dev/null @@ -1,96 +0,0 @@ -#if GC2_INVENTORY -using System; -using System.Threading.Tasks; -using GameCreator.Runtime.Characters; -using GameCreator.Runtime.Common; -using GameCreator.Runtime.Inventory; -using GameCreator.Runtime.VisualScripting; -using UnityEngine; - -namespace Arawn.GameCreator2.Networking.Inventory -{ - [Title("Network Pickup Request")] - [Description("Requests a server-authoritative pickup of a Network World Object")] - [Category("Network/Inventory/Network Pickup Request")] - [Parameter("Pickup Source", "GameObject with the NetworkWorldObject to pick up. Usually Self.")] - [Parameter("Picker", "Player or character GameObject with the NetworkInventoryController that receives the item.")] - [Parameter("Destination Position", "Inventory destination cell. Use (-1, -1) to let the bag auto-place the item.")] - [Parameter("Log Diagnostics", "Print diagnostic logs when this instruction sends or rejects a pickup request.")] - [Keywords("Network", "Inventory", "Pickup", "Item", "World")] - [Serializable] - public sealed class InstructionNetworkPickupRequest : Instruction - { - // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT - [Header("Pickup")] - [SerializeField] - [Tooltip("GameObject with the NetworkWorldObject to pick up. Usually Self.")] - private PropertyGetGameObject m_PickupSource = GetGameObjectSelf.Create(); - - [SerializeField] - [Tooltip("Player or character GameObject with the NetworkInventoryController that receives the item.")] - private PropertyGetGameObject m_Picker = GetGameObjectPlayer.Create(); - - [SerializeField] - [Tooltip("Inventory destination cell. Use (-1, -1) to let the bag auto-place the item.")] - private Vector2Int m_DestinationPosition = TBagContent.INVALID; - - [Header("Debug")] - [SerializeField] - [Tooltip("Print diagnostic logs when this instruction sends or rejects a pickup request.")] - private bool m_LogDiagnostics; - - public override string Title => $"Network Pickup {m_PickupSource}"; - - protected override Task Run(Args args) - { - GameObject sourceObject = m_PickupSource.Get(args); - GameObject pickerObject = m_Picker.Get(args); - - if (sourceObject == null) - { - LogWarning("No pickup source resolved."); - return DefaultResult; - } - - if (pickerObject == null) - { - LogWarning("No picker resolved."); - return DefaultResult; - } - - NetworkWorldObject worldObject = sourceObject.GetComponentInParent(); - if (worldObject == null) - { - LogWarning($"Pickup source '{sourceObject.name}' has no NetworkWorldObject."); - return DefaultResult; - } - - NetworkInventoryController pickerInventory = pickerObject.GetComponent(); - if (pickerInventory == null) - { - LogWarning($"Picker '{pickerObject.name}' has no NetworkInventoryController."); - return DefaultResult; - } - - pickerInventory.RequestWorldObjectPickup(worldObject, m_DestinationPosition); - Log( - $"sent pickup source={sourceObject.name} picker={pickerObject.name} " + - $"worldObject={worldObject.NetworkId} item={worldObject.Item?.ID.String} destination={m_DestinationPosition}"); - - return DefaultResult; - } - - private void Log(string message) - { - if (!m_LogDiagnostics) return; - Debug.Log($"[InstructionNetworkPickupRequest] {message}"); - } - - private void LogWarning(string message) - { - if (!m_LogDiagnostics) return; - Debug.LogWarning($"[InstructionNetworkPickupRequest] {message}"); - } - } -} -#endif diff --git a/NetworkInventoryController.Client.cs b/NetworkInventoryController.Client.cs deleted file mode 100644 index 18d8ba1..0000000 --- a/NetworkInventoryController.Client.cs +++ /dev/null @@ -1,1456 +0,0 @@ -#if GC2_INVENTORY -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using UnityEngine; -using GameCreator.Runtime.Common; -using GameCreator.Runtime.Inventory; - -namespace Arawn.GameCreator2.Networking.Inventory -{ - // ════════════════════════════════════════════════════════════════════════════════════════════ - // CLIENT-SIDE — Requests, response handlers, and local change detection - // ════════════════════════════════════════════════════════════════════════════════════════════ - - public partial class NetworkInventoryController - { - // ════════════════════════════════════════════════════════════════════════════════════════ - // CLIENT-SIDE: REQUEST OPERATIONS - // ════════════════════════════════════════════════════════════════════════════════════════ - - #region Content Requests - - /// - /// Request to add an item type to the bag. - /// - public void RequestAddItem(Item item, Vector2Int position, bool allowStack, - InventoryModificationSource source = InventoryModificationSource.Direct, int sourceHash = 0) - { - if (m_IsRemoteClient) - { - Debug.LogWarning("[NetworkInventoryController] Cannot modify inventory on remote client"); - return; - } - - var request = new NetworkContentAddRequest - { - RequestId = GetNextRequestId(), - ActorNetworkId = NetworkId, - CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), - TargetBagNetworkId = NetworkId, - ItemHash = item.ID.Hash, - ItemIdString = item.ID.String, - Position = position, - AllowStack = allowStack, - Source = source, - SourceHash = sourceHash - }; - - m_PendingAdds[GetPendingKey(request.ActorNetworkId, request.CorrelationId, request.RequestId)] = new PendingContentAdd - { - Request = request, - SentTime = Time.time - }; - - OnContentAddRequested?.Invoke(request); - - if (m_IsServer) - { - var response = ProcessContentAddRequest(request, NetworkId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - ReceiveContentAddResponse(response); - } - else - { - NetworkInventoryManager.Instance?.SendContentAddRequest(request); - } - } - - /// - /// Request to add an existing RuntimeItem to the bag. - /// - public void RequestAddRuntimeItem(NetworkRuntimeItem runtimeItem, Vector2Int position, bool allowStack, - InventoryModificationSource source = InventoryModificationSource.Direct, int sourceHash = 0) - { - if (m_IsRemoteClient) return; - - // Arbitrary runtime payload creation is server-authorized only. - if (!m_IsServer) - { - if (m_LogRejections) - { - Debug.LogWarning("[NetworkInventoryController] RequestAddRuntimeItem is server-authorized only"); - } - OnOperationRejected?.Invoke(InventoryRejectionReason.SecurityViolation, "Add runtime item"); - return; - } - - if (runtimeItem.ItemHash == 0) - { - if (m_LogRejections) - { - Debug.LogWarning("[NetworkInventoryController] Runtime item payload missing item hash"); - } - OnOperationRejected?.Invoke(InventoryRejectionReason.IdentityMismatch, "Add runtime item"); - return; - } - - var request = new NetworkContentAddRequest - { - RequestId = GetNextRequestId(), - ActorNetworkId = NetworkId, - CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), - TargetBagNetworkId = NetworkId, - // Server-authorized flow resolves by deterministic item hash. - ItemHash = runtimeItem.ItemHash, - ItemIdString = string.Empty, - RuntimeItem = runtimeItem, - Position = position, - AllowStack = allowStack, - Source = source, - SourceHash = sourceHash - }; - - m_PendingAdds[GetPendingKey(request.ActorNetworkId, request.CorrelationId, request.RequestId)] = new PendingContentAdd - { - Request = request, - SentTime = Time.time - }; - - OnContentAddRequested?.Invoke(request); - - if (m_IsServer) - { - var response = ProcessContentAddRequest(request, NetworkId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - ReceiveContentAddResponse(response); - } - } - - /// - /// Request to remove an item from the bag. - /// - public void RequestRemoveItem(RuntimeItem runtimeItem, - InventoryModificationSource source = InventoryModificationSource.Direct) - { - if (m_IsRemoteClient) return; - if (runtimeItem == null) return; - - var request = new NetworkContentRemoveRequest - { - RequestId = GetNextRequestId(), - ActorNetworkId = NetworkId, - CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), - TargetBagNetworkId = NetworkId, - RuntimeIdHash = runtimeItem.RuntimeID.Hash, - UsePosition = false, - Source = source - }; - - m_PendingRemoves[GetPendingKey(request.ActorNetworkId, request.CorrelationId, request.RequestId)] = new PendingContentRemove - { - Request = request, - RemovedItem = runtimeItem, - SentTime = Time.time - }; - - OnContentRemoveRequested?.Invoke(request); - - if (m_IsServer) - { - var response = ProcessContentRemoveRequest(request, NetworkId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - ReceiveContentRemoveResponse(response); - } - else - { - NetworkInventoryManager.Instance?.SendContentRemoveRequest(request); - } - } - - /// - /// Request to remove item at position. - /// - public void RequestRemoveAtPosition(Vector2Int position, - InventoryModificationSource source = InventoryModificationSource.Direct) - { - if (m_IsRemoteClient) return; - - var request = new NetworkContentRemoveRequest - { - RequestId = GetNextRequestId(), - ActorNetworkId = NetworkId, - CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), - TargetBagNetworkId = NetworkId, - Position = position, - UsePosition = true, - Source = source - }; - - m_PendingRemoves[GetPendingKey(request.ActorNetworkId, request.CorrelationId, request.RequestId)] = new PendingContentRemove - { - Request = request, - SentTime = Time.time - }; - - OnContentRemoveRequested?.Invoke(request); - - if (m_IsServer) - { - var response = ProcessContentRemoveRequest(request, NetworkId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - ReceiveContentRemoveResponse(response); - } - else - { - NetworkInventoryManager.Instance?.SendContentRemoveRequest(request); - } - } - - /// - /// Request to move item within bag. - /// - public void RequestMoveItem(Vector2Int fromPosition, Vector2Int toPosition, bool allowStack) - { - if (m_IsRemoteClient) return; - - var request = new NetworkContentMoveRequest - { - RequestId = GetNextRequestId(), - ActorNetworkId = NetworkId, - CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), - TargetBagNetworkId = NetworkId, - FromPosition = fromPosition, - ToPosition = toPosition, - AllowStack = allowStack - }; - - m_PendingMoves[GetPendingKey(request.ActorNetworkId, request.CorrelationId, request.RequestId)] = new PendingContentMove - { - Request = request, - SentTime = Time.time - }; - - OnContentMoveRequested?.Invoke(request); - - if (m_IsServer) - { - var response = ProcessContentMoveRequest(request, NetworkId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - ReceiveContentMoveResponse(response); - } - else - { - NetworkInventoryManager.Instance?.SendContentMoveRequest(request); - } - } - - /// - /// Request to use an item. - /// - public void RequestUseItem(RuntimeItem runtimeItem) - { - if (m_IsRemoteClient) return; - if (runtimeItem == null) return; - - var request = new NetworkContentUseRequest - { - RequestId = GetNextRequestId(), - ActorNetworkId = NetworkId, - CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), - TargetBagNetworkId = NetworkId, - RuntimeIdHash = runtimeItem.RuntimeID.Hash, - UsePosition = false - }; - - OnContentUseRequested?.Invoke(request); - - if (m_IsServer) - { - var response = ProcessContentUseRequest(request, NetworkId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - ReceiveContentUseResponse(response); - } - else - { - NetworkInventoryManager.Instance?.SendContentUseRequest(request); - } - } - - /// - /// Request to drop an item. - /// - public void RequestDropItem(RuntimeItem runtimeItem, Vector3 dropPosition, int maxAmount = 1) - { - if (m_IsRemoteClient) return; - if (runtimeItem == null) return; - - SendDropRequest(NetworkId, NetworkId, runtimeItem, dropPosition, maxAmount); - } - - private void SendDropRequest(uint actorNetworkId, uint targetBagNetworkId, RuntimeItem runtimeItem, Vector3 dropPosition, int maxAmount = 1) - { - if (runtimeItem == null || actorNetworkId == 0 || targetBagNetworkId == 0) return; - - var request = new NetworkContentDropRequest - { - RequestId = GetNextRequestId(), - ActorNetworkId = actorNetworkId, - CorrelationId = NetworkCorrelation.Compose(actorNetworkId, m_LastIssuedRequestId), - TargetBagNetworkId = targetBagNetworkId, - RuntimeIdHash = runtimeItem.RuntimeID.Hash, - DropPosition = dropPosition, - MaxAmount = maxAmount - }; - - LogPickupDebug( - $"{name}: sending drop request req={request.RequestId} actor={actorNetworkId} targetBag={targetBagNetworkId} item={DescribeRuntimeItem(runtimeItem)} position={dropPosition} server={m_IsServer} local={m_IsLocalClient}", - this); - - if (m_IsServer) - { - var response = ProcessContentDropRequest(request, NetworkId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - ReceiveContentDropResponse(response); - } - else - { - NetworkInventoryManager.Instance?.SendContentDropRequest(request); - } - } - - // [LOCAL-EDIT] #PILFER-INVENTORY-UI-TRANSFER-BRIDGE - public void RequestTransferItem(NetworkInventoryController destination, RuntimeItem runtimeItem, Vector2Int destinationPosition, bool allowStack) - { - if (destination == null || runtimeItem == null) return; - - if (destination == this) - { - Vector2Int sourcePosition = m_Bag.Content.FindPosition(runtimeItem.RuntimeID); - if (sourcePosition == TBagContent.INVALID) return; - - // TODO: Same world/container reorganization needs explicit access rules before client UI can request it. - if (IsWorldInventory) - { - LogPickupWarning( - $"{name}: same world-container move skipped until container access rules are implemented item={DescribeRuntimeItem(runtimeItem)} source={sourcePosition} destination={destinationPosition}", - this); - return; - } - - RequestMoveItem(sourcePosition, destinationPosition, allowStack); - return; - } - - if (NetworkId == 0 || destination.NetworkId == 0) return; - if (!TryGetLocalActorNetworkId(out uint actorNetworkId)) - { - LogPickupWarning( - $"{name}: transfer request skipped no local actor network id sourceBag={NetworkId} destinationBag={destination.NetworkId} item={DescribeRuntimeItem(runtimeItem)}", - this); - return; - } - - var request = new NetworkTransferRequest - { - RequestId = GetNextRequestId(), - ActorNetworkId = actorNetworkId, - CorrelationId = NetworkCorrelation.Compose(actorNetworkId, m_LastIssuedRequestId), - SourceBagNetworkId = NetworkId, - DestinationBagNetworkId = destination.NetworkId, - RuntimeIdHash = runtimeItem.RuntimeID.Hash, - DestinationPosition = destinationPosition, - AllowStack = allowStack, - Source = InventoryModificationSource.Loot - }; - - LogPickupDebug( - $"{name}: sending transfer request req={request.RequestId} actor={actorNetworkId} sourceBag={NetworkId} destinationBag={destination.NetworkId} item={DescribeRuntimeItem(runtimeItem)} destination={destinationPosition} server={m_IsServer}", - this); - - if (m_IsServer) - { - NetworkTransferResponse response = ProcessTransferRequest(request, destination, NetworkId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - NetworkInventoryManager.Instance?.ReceiveTransferResponse(response, actorNetworkId); - return; - } - - NetworkInventoryManager.Instance?.SendTransferRequest(request); - } - - // [LOCAL-EDIT] #INVENTORY-SERVER-LOOT - public void RequestLootGeneration(NetworkInventoryController containerInventory) - { - if (containerInventory == null) return; - if (m_IsRemoteClient) return; - if (!m_IsLocalClient && !m_IsServer) return; - - if (!TryGetLocalActorNetworkId(out uint actorNetworkId)) - { - LogPickupWarning($"{name}: loot request skipped no local actor network id container={containerInventory.NetworkId}", this); - return; - } - - var request = new NetworkLootRequest - { - RequestId = GetNextRequestId(), - ActorNetworkId = actorNetworkId, - CorrelationId = NetworkCorrelation.Compose(actorNetworkId, m_LastIssuedRequestId), - ContainerBagNetworkId = containerInventory.NetworkId - }; - - Debug.Log( - $"[NetworkInventoryLootDebug] {name}: sending loot request req={request.RequestId} actor={request.ActorNetworkId} container={request.ContainerBagNetworkId} server={m_IsServer} local={m_IsLocalClient}"); - - if (m_IsServer) - { - NetworkLootResponse response = containerInventory.ProcessLootRequest(request, NetworkId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - return; - } - - NetworkInventoryManager.Instance?.SendLootRequest(request); - } - - #endregion - - #region Equipment Requests - - /// - /// Request to equip an item. - /// - public void RequestEquip(RuntimeItem runtimeItem, int slot = -1) - { - if (m_IsRemoteClient) return; - if (runtimeItem == null) return; - - var request = new NetworkEquipmentRequest - { - RequestId = GetNextRequestId(), - ActorNetworkId = NetworkId, - CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), - TargetBagNetworkId = NetworkId, - RuntimeIdHash = runtimeItem.RuntimeID.Hash, - Action = slot >= 0 ? EquipmentAction.EquipToSlot : EquipmentAction.Equip, - SlotOrIndex = slot - }; - - m_PendingEquipment[GetPendingKey(request.ActorNetworkId, request.CorrelationId, request.RequestId)] = new PendingEquipment - { - Request = request, - SentTime = Time.time - }; - - OnEquipmentRequested?.Invoke(request); - - if (m_IsServer) - { - _ = ProcessLocalEquipmentRequestAsync(request); - } - else - { - NetworkInventoryManager.Instance?.SendEquipmentRequest(request); - } - } - - /// - /// Request to unequip an item. - /// - public void RequestUnequip(RuntimeItem runtimeItem) - { - if (m_IsRemoteClient) return; - if (runtimeItem == null) return; - - var request = new NetworkEquipmentRequest - { - RequestId = GetNextRequestId(), - ActorNetworkId = NetworkId, - CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), - TargetBagNetworkId = NetworkId, - RuntimeIdHash = runtimeItem.RuntimeID.Hash, - Action = EquipmentAction.Unequip, - SlotOrIndex = -1 - }; - - m_PendingEquipment[GetPendingKey(request.ActorNetworkId, request.CorrelationId, request.RequestId)] = new PendingEquipment - { - Request = request, - SentTime = Time.time - }; - - OnEquipmentRequested?.Invoke(request); - - if (m_IsServer) - { - _ = ProcessLocalEquipmentRequestAsync(request); - } - else - { - NetworkInventoryManager.Instance?.SendEquipmentRequest(request); - } - } - - /// - /// Request to unequip from specific index. - /// - public void RequestUnequipFromIndex(int index) - { - if (m_IsRemoteClient) return; - - var request = new NetworkEquipmentRequest - { - RequestId = GetNextRequestId(), - ActorNetworkId = NetworkId, - CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), - TargetBagNetworkId = NetworkId, - RuntimeIdHash = 0, - Action = EquipmentAction.UnequipFromIndex, - SlotOrIndex = index - }; - - m_PendingEquipment[GetPendingKey(request.ActorNetworkId, request.CorrelationId, request.RequestId)] = new PendingEquipment - { - Request = request, - SentTime = Time.time - }; - - OnEquipmentRequested?.Invoke(request); - - if (m_IsServer) - { - _ = ProcessLocalEquipmentRequestAsync(request); - } - else - { - NetworkInventoryManager.Instance?.SendEquipmentRequest(request); - } - } - - #endregion - - #region Socket Requests - - /// - /// Request to attach item to socket. - /// - public void RequestAttachToSocket(RuntimeItem parent, RuntimeItem attachment, IdString socketId = default) - { - if (m_IsRemoteClient) return; - if (parent == null || attachment == null) return; - - var request = new NetworkSocketRequest - { - RequestId = GetNextRequestId(), - ActorNetworkId = NetworkId, - CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), - TargetBagNetworkId = NetworkId, - ParentRuntimeIdHash = parent.RuntimeID.Hash, - AttachmentRuntimeIdHash = attachment.RuntimeID.Hash, - SocketHash = socketId.Hash, - SocketIdString = socketId.String, - Action = socketId.Hash != 0 ? SocketAction.AttachToSocket : SocketAction.Attach - }; - - OnSocketRequested?.Invoke(request); - - if (m_IsServer) - { - var response = ProcessSocketRequest(request, NetworkId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - ReceiveSocketResponse(response); - } - else - { - NetworkInventoryManager.Instance?.SendSocketRequest(request); - } - } - - /// - /// Request to detach from socket. - /// - public void RequestDetachFromSocket(RuntimeItem parent, IdString socketId) - { - if (m_IsRemoteClient) return; - if (parent == null) return; - - var request = new NetworkSocketRequest - { - RequestId = GetNextRequestId(), - ActorNetworkId = NetworkId, - CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), - TargetBagNetworkId = NetworkId, - ParentRuntimeIdHash = parent.RuntimeID.Hash, - SocketHash = socketId.Hash, - SocketIdString = socketId.String, - Action = SocketAction.DetachFromSocket - }; - - OnSocketRequested?.Invoke(request); - - if (m_IsServer) - { - var response = ProcessSocketRequest(request, NetworkId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - ReceiveSocketResponse(response); - } - else - { - NetworkInventoryManager.Instance?.SendSocketRequest(request); - } - } - - /// - /// Request to detach a specific attached item from its parent. - /// - public void RequestDetachFromSocket(RuntimeItem parent, RuntimeItem attachment) - { - if (m_IsRemoteClient) return; - if (parent == null || attachment == null) return; - - var request = new NetworkSocketRequest - { - RequestId = GetNextRequestId(), - ActorNetworkId = NetworkId, - CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), - TargetBagNetworkId = NetworkId, - ParentRuntimeIdHash = parent.RuntimeID.Hash, - AttachmentRuntimeIdHash = attachment.RuntimeID.Hash, - Action = SocketAction.Detach - }; - - OnSocketRequested?.Invoke(request); - - if (m_IsServer) - { - var response = ProcessSocketRequest(request, NetworkId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - ReceiveSocketResponse(response); - } - else - { - NetworkInventoryManager.Instance?.SendSocketRequest(request); - } - } - - #endregion - - #region Wealth Requests - - /// - /// Request to modify wealth. - /// - public void RequestWealthModify(Currency currency, int value, WealthAction action, - InventoryModificationSource source = InventoryModificationSource.Direct, int sourceHash = 0) - { - if (m_IsRemoteClient) return; - if (currency == null) return; - - var request = new NetworkWealthRequest - { - RequestId = GetNextRequestId(), - ActorNetworkId = NetworkId, - CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), - TargetBagNetworkId = NetworkId, - CurrencyHash = currency.ID.Hash, - CurrencyIdString = currency.ID.String, - Value = value, - Action = action, - Source = source, - SourceHash = sourceHash - }; - - int originalValue = m_Bag.Wealth.Get(currency); - - m_PendingWealth[GetPendingKey(request.ActorNetworkId, request.CorrelationId, request.RequestId)] = new PendingWealth - { - Request = request, - OriginalValue = originalValue, - SentTime = Time.time - }; - - OnWealthRequested?.Invoke(request); - - if (m_IsServer) - { - var response = ProcessWealthRequest(request, NetworkId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - ReceiveWealthResponse(response); - } - else - { - NetworkInventoryManager.Instance?.SendWealthRequest(request); - } - } - - #endregion - - // ════════════════════════════════════════════════════════════════════════════════════════ - // CLIENT-SIDE: RECEIVE RESPONSES - // ════════════════════════════════════════════════════════════════════════════════════════ - - #region Client Response Handlers - - public void ReceiveContentAddResponse(NetworkContentAddResponse response) - { - ulong key = GetPendingKey(response.ActorNetworkId, response.CorrelationId, response.RequestId); - if (!m_PendingAdds.TryGetValue(key, out var pending)) - return; - - m_PendingAdds.Remove(key); - - if (!response.Authorized) - { - if (m_LogRejections) - Debug.LogWarning($"[NetworkInventoryController] Add rejected: {response.RejectionReason}"); - OnOperationRejected?.Invoke(response.RejectionReason, "Add item"); - } - } - - public void ReceiveContentRemoveResponse(NetworkContentRemoveResponse response) - { - ulong key = GetPendingKey(response.ActorNetworkId, response.CorrelationId, response.RequestId); - if (!m_PendingRemoves.TryGetValue(key, out var pending)) - return; - - m_PendingRemoves.Remove(key); - - if (!response.Authorized) - { - if (m_LogRejections) - Debug.LogWarning($"[NetworkInventoryController] Remove rejected: {response.RejectionReason}"); - OnOperationRejected?.Invoke(response.RejectionReason, "Remove item"); - } - } - - public void ReceiveContentMoveResponse(NetworkContentMoveResponse response) - { - ulong key = GetPendingKey(response.ActorNetworkId, response.CorrelationId, response.RequestId); - if (!m_PendingMoves.TryGetValue(key, out var pending)) - return; - - m_PendingMoves.Remove(key); - - if (!response.Authorized) - { - if (m_LogRejections) - Debug.LogWarning($"[NetworkInventoryController] Move rejected: {response.RejectionReason}"); - OnOperationRejected?.Invoke(response.RejectionReason, "Move item"); - } - } - - public void ReceiveContentUseResponse(NetworkContentUseResponse response) - { - if (!response.Authorized) - { - if (m_LogRejections) - Debug.LogWarning($"[NetworkInventoryController] Use rejected: {response.RejectionReason}"); - OnOperationRejected?.Invoke(response.RejectionReason, "Use item"); - } - } - - public void ReceiveContentDropResponse(NetworkContentDropResponse response) - { - if (!response.Authorized) - { - if (m_LogRejections) - Debug.LogWarning($"[NetworkInventoryController] Drop rejected: {response.RejectionReason}"); - OnOperationRejected?.Invoke(response.RejectionReason, "Drop item"); - } - } - - public void ReceiveEquipmentResponse(NetworkEquipmentResponse response) - { - ulong key = GetPendingKey(response.ActorNetworkId, response.CorrelationId, response.RequestId); - if (!m_PendingEquipment.TryGetValue(key, out var pending)) - return; - - m_PendingEquipment.Remove(key); - - if (!response.Authorized) - { - if (m_LogRejections) - Debug.LogWarning($"[NetworkInventoryController] Equipment rejected: {response.RejectionReason}"); - OnOperationRejected?.Invoke(response.RejectionReason, "Equipment operation"); - } - } - - public void ReceiveSocketResponse(NetworkSocketResponse response) - { - if (!response.Authorized) - { - if (m_LogRejections) - Debug.LogWarning($"[NetworkInventoryController] Socket rejected: {response.RejectionReason}"); - OnOperationRejected?.Invoke(response.RejectionReason, "Socket operation"); - } - } - - public void ReceiveWealthResponse(NetworkWealthResponse response) - { - ulong key = GetPendingKey(response.ActorNetworkId, response.CorrelationId, response.RequestId); - if (!m_PendingWealth.TryGetValue(key, out var pending)) - return; - - m_PendingWealth.Remove(key); - - if (!response.Authorized) - { - if (m_LogRejections) - Debug.LogWarning($"[NetworkInventoryController] Wealth rejected: {response.RejectionReason}"); - OnOperationRejected?.Invoke(response.RejectionReason, "Wealth operation"); - } - } - - #endregion - - // ════════════════════════════════════════════════════════════════════════════════════════ - // LOCAL CHANGE DETECTION - // ════════════════════════════════════════════════════════════════════════════════════════ - - private void OnLocalItemAdded(RuntimeItem item) - { - if (item != null) - { - TrackRuntimeItemRecursive(item); - } - - LogPickupDebug( - $"{name}: local add observed item={DescribeRuntimeItem(item)} bag={NetworkId} server={m_IsServer} local={m_IsLocalClient} remote={m_IsRemoteClient} applying={m_IsApplyingNetworkState} " + - $"hasDroppedInstance={(item != null && s_DroppedItemInstances.ContainsKey(item.RuntimeID.Hash))} position={(item != null ? m_Bag.Content.FindPosition(item.RuntimeID).ToString() : "n/a")}", - this); - - if (m_IsServer && !m_IsApplyingNetworkState && item != null) - { - BroadcastServerPickupFromDroppedItemIfNeeded(item); - } - else if (!m_IsServer && !m_IsApplyingNetworkState) - { - if (!TrySendPickupForLocalAdd(item)) - { - TrySendTransferForLocalAdd(item); - } - } - - if (m_LogAllChanges && !m_IsServer) - Debug.Log($"[NetworkInventoryController] Local item added: {item?.ItemID.String}"); - } - - private void OnLocalItemRemoved(RuntimeItem item) - { - if (item != null) - { - if (ContainsRuntimeItemRecursive(item.RuntimeID.Hash)) - { - TrackRuntimeItemRecursive(item); - } - else - { - UntrackRuntimeItemRecursive(item); - } - } - - if (!m_IsApplyingNetworkState) - { - RememberLocalRemoval(this, item); - } - - if (m_LogAllChanges && !m_IsServer) - Debug.Log($"[NetworkInventoryController] Local item removed: {item?.ItemID.String}"); - } - - private void OnLocalItemUsed(RuntimeItem item) - { - if (!m_IsServer && !m_IsApplyingNetworkState && item != null && m_IsLocalClient) - { - RequestUseItem(item); - } - - if (m_LogAllChanges && !m_IsServer) - Debug.Log($"[NetworkInventoryController] Local item used: {item?.ItemID.String}"); - } - - private void OnLocalItemEquipped(RuntimeItem item, int index) - { - if (!m_IsServer && !m_IsApplyingNetworkState && item != null && m_IsLocalClient) - { - RequestEquipToIndexFromLocalEvent(item, index); - } - - if (m_LogAllChanges && !m_IsServer) - Debug.Log($"[NetworkInventoryController] Local item equipped: {item?.ItemID.String} at {index}"); - } - - private void OnLocalItemUnequipped(RuntimeItem item, int index) - { - if (!m_IsServer && !m_IsApplyingNetworkState && m_IsLocalClient) - { - RequestUnequipFromIndex(index); - } - - if (m_LogAllChanges && !m_IsServer) - Debug.Log($"[NetworkInventoryController] Local item unequipped: {item?.ItemID.String} from {index}"); - } - - private void OnLocalWealthChanged(IdString currencyId, int oldValue, int newValue) - { - if (m_LogAllChanges && !m_IsServer) - Debug.Log($"[NetworkInventoryController] Local wealth changed: {currencyId.String} {oldValue} -> {newValue}"); - } - - private async Task ProcessLocalEquipmentRequestAsync(NetworkEquipmentRequest request) - { - NetworkEquipmentResponse response = await ProcessEquipmentRequest(request, NetworkId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - ReceiveEquipmentResponse(response); - } - - private void RequestEquipToIndexFromLocalEvent(RuntimeItem runtimeItem, int index) - { - if (runtimeItem == null) return; - - var request = new NetworkEquipmentRequest - { - RequestId = GetNextRequestId(), - ActorNetworkId = NetworkId, - CorrelationId = NetworkCorrelation.Compose(NetworkId, m_LastIssuedRequestId), - TargetBagNetworkId = NetworkId, - RuntimeIdHash = runtimeItem.RuntimeID.Hash, - Action = EquipmentAction.EquipToIndex, - SlotOrIndex = index - }; - - m_PendingEquipment[GetPendingKey(request.ActorNetworkId, request.CorrelationId, request.RequestId)] = new PendingEquipment - { - Request = request, - SentTime = Time.time - }; - - OnEquipmentRequested?.Invoke(request); - NetworkInventoryManager.Instance?.SendEquipmentRequest(request); - } - - private void TrySendTransferForLocalAdd(RuntimeItem item) - { - if (item == null) return; - if (!TryGetLocalActorNetworkId(out uint actorNetworkId)) return; - if (!TryTakePendingRemoval(item.RuntimeID.Hash, out PendingLocalRemoval removal)) return; - if (removal.SourceController == null || removal.SourceController == this) return; - if (removal.SourceController.NetworkId == 0 || NetworkId == 0) return; - - LogPickupDebug( - $"{name}: sending transfer fallback for local add item={DescribeRuntimeItem(item)} actor={actorNetworkId} sourceBag={removal.SourceController.NetworkId} destinationBag={NetworkId}", - this); - - var request = new NetworkTransferRequest - { - RequestId = GetNextRequestId(), - ActorNetworkId = actorNetworkId, - CorrelationId = NetworkCorrelation.Compose(actorNetworkId, m_LastIssuedRequestId), - SourceBagNetworkId = removal.SourceController.NetworkId, - DestinationBagNetworkId = NetworkId, - RuntimeIdHash = item.RuntimeID.Hash, - DestinationPosition = m_Bag.Content.FindPosition(item.RuntimeID), - AllowStack = true, - Source = InventoryModificationSource.Loot - }; - - NetworkInventoryManager.Instance?.SendTransferRequest(request); - } - - private bool TrySendPickupForLocalAdd(RuntimeItem item) - { - if (item == null) return false; - - bool hasDroppedInstance = TryGetDroppedItemInstance(item.RuntimeID.Hash, out DroppedItemInstance droppedItem); - bool exactRuntimeMatch = hasDroppedInstance; - if (!hasDroppedInstance) - { - hasDroppedInstance = TryFindDroppedItemInstanceForLocalPickup(item, out droppedItem); - } - - if (!m_IsLocalClient || !UsesNetworkCharacterId) - { - if (hasDroppedInstance) - { - LogPickupWarning( - $"{name}: pickup request skipped because controller is not a local network-character inventory item={DescribeRuntimeItem(item)} local={m_IsLocalClient} usesCharacterId={UsesNetworkCharacterId} bag={NetworkId}", - this); - } - return false; - } - - if (!TryGetLocalActorNetworkId(out uint actorNetworkId)) - { - LogPickupWarning( - $"{name}: pickup request skipped because no local actor network id was found item={DescribeRuntimeItem(item)} bag={NetworkId}", - this); - return false; - } - - if (!hasDroppedInstance) - { - LogPickupDebug( - $"{name}: pickup request skipped because local add is not a tracked network drop item={DescribeRuntimeItem(item)} bag={NetworkId} trackedDrops={s_DroppedItemInstances.Count}", - this); - return false; - } - - var request = new NetworkPickupRequest - { - RequestId = GetNextRequestId(), - ActorNetworkId = actorNetworkId, - CorrelationId = NetworkCorrelation.Compose(actorNetworkId, m_LastIssuedRequestId), - PickerBagNetworkId = NetworkId, - SourceBagNetworkId = droppedItem.SourceBagNetworkId, - RuntimeIdHash = droppedItem.Item.RuntimeIdHash, - DestinationPosition = m_Bag.Content.FindPosition(item.RuntimeID) - }; - - if (droppedItem.Item.RuntimeIdHash != 0 && droppedItem.Item.RuntimeIdHash != item.RuntimeID.Hash) - { - m_PendingPickupLocalRuntimeByServerRuntime[droppedItem.Item.RuntimeIdHash] = item.RuntimeID.Hash; - } - - LogPickupDebug( - $"{name}: sending pickup request req={request.RequestId} actor={actorNetworkId} pickerBag={NetworkId} sourceBag={droppedItem.SourceBagNetworkId} localItem={DescribeRuntimeItem(item)} serverRuntime={droppedItem.Item.RuntimeIdHash} exactRuntimeMatch={exactRuntimeMatch} destination={request.DestinationPosition} dropPosition={droppedItem.Position} instanceAlive={droppedItem.Instance != null}", - this); - - NetworkInventoryManager.Instance?.SendPickupRequest(request); - return true; - } - - private static void RememberLocalRemoval(NetworkInventoryController source, RuntimeItem item) - { - if (source == null || item == null) return; - if (!source.m_IsServer && !TryGetLocalActorNetworkId(out _)) return; - - PrunePendingLocalRemovals(); - long runtimeIdHash = item.RuntimeID.Hash; - - for (int i = s_PendingLocalRemovals.Count - 1; i >= 0; i--) - { - if (s_PendingLocalRemovals[i].RuntimeIdHash == runtimeIdHash) - { - s_PendingLocalRemovals.RemoveAt(i); - } - } - - s_PendingLocalRemovals.Add(new PendingLocalRemoval - { - SourceController = source, - Item = source.ConvertToNetworkItem(item), - RuntimeIdHash = runtimeIdHash, - Time = Time.unscaledTime - }); - } - - private static bool TryTakePendingRemoval(long runtimeIdHash, out PendingLocalRemoval removal) - { - PrunePendingLocalRemovals(); - - for (int i = 0; i < s_PendingLocalRemovals.Count; i++) - { - if (s_PendingLocalRemovals[i].RuntimeIdHash != runtimeIdHash) continue; - - removal = s_PendingLocalRemovals[i]; - s_PendingLocalRemovals.RemoveAt(i); - return true; - } - - removal = default; - return false; - } - - private static bool TryPeekPendingRemoval(long runtimeIdHash, out PendingLocalRemoval removal) - { - PrunePendingLocalRemovals(); - - for (int i = 0; i < s_PendingLocalRemovals.Count; i++) - { - if (s_PendingLocalRemovals[i].RuntimeIdHash != runtimeIdHash) continue; - - removal = s_PendingLocalRemovals[i]; - return true; - } - - removal = default; - return false; - } - - private static void PrunePendingLocalRemovals() - { - float now = Time.unscaledTime; - for (int i = s_PendingLocalRemovals.Count - 1; i >= 0; i--) - { - if (now - s_PendingLocalRemovals[i].Time <= 2f) continue; - s_PendingLocalRemovals.RemoveAt(i); - } - } - - private static bool TryGetLocalActorNetworkId(out uint actorNetworkId) - { - if (s_LocalPlayerController != null && - s_LocalPlayerController.NetworkId != 0 && - s_LocalPlayerController.m_IsLocalClient) - { - actorNetworkId = s_LocalPlayerController.NetworkId; - return true; - } - - for (int i = 0; i < s_Controllers.Count; i++) - { - NetworkInventoryController controller = s_Controllers[i]; - if (controller == null || !controller.m_IsLocalClient || !controller.UsesNetworkCharacterId) continue; - if (controller.NetworkId == 0) continue; - - s_LocalPlayerController = controller; - actorNetworkId = controller.NetworkId; - return true; - } - - actorNetworkId = 0; - return false; - } - - private static void HandleGlobalItemInstantiated() - { - RuntimeItem item = Item.LastItemInstantiated; - GameObject instance = Item.LastItemInstanceInstantiated; - if (item == null || instance == null) return; - if (!TryPeekPendingRemoval(item.RuntimeID.Hash, out PendingLocalRemoval removal)) return; - if (removal.SourceController == null) return; - - if (removal.SourceController.m_IsServer) - { - RememberDroppedItemInstance(item.RuntimeID.Hash, instance, removal.SourceController.NetworkId, removal.Item, instance.transform.position); - LogPickupDebug( - $"global instantiated server-side drop item={DescribeRuntimeItem(item)} sourceBag={removal.SourceController.NetworkId} position={instance.transform.position}", - instance); - removal.SourceController.BroadcastServerDropFromLocalMutation(removal.Item, item.RuntimeID.Hash, instance.transform.position); - TryTakePendingRemoval(item.RuntimeID.Hash, out _); - return; - } - - if (!TryGetLocalActorNetworkId(out uint actorNetworkId)) return; - - s_LocalDropRuntimeIds.Add(item.RuntimeID.Hash); - RememberDroppedItemInstance(item.RuntimeID.Hash, instance, removal.SourceController.NetworkId, removal.Item, instance.transform.position); - LogPickupDebug( - $"global instantiated client-side drop item={DescribeRuntimeItem(item)} sourceBag={removal.SourceController.NetworkId} actor={actorNetworkId} position={instance.transform.position}", - instance); - removal.SourceController.SendDropRequest( - actorNetworkId, - removal.SourceController.NetworkId, - item, - instance.transform.position, - 1); - - TryTakePendingRemoval(item.RuntimeID.Hash, out _); - } - - private static void RememberDroppedItemInstance( - long runtimeIdHash, - GameObject instance, - uint sourceBagNetworkId, - NetworkRuntimeItem item, - Vector3 position) - { - if (runtimeIdHash == 0 || instance == null) return; - - if (s_DroppedItemInstances.TryGetValue(runtimeIdHash, out DroppedItemInstance previous) && - previous.Instance != null && - previous.Instance != instance) - { - LogPickupDebug( - $"replacing tracked dropped instance runtime={runtimeIdHash} old={previous.Instance.name} new={instance.name} sourceBag={sourceBagNetworkId}"); - UnityEngine.Object.Destroy(previous.Instance); - } - - s_DroppedItemInstances[runtimeIdHash] = new DroppedItemInstance - { - Instance = instance, - SourceBagNetworkId = sourceBagNetworkId, - Item = item, - Position = position - }; - - LogPickupDebug( - $"remembered dropped instance runtime={runtimeIdHash} sourceBag={sourceBagNetworkId} item={DescribeNetworkItem(item)} instance={instance.name} position={position} trackedDrops={s_DroppedItemInstances.Count}"); - } - - private static bool TryAdoptPredictedDroppedItemInstance(NetworkItemDroppedBroadcast broadcast) - { - long serverRuntimeIdHash = broadcast.Item.RuntimeIdHash; - if (serverRuntimeIdHash != 0 && s_LocalDropRuntimeIds.Remove(serverRuntimeIdHash)) - { - if (s_DroppedItemInstances.TryGetValue(serverRuntimeIdHash, out DroppedItemInstance exactDrop) && - exactDrop.Instance != null) - { - s_DroppedItemInstances[serverRuntimeIdHash] = new DroppedItemInstance - { - Instance = exactDrop.Instance, - SourceBagNetworkId = broadcast.SourceBagNetworkId, - Item = broadcast.Item, - Position = broadcast.Position - }; - } - - LogPickupDebug( - $"adopted server dropped broadcast for exact local predicted drop runtime={serverRuntimeIdHash} sourceBag={broadcast.SourceBagNetworkId} item={DescribeNetworkItem(broadcast.Item)}"); - return true; - } - - if (serverRuntimeIdHash == 0 || s_LocalDropRuntimeIds.Count == 0) - { - return false; - } - - long bestLocalRuntimeIdHash = 0; - DroppedItemInstance bestDrop = default; - float bestDistance = float.MaxValue; - - foreach (KeyValuePair entry in s_DroppedItemInstances) - { - if (!s_LocalDropRuntimeIds.Contains(entry.Key)) continue; - - DroppedItemInstance candidate = entry.Value; - if (candidate.Item.ItemHash != broadcast.Item.ItemHash) continue; - if (broadcast.SourceBagNetworkId != 0 && - candidate.SourceBagNetworkId != 0 && - candidate.SourceBagNetworkId != broadcast.SourceBagNetworkId) - { - continue; - } - - Vector3 candidatePosition = candidate.Instance != null - ? candidate.Instance.transform.position - : candidate.Position; - float distance = Vector3.SqrMagnitude(candidatePosition - broadcast.Position); - if (distance >= bestDistance) continue; - - bestDistance = distance; - bestLocalRuntimeIdHash = entry.Key; - bestDrop = candidate; - } - - if (bestLocalRuntimeIdHash == 0 || bestDrop.Instance == null || bestDistance > 16f) - { - return false; - } - - s_LocalDropRuntimeIds.Remove(bestLocalRuntimeIdHash); - s_DroppedItemInstances.Remove(bestLocalRuntimeIdHash); - RememberDroppedItemInstance( - serverRuntimeIdHash, - bestDrop.Instance, - broadcast.SourceBagNetworkId, - broadcast.Item, - broadcast.Position); - - LogPickupDebug( - $"adopted server dropped broadcast by remapping local predicted drop localRuntime={bestLocalRuntimeIdHash} serverRuntime={serverRuntimeIdHash} sourceBag={broadcast.SourceBagNetworkId} distance={Mathf.Sqrt(bestDistance):0.00} item={DescribeNetworkItem(broadcast.Item)}", - bestDrop.Instance); - return true; - } - - private static bool TryGetDroppedItemInstance(long runtimeIdHash, out DroppedItemInstance droppedItem) - { - if (runtimeIdHash != 0 && s_DroppedItemInstances.TryGetValue(runtimeIdHash, out droppedItem)) - { - return true; - } - - droppedItem = default; - return false; - } - - private bool TryFindDroppedItemInstanceForLocalPickup(RuntimeItem localItem, out DroppedItemInstance droppedItem) - { - droppedItem = default; - if (localItem?.Item == null || s_DroppedItemInstances.Count == 0) return false; - - int itemHash = localItem.ItemID.Hash; - Vector3 pickerPosition = transform.position; - float bestDistance = float.MaxValue; - bool found = false; - - foreach (var entry in s_DroppedItemInstances) - { - DroppedItemInstance candidate = entry.Value; - if (candidate.Item.ItemHash != itemHash) continue; - - Vector3 candidatePosition = candidate.Instance != null - ? candidate.Instance.transform.position - : candidate.Position; - - float distance = Vector3.SqrMagnitude(candidatePosition - pickerPosition); - if (distance >= bestDistance) continue; - - bestDistance = distance; - droppedItem = candidate; - found = true; - } - - if (found) - { - LogPickupDebug( - $"{name}: matched local pickup by item type localItem={DescribeRuntimeItem(localItem)} serverItem={DescribeNetworkItem(droppedItem.Item)} sourceBag={droppedItem.SourceBagNetworkId} distance={Mathf.Sqrt(bestDistance):0.00}", - this); - } - - return found; - } - - private static bool TryDestroyDroppedItemInstance(long runtimeIdHash) - { - if (runtimeIdHash == 0) return false; - if (!s_DroppedItemInstances.TryGetValue(runtimeIdHash, out DroppedItemInstance droppedItem)) return false; - - s_DroppedItemInstances.Remove(runtimeIdHash); - s_LocalDropRuntimeIds.Remove(runtimeIdHash); - GameObject instance = droppedItem.Instance; - if (instance == null) return false; - - LogPickupDebug( - $"destroying tracked dropped instance runtime={runtimeIdHash} sourceBag={droppedItem.SourceBagNetworkId} instance={instance.name} remainingTrackedDrops={s_DroppedItemInstances.Count}", - instance); - UnityEngine.Object.Destroy(instance); - return true; - } - - private static bool TryDestroyDroppedItemInstance(NetworkDroppedItemRemovedBroadcast broadcast, out long destroyedRuntimeIdHash) - { - destroyedRuntimeIdHash = broadcast.RuntimeIdHash; - if (TryDestroyDroppedItemInstance(broadcast.RuntimeIdHash)) - { - return true; - } - - destroyedRuntimeIdHash = 0; - if (s_DroppedItemInstances.Count == 0) - { - return false; - } - - long bestRuntimeIdHash = 0; - DroppedItemInstance bestDrop = default; - float bestDistance = float.MaxValue; - bool foundSameSource = false; - - foreach (KeyValuePair entry in s_DroppedItemInstances) - { - DroppedItemInstance candidate = entry.Value; - bool sameSource = broadcast.SourceBagNetworkId == 0 || - candidate.SourceBagNetworkId == 0 || - candidate.SourceBagNetworkId == broadcast.SourceBagNetworkId; - - if (foundSameSource && !sameSource) continue; - if (!foundSameSource && sameSource) - { - foundSameSource = true; - bestDistance = float.MaxValue; - bestRuntimeIdHash = 0; - bestDrop = default; - } - - Vector3 candidatePosition = candidate.Instance != null - ? candidate.Instance.transform.position - : candidate.Position; - float distance = Vector3.SqrMagnitude(candidatePosition - broadcast.Position); - if (distance >= bestDistance) continue; - - bestDistance = distance; - bestRuntimeIdHash = entry.Key; - bestDrop = candidate; - } - - float maxDistance = foundSameSource ? 16f : 2.25f; - if (bestRuntimeIdHash == 0 || bestDrop.Instance == null || bestDistance > maxDistance) - { - return false; - } - - destroyedRuntimeIdHash = bestRuntimeIdHash; - return TryDestroyDroppedItemInstance(bestRuntimeIdHash); - } - - private static void HandleGlobalSocketAttached(RuntimeItem parent, RuntimeItem attachment) - { - if (parent == null || attachment == null) return; - NetworkInventoryController controller = FindControllerOwningRuntimeItem(parent.RuntimeID.Hash); - if (controller == null || controller.m_IsApplyingNetworkState) return; - if (!TryFindAttachedSocketId(parent, attachment, out IdString socketId)) socketId = IdString.EMPTY; - - if (controller.m_IsServer) - { - controller.BroadcastServerSocketAttach(parent, attachment, socketId); - return; - } - - if (!controller.m_IsLocalClient) return; - controller.RequestAttachToSocket(parent, attachment, socketId); - } - - private static void HandleGlobalSocketDetached(RuntimeItem parent, RuntimeItem attachment) - { - if (parent == null || attachment == null) return; - NetworkInventoryController controller = FindControllerOwningRuntimeItem(parent.RuntimeID.Hash); - if (controller == null || controller.m_IsApplyingNetworkState) return; - - if (controller.m_IsServer) - { - controller.BroadcastServerSocketDetach(parent); - return; - } - - if (!controller.m_IsLocalClient) return; - - controller.RequestDetachFromSocket(parent, attachment); - } - - private static NetworkInventoryController FindControllerOwningRuntimeItem(long runtimeIdHash) - { - for (int i = 0; i < s_Controllers.Count; i++) - { - NetworkInventoryController controller = s_Controllers[i]; - if (controller == null) continue; - if (controller.ContainsRuntimeItemRecursive(runtimeIdHash)) return controller; - } - - return null; - } - - private static bool TryFindAttachedSocketId(RuntimeItem parent, RuntimeItem attachment, out IdString socketId) - { - socketId = IdString.EMPTY; - if (parent == null || attachment == null) return false; - - foreach (var socketEntry in parent.Sockets) - { - RuntimeSocket socket = socketEntry.Value; - if (socket == null || !socket.HasAttachment) continue; - if (socket.Attachment.RuntimeID.Hash != attachment.RuntimeID.Hash) continue; - - socketId = socketEntry.Key; - return true; - } - - return false; - } - } -} -#endif diff --git a/NetworkInventoryController.Server.SyncAndHelpers.cs b/NetworkInventoryController.Server.SyncAndHelpers.cs deleted file mode 100644 index a851060..0000000 --- a/NetworkInventoryController.Server.SyncAndHelpers.cs +++ /dev/null @@ -1,924 +0,0 @@ -#if GC2_INVENTORY -using System; -using System.Collections.Generic; -using UnityEngine; -using GameCreator.Runtime.Common; -using GameCreator.Runtime.Inventory; - -namespace Arawn.GameCreator2.Networking.Inventory -{ - // ════════════════════════════════════════════════════════════════════════════════════════════ - // SERVER-SIDE — Sync, snapshot, and runtime-item helper methods - // ════════════════════════════════════════════════════════════════════════════════════════════ - - public partial class NetworkInventoryController - { - // SERVER BROADCASTING - // ════════════════════════════════════════════════════════════════════════════════════════ - - private void BroadcastFullState() - { - var snapshot = GetFullSnapshot(); - NetworkInventoryManager.Instance?.BroadcastFullSnapshot(snapshot); - } - - private void BroadcastDeltaState() - { - bool cellsChanged = HasInventoryPositionStateChanged(); - bool equipmentChanged = HasEquipmentStateChanged(); - bool wealthChanged = HasWealthStateChanged(); - - if (!cellsChanged && !equipmentChanged && !wealthChanged) - { - return; - } - - const uint maskCells = 1u << 0; - const uint maskEquipment = 1u << 1; - const uint maskWealth = 1u << 2; - - var delta = new NetworkInventoryDelta - { - BagNetworkId = NetworkId, - Timestamp = Time.time, - ChangeMask = (cellsChanged ? maskCells : 0u) | - (equipmentChanged ? maskEquipment : 0u) | - (wealthChanged ? maskWealth : 0u), - ChangedCells = cellsChanged ? BuildChangedCellDelta() : Array.Empty(), - ChangedEquipment = equipmentChanged ? BuildChangedEquipmentDelta() : Array.Empty(), - ChangedWealth = wealthChanged ? BuildChangedWealthDelta() : Array.Empty() - }; - - NetworkInventoryManager.Instance?.BroadcastDelta(delta); - CacheCurrentSyncState(); - - if (m_LogAllChanges) - { - Debug.Log( - $"[NetworkInventoryController] Broadcasted delta update (mask={delta.ChangeMask}) " + - $"cells={delta.ChangedCells.Length} equipment={delta.ChangedEquipment.Length} wealth={delta.ChangedWealth.Length}"); - } - } - - /// - /// Get full inventory snapshot for initial sync. - /// - public NetworkInventorySnapshot GetFullSnapshot() - { - var cells = new List(); - var equipment = new List(); - var wealth = new List(); - - // Collect cells - foreach (var cell in m_Bag.Content.CellList) - { - if (cell == null || cell.Available) continue; - - var position = m_Bag.Content.FindPosition(cell.RootRuntimeItemID); - GetStackedRuntimeIdentity(cell, out long[] stackedRuntimeIds, out string[] stackedRuntimeIdStrings); - - cells.Add(new NetworkCell - { - Position = position, - ItemHash = cell.Item.ID.Hash, - StackCount = cell.Count, - RootItem = ConvertToNetworkItem(cell.RootRuntimeItem), - StackedRuntimeIds = stackedRuntimeIds, - StackedRuntimeIdStrings = stackedRuntimeIdStrings - }); - } - - // Collect equipment - for (int i = 0; i < m_Bag.Equipment.Count; i++) - { - var slotId = m_Bag.Equipment.GetSlotRootRuntimeItemID(i); - var baseId = m_Bag.Equipment.GetSlotBaseID(i); - - equipment.Add(new NetworkEquipmentSlot - { - SlotIndex = i, - BaseItemHash = baseId.Hash, - IsOccupied = !string.IsNullOrEmpty(slotId.String), - EquippedRuntimeIdHash = slotId.Hash - }); - } - - // Collect wealth - foreach (var currencyId in m_Bag.Wealth.List) - { - wealth.Add(new NetworkWealthEntry - { - CurrencyHash = currencyId.Hash, - Amount = m_Bag.Wealth.Get(currencyId) - }); - } - - return new NetworkInventorySnapshot - { - BagNetworkId = NetworkId, - Timestamp = Time.time, - Cells = cells.ToArray(), - Equipment = equipment.ToArray(), - Wealth = wealth.ToArray() - }; - } - - // ════════════════════════════════════════════════════════════════════════════════════════ - // HELPER METHODS - // ════════════════════════════════════════════════════════════════════════════════════════ - - private void CleanupPendingRequests() - { - float timeout = 5f; - float currentTime = Time.time; - - CleanupPendingBucket(m_PendingAdds, currentTime, timeout, "Add item"); - CleanupPendingBucket(m_PendingRemoves, currentTime, timeout, "Remove item"); - CleanupPendingBucket(m_PendingMoves, currentTime, timeout, "Move item"); - CleanupPendingBucket(m_PendingEquipment, currentTime, timeout, "Equipment operation"); - CleanupPendingBucket(m_PendingWealth, currentTime, timeout, "Wealth operation"); - - void CleanupPendingBucket(Dictionary pending, float now, float timeoutSeconds, string operationName) - where T : struct, ITimedPendingRequest - { - int removedCount = PendingRequestCleanup.RemoveTimedOut( - pending, - s_SharedKeyBuffer, - now, - timeoutSeconds); - - if (removedCount <= 0) return; - - if (m_LogRejections) - { - Debug.LogWarning($"[NetworkInventoryController] {operationName} timed out ({removedCount} pending request(s) dropped)."); - } - - if (!m_IsServer) - { - OnOperationRejected?.Invoke(InventoryRejectionReason.RequestTimeout, operationName); - } - } - } - - private bool HasInventoryPositionStateChanged() - { - Dictionary current = BuildCurrentPositionState(); - return !DictionariesEqual(m_LastSyncedPositions, current); - } - - private bool HasEquipmentStateChanged() - { - var current = new Dictionary(Mathf.Max(1, m_Bag.Equipment.Count)); - for (int i = 0; i < m_Bag.Equipment.Count; i++) - { - current[i] = m_Bag.Equipment.GetSlotRootRuntimeItemID(i).Hash; - } - - return !DictionariesEqual(m_LastSyncedEquipment, current); - } - - private bool HasWealthStateChanged() - { - var current = new Dictionary(8); - foreach (IdString currencyId in m_Bag.Wealth.List) - { - current[currencyId.Hash] = m_Bag.Wealth.Get(currencyId); - } - - return !DictionariesEqual(m_LastSyncedWealth, current); - } - - private Dictionary BuildCurrentPositionState() - { - var current = new Dictionary(m_RuntimeItemMap.Count); - foreach (Cell cell in m_Bag.Content.CellList) - { - if (cell == null || cell.Available) continue; - - Vector2Int position = m_Bag.Content.FindPosition(cell.RootRuntimeItemID); - foreach (IdString runtimeId in cell.List) - { - current[runtimeId.Hash] = position; - } - } - - return current; - } - - private NetworkCell[] BuildChangedCellDelta() - { - Dictionary currentPositions = BuildCurrentPositionState(); - var changedPositions = new HashSet(); - - foreach (KeyValuePair entry in currentPositions) - { - if (!m_LastSyncedPositions.TryGetValue(entry.Key, out Vector2Int previousPosition) || - previousPosition != entry.Value) - { - changedPositions.Add(entry.Value); - } - } - - foreach (KeyValuePair entry in m_LastSyncedPositions) - { - if (!currentPositions.ContainsKey(entry.Key)) - { - changedPositions.Add(entry.Value); - } - } - - if (changedPositions.Count == 0) return Array.Empty(); - - var orderedPositions = new List(changedPositions); - orderedPositions.Sort((left, right) => - { - int x = left.x.CompareTo(right.x); - return x != 0 ? x : left.y.CompareTo(right.y); - }); - - var changedCells = new List(orderedPositions.Count); - foreach (Vector2Int position in orderedPositions) - { - Cell cell = m_Bag.Content.GetContent(position); - if (cell == null || cell.Available) - { - changedCells.Add(new NetworkCell - { - Position = position, - ItemHash = 0, - StackCount = 0, - RootItem = default, - StackedRuntimeIds = Array.Empty(), - StackedRuntimeIdStrings = Array.Empty() - }); - continue; - } - - GetStackedRuntimeIdentity(cell, out long[] stackedRuntimeIds, out string[] stackedRuntimeIdStrings); - changedCells.Add(new NetworkCell - { - Position = position, - ItemHash = cell.Item.ID.Hash, - StackCount = cell.Count, - RootItem = ConvertToNetworkItem(cell.RootRuntimeItem), - StackedRuntimeIds = stackedRuntimeIds, - StackedRuntimeIdStrings = stackedRuntimeIdStrings - }); - } - - return changedCells.ToArray(); - } - - private NetworkEquipmentSlot[] BuildChangedEquipmentDelta() - { - var changedSlots = new List(Mathf.Max(1, m_Bag.Equipment.Count)); - for (int i = 0; i < m_Bag.Equipment.Count; i++) - { - IdString slotRuntimeId = m_Bag.Equipment.GetSlotRootRuntimeItemID(i); - long currentRuntimeHash = slotRuntimeId.Hash; - if (m_LastSyncedEquipment.TryGetValue(i, out long previousRuntimeHash) && - previousRuntimeHash == currentRuntimeHash) - { - continue; - } - - changedSlots.Add(new NetworkEquipmentSlot - { - SlotIndex = i, - BaseItemHash = m_Bag.Equipment.GetSlotBaseID(i).Hash, - IsOccupied = !string.IsNullOrEmpty(slotRuntimeId.String), - EquippedRuntimeIdHash = currentRuntimeHash - }); - } - - return changedSlots.ToArray(); - } - - private NetworkWealthEntry[] BuildChangedWealthDelta() - { - var changedEntries = new List(m_Bag.Wealth.List.Count); - var seenCurrencyHashes = new HashSet(); - - foreach (IdString currencyId in m_Bag.Wealth.List) - { - int hash = currencyId.Hash; - int amount = m_Bag.Wealth.Get(currencyId); - seenCurrencyHashes.Add(hash); - - if (m_LastSyncedWealth.TryGetValue(hash, out int previousAmount) && - previousAmount == amount) - { - continue; - } - - changedEntries.Add(new NetworkWealthEntry - { - CurrencyHash = hash, - Amount = amount - }); - } - - foreach (KeyValuePair entry in m_LastSyncedWealth) - { - if (seenCurrencyHashes.Contains(entry.Key)) continue; - - changedEntries.Add(new NetworkWealthEntry - { - CurrencyHash = entry.Key, - Amount = 0 - }); - } - - return changedEntries.ToArray(); - } - - private void CacheCurrentSyncState() - { - Dictionary currentPositions = BuildCurrentPositionState(); - m_LastSyncedPositions.Clear(); - foreach (KeyValuePair entry in currentPositions) - { - m_LastSyncedPositions[entry.Key] = entry.Value; - } - - m_LastSyncedEquipment.Clear(); - for (int i = 0; i < m_Bag.Equipment.Count; i++) - { - m_LastSyncedEquipment[i] = m_Bag.Equipment.GetSlotRootRuntimeItemID(i).Hash; - } - - m_LastSyncedWealth.Clear(); - foreach (IdString currencyId in m_Bag.Wealth.List) - { - m_LastSyncedWealth[currencyId.Hash] = m_Bag.Wealth.Get(currencyId); - } - } - - private static bool DictionariesEqual( - Dictionary left, - Dictionary right) - { - if (ReferenceEquals(left, right)) return true; - if (left == null || right == null) return false; - if (left.Count != right.Count) return false; - - var comparer = EqualityComparer.Default; - foreach (var entry in left) - { - if (!right.TryGetValue(entry.Key, out TValue value)) return false; - if (!comparer.Equals(entry.Value, value)) return false; - } - - return true; - } - - private bool TryResolveItem(int itemHash, string itemIdString, out Item item) - { - item = null; - InventoryRepository inventory = Settings.From(); - if (inventory == null) return false; - - if (string.IsNullOrWhiteSpace(itemIdString)) - { - return false; - } - - var itemId = new IdString(itemIdString); - if (itemId.Hash != itemHash) return false; - - item = inventory.Items.Get(itemId); - return item != null && item.ID.Hash == itemHash; - } - - private bool TryResolveCurrencyId(int currencyHash, string currencyIdString, out IdString currencyId) - { - currencyId = IdString.EMPTY; - if (string.IsNullOrWhiteSpace(currencyIdString)) return false; - - currencyId = new IdString(currencyIdString); - if (currencyId.Hash != currencyHash) return false; - - foreach (IdString entry in m_Bag.Wealth.List) - { - if (entry.Hash == currencyHash && entry == currencyId) - { - return true; - } - } - - return false; - } - - private bool TryResolveCurrencyIdByHash(int currencyHash, out IdString currencyId) - { - currencyId = IdString.EMPTY; - foreach (IdString entry in m_Bag.Wealth.List) - { - if (entry.Hash == currencyHash) - { - currencyId = entry; - return true; - } - } - - return false; - } - - private static bool TryResolveSocketId(RuntimeItem parentItem, int socketHash, string socketIdString, out IdString socketId) - { - socketId = IdString.EMPTY; - if (parentItem == null || parentItem.Item == null) return false; - if (string.IsNullOrWhiteSpace(socketIdString)) return false; - - socketId = new IdString(socketIdString); - if (socketId.Hash != socketHash) return false; - - var sockets = Sockets.FlattenHierarchy(parentItem.Item); - return sockets != null && sockets.ContainsKey(socketId); - } - - private NetworkRuntimeItem ConvertToNetworkItem(RuntimeItem runtimeItem) - { - if (runtimeItem == null) return default; - - var properties = new List(); - foreach (var prop in runtimeItem.Properties) - { - properties.Add(new NetworkRuntimeProperty - { - PropertyHash = prop.Key.Hash, - PropertyIdString = prop.Key.String, - Number = prop.Value.Number, - Text = prop.Value.Text - }); - } - - var sockets = new List(); - foreach (var socket in runtimeItem.Sockets) - { - sockets.Add(new NetworkRuntimeSocket - { - SocketHash = socket.Key.Hash, - SocketIdString = socket.Key.String, - HasAttachment = socket.Value.HasAttachment, - Attachment = socket.Value.HasAttachment ? ConvertToNetworkItem(socket.Value.Attachment) : default - }); - } - - return new NetworkRuntimeItem - { - ItemHash = runtimeItem.ItemID.Hash, - ItemIdString = runtimeItem.ItemID.String, - RuntimeIdHash = runtimeItem.RuntimeID.Hash, - RuntimeIdString = runtimeItem.RuntimeID.String, - Properties = properties.ToArray(), - Sockets = sockets.ToArray() - }; - } - - private RuntimeItem ReconstructRuntimeItem(NetworkRuntimeItem networkItem) - { - if (networkItem.ItemHash == 0) return null; - - if (!TryResolveItem(networkItem.ItemHash, networkItem.ItemIdString, out Item item)) - { - return null; - } - - var runtimeItem = new RuntimeItem(item); - TryApplyRuntimeId(runtimeItem, networkItem.RuntimeIdString, networkItem.RuntimeIdHash); - - if (networkItem.Properties != null) - { - foreach (NetworkRuntimeProperty property in networkItem.Properties) - { - if (!TryResolveRuntimePropertyId(runtimeItem, property.PropertyHash, property.PropertyIdString, out IdString propertyId)) - { - continue; - } - - if (!runtimeItem.Properties.TryGetValue(propertyId, out RuntimeProperty runtimeProperty)) - { - continue; - } - - runtimeProperty.Number = property.Number; - runtimeProperty.Text = property.Text; - } - } - - if (networkItem.Sockets != null && s_RuntimeSocketAttachmentField != null) - { - foreach (NetworkRuntimeSocket socket in networkItem.Sockets) - { - if (!TryResolveRuntimeSocketId(runtimeItem, socket.SocketHash, socket.SocketIdString, out IdString socketId) || - !runtimeItem.Sockets.TryGetValue(socketId, out RuntimeSocket runtimeSocket)) - { - continue; - } - - if (!socket.HasAttachment) - { - s_RuntimeSocketAttachmentField.SetValue(runtimeSocket, null); - continue; - } - - RuntimeItem attachment = ReconstructRuntimeItem(socket.Attachment); - if (attachment != null) - { - s_RuntimeSocketAttachmentField.SetValue(runtimeSocket, attachment); - } - } - } - - return runtimeItem; - } - - private void ApplyCellDelta(NetworkCell[] changedCells) - { - if (changedCells == null) return; - - foreach (NetworkCell cell in changedCells) - { - ClearCellAtPosition(cell.Position); - - bool isDeleteEntry = cell.ItemHash == 0 || cell.StackCount <= 0 || cell.RootItem.ItemHash == 0; - if (isDeleteEntry) - { - continue; - } - - RuntimeItem rootItem = ReconstructRuntimeItem(cell.RootItem); - if (rootItem == null) - { - continue; - } - - bool addedRoot = m_Bag.Content.Add(rootItem, cell.Position, true); - if (!addedRoot) - { - continue; - } - - TrackRuntimeItemRecursive(rootItem); - - int stackCount = Mathf.Max(1, cell.StackCount); - long[] stackedRuntimeIds = cell.StackedRuntimeIds; - string[] stackedRuntimeIdStrings = cell.StackedRuntimeIdStrings; - for (int i = 1; i < stackCount; i++) - { - RuntimeItem stackedItem = new RuntimeItem(rootItem, true); - int stackedIndex = i - 1; - if (stackedRuntimeIds != null && stackedIndex < stackedRuntimeIds.Length) - { - string runtimeIdString = stackedRuntimeIdStrings != null && stackedIndex < stackedRuntimeIdStrings.Length - ? stackedRuntimeIdStrings[stackedIndex] - : null; - TryApplyRuntimeId(stackedItem, runtimeIdString, stackedRuntimeIds[stackedIndex]); - } - - if (m_Bag.Content.Add(stackedItem, cell.Position, true)) - { - TrackRuntimeItemRecursive(stackedItem); - } - } - } - } - - private void ApplyEquipmentDelta(NetworkEquipmentSlot[] changedEquipment) - { - if (changedEquipment == null) return; - - foreach (NetworkEquipmentSlot slot in changedEquipment) - { - if (slot.SlotIndex < 0 || slot.SlotIndex >= m_Bag.Equipment.Count) - { - continue; - } - - _ = m_Bag.Equipment.UnequipFromIndex(slot.SlotIndex); - if (!slot.IsOccupied) - { - continue; - } - - if (m_RuntimeItemMap.TryGetValue(slot.EquippedRuntimeIdHash, out RuntimeItem runtimeItem)) - { - _ = m_Bag.Equipment.EquipToIndex(runtimeItem, slot.SlotIndex); - } - } - } - - private void ApplyWealthDelta(NetworkWealthEntry[] changedWealth) - { - if (changedWealth == null) return; - - foreach (NetworkWealthEntry wealthEntry in changedWealth) - { - if (TryResolveCurrencyIdByHash(wealthEntry.CurrencyHash, out IdString currencyId)) - { - m_Bag.Wealth.Set(currencyId, wealthEntry.Amount); - } - } - } - - private void ClearCellAtPosition(Vector2Int position) - { - int safety = 0; - while (safety++ < 256) - { - RuntimeItem removed = m_Bag.Content.Remove(position); - if (removed == null) - { - break; - } - - UntrackRuntimeItemRecursive(removed); - } - } - - private void ApplyFullSnapshot(NetworkInventorySnapshot snapshot) - { - // [LOCAL-EDIT] #PILFER-INVENTORY-SNAPSHOT-DEBUG - int localItemsBefore = m_Bag.Content.CountWithStack; - int trackedItemsBefore = m_RuntimeItemMap.Count; - int snapshotCells = snapshot.Cells?.Length ?? 0; - int snapshotStackItems = CountSnapshotStackItems(snapshot.Cells); - - LogPickupDebug( - $"{name}: applying full snapshot bag={NetworkId} snapshotBag={snapshot.BagNetworkId} world={IsWorldInventory} server={m_IsServer} local={m_IsLocalClient} remote={m_IsRemoteClient} localItemsBefore={localItemsBefore} trackedBefore={trackedItemsBefore} snapshotCells={snapshotCells} snapshotStackItems={snapshotStackItems} firstCells={DescribeSnapshotCells(snapshot.Cells, 5)}", - this); - - ClearCurrentInventoryState(); - - if (snapshot.Cells != null) - { - foreach (NetworkCell cell in snapshot.Cells) - { - RuntimeItem rootItem = ReconstructRuntimeItem(cell.RootItem); - if (rootItem == null) continue; - - bool addedRoot = m_Bag.Content.Add(rootItem, cell.Position, true); - if (!addedRoot) - { - continue; - } - - TrackRuntimeItemRecursive(rootItem); - - int stackCount = Mathf.Max(1, cell.StackCount); - long[] stackedRuntimeIds = cell.StackedRuntimeIds; - string[] stackedRuntimeIdStrings = cell.StackedRuntimeIdStrings; - for (int i = 1; i < stackCount; i++) - { - RuntimeItem stackedItem = new RuntimeItem(rootItem, true); - int stackedIndex = i - 1; - if (stackedRuntimeIds != null && stackedIndex < stackedRuntimeIds.Length) - { - string runtimeIdString = stackedRuntimeIdStrings != null && stackedIndex < stackedRuntimeIdStrings.Length - ? stackedRuntimeIdStrings[stackedIndex] - : null; - TryApplyRuntimeId(stackedItem, runtimeIdString, stackedRuntimeIds[stackedIndex]); - } - - if (m_Bag.Content.Add(stackedItem, cell.Position, true)) - { - TrackRuntimeItemRecursive(stackedItem); - } - } - } - } - - for (int i = 0; i < m_Bag.Equipment.Count; i++) - { - _ = m_Bag.Equipment.UnequipFromIndex(i); - } - - if (snapshot.Equipment != null) - { - foreach (NetworkEquipmentSlot slot in snapshot.Equipment) - { - if (!slot.IsOccupied) continue; - if (!m_RuntimeItemMap.TryGetValue(slot.EquippedRuntimeIdHash, out RuntimeItem runtimeItem)) continue; - _ = m_Bag.Equipment.EquipToIndex(runtimeItem, slot.SlotIndex); - } - } - - foreach (IdString currencyId in m_Bag.Wealth.List) - { - m_Bag.Wealth.Set(currencyId, 0); - } - - if (snapshot.Wealth != null) - { - foreach (NetworkWealthEntry wealthEntry in snapshot.Wealth) - { - if (TryResolveCurrencyIdByHash(wealthEntry.CurrencyHash, out IdString currencyId)) - { - m_Bag.Wealth.Set(currencyId, wealthEntry.Amount); - } - } - } - - CacheCurrentSyncState(); - - // [LOCAL-EDIT] #PILFER-INVENTORY-SNAPSHOT-DEBUG - LogPickupDebug( - $"{name}: applied full snapshot bag={NetworkId} localItemsAfter={m_Bag.Content.CountWithStack} trackedAfter={m_RuntimeItemMap.Count} snapshotCells={snapshotCells} snapshotStackItems={snapshotStackItems}", - this); - } - - // [LOCAL-EDIT] #PILFER-INVENTORY-SNAPSHOT-DEBUG - private static int CountSnapshotStackItems(NetworkCell[] cells) - { - if (cells == null) return 0; - - int count = 0; - for (int i = 0; i < cells.Length; i++) - { - count += Mathf.Max(0, cells[i].StackCount); - } - - return count; - } - - // [LOCAL-EDIT] #PILFER-INVENTORY-SNAPSHOT-DEBUG - private static string DescribeSnapshotCells(NetworkCell[] cells, int maxCells) - { - if (cells == null || cells.Length == 0) return "none"; - - int limit = Mathf.Min(cells.Length, Mathf.Max(0, maxCells)); - string description = string.Empty; - for (int i = 0; i < limit; i++) - { - NetworkCell cell = cells[i]; - if (i > 0) description += "; "; - description += $"#{i} pos={cell.Position} item={cell.RootItem.ItemIdString} runtime={cell.RootItem.RuntimeIdHash} stack={cell.StackCount}"; - } - - if (cells.Length > limit) - { - description += $"; +{cells.Length - limit} more"; - } - - return description; - } - - private void ClearCurrentInventoryState() - { - int safety = 0; - while (safety++ < 4096) - { - RuntimeItem itemToRemove = null; - foreach (Cell cell in m_Bag.Content.CellList) - { - if (cell == null || cell.Available) continue; - itemToRemove = cell.Peek(); - if (itemToRemove != null) break; - } - - if (itemToRemove == null) break; - m_Bag.Content.Remove(itemToRemove); - } - - m_RuntimeItemMap.Clear(); - } - - private void TrackRuntimeItemRecursive(RuntimeItem runtimeItem) - { - if (runtimeItem == null) return; - - m_RuntimeItemMap[runtimeItem.RuntimeID.Hash] = runtimeItem; - foreach (KeyValuePair socketEntry in runtimeItem.Sockets) - { - RuntimeSocket socket = socketEntry.Value; - if (socket == null || !socket.HasAttachment) continue; - TrackRuntimeItemRecursive(socket.Attachment); - } - } - - private void UntrackRuntimeItemRecursive(RuntimeItem runtimeItem) - { - if (runtimeItem == null) return; - - m_RuntimeItemMap.Remove(runtimeItem.RuntimeID.Hash); - foreach (KeyValuePair socketEntry in runtimeItem.Sockets) - { - RuntimeSocket socket = socketEntry.Value; - if (socket == null || !socket.HasAttachment) continue; - UntrackRuntimeItemRecursive(socket.Attachment); - } - } - - private bool ContainsRuntimeItemRecursive(long runtimeIdHash) - { - foreach (Cell cell in m_Bag.Content.CellList) - { - if (cell == null || cell.Available) continue; - - RuntimeItem rootItem = cell.RootRuntimeItem; - if (ContainsRuntimeItemRecursive(rootItem, runtimeIdHash)) return true; - - foreach (IdString stackedId in cell.List) - { - RuntimeItem stackedItem = m_Bag.Content.GetRuntimeItem(stackedId); - if (ContainsRuntimeItemRecursive(stackedItem, runtimeIdHash)) return true; - } - } - - return false; - } - - private static bool ContainsRuntimeItemRecursive(RuntimeItem runtimeItem, long runtimeIdHash) - { - if (runtimeItem == null) return false; - if (runtimeItem.RuntimeID.Hash == runtimeIdHash) return true; - - foreach (KeyValuePair socketEntry in runtimeItem.Sockets) - { - RuntimeSocket socket = socketEntry.Value; - if (socket == null || !socket.HasAttachment) continue; - if (ContainsRuntimeItemRecursive(socket.Attachment, runtimeIdHash)) return true; - } - - return false; - } - - private static void TryApplyRuntimeId(RuntimeItem runtimeItem, string runtimeIdString, long runtimeIdHash) - { - if (runtimeItem == null || s_RuntimeItemIdField == null) return; - if (string.IsNullOrWhiteSpace(runtimeIdString)) return; - - IdString runtimeId = new IdString(runtimeIdString); - if (runtimeIdHash != 0 && runtimeId.Hash != runtimeIdHash) return; - s_RuntimeItemIdField.SetValue(runtimeItem, runtimeId); - } - - private static bool TryResolveRuntimePropertyId(RuntimeItem runtimeItem, int propertyHash, string propertyIdString, out IdString propertyId) - { - propertyId = IdString.EMPTY; - if (runtimeItem == null) return false; - - if (!string.IsNullOrWhiteSpace(propertyIdString)) - { - IdString candidate = new IdString(propertyIdString); - if (candidate.Hash == propertyHash && runtimeItem.Properties.ContainsKey(candidate)) - { - propertyId = candidate; - return true; - } - } - - foreach (KeyValuePair entry in runtimeItem.Properties) - { - if (entry.Key.Hash != propertyHash) continue; - propertyId = entry.Key; - return true; - } - - return false; - } - - private static bool TryResolveRuntimeSocketId(RuntimeItem runtimeItem, int socketHash, string socketIdString, out IdString socketId) - { - socketId = IdString.EMPTY; - if (runtimeItem == null) return false; - - if (!string.IsNullOrWhiteSpace(socketIdString)) - { - IdString candidate = new IdString(socketIdString); - if (candidate.Hash == socketHash && runtimeItem.Sockets.ContainsKey(candidate)) - { - socketId = candidate; - return true; - } - } - - foreach (KeyValuePair entry in runtimeItem.Sockets) - { - if (entry.Key.Hash != socketHash) continue; - socketId = entry.Key; - return true; - } - - return false; - } - - private static void GetStackedRuntimeIdentity(Cell cell, out long[] runtimeIds, out string[] runtimeIdStrings) - { - var ids = new List(); - var idStrings = new List(); - foreach (var id in cell.List) - { - if (id.Hash == cell.RootRuntimeItemID.Hash) continue; - ids.Add(id.Hash); - idStrings.Add(id.String); - } - - runtimeIds = ids.ToArray(); - runtimeIdStrings = idStrings.ToArray(); - } - } -} -#endif diff --git a/NetworkInventoryController.Server.cs b/NetworkInventoryController.Server.cs deleted file mode 100644 index fca1435..0000000 --- a/NetworkInventoryController.Server.cs +++ /dev/null @@ -1,1766 +0,0 @@ -#if GC2_INVENTORY -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Threading.Tasks; -using UnityEngine; -using GameCreator.Runtime.Common; -using GameCreator.Runtime.Inventory; - -namespace Arawn.GameCreator2.Networking.Inventory -{ - // ════════════════════════════════════════════════════════════════════════════════════════════ - // SERVER-SIDE — Request processing, broadcasting, and helper methods - // ════════════════════════════════════════════════════════════════════════════════════════════ - - public partial class NetworkInventoryController - { - private static readonly FieldInfo s_RuntimeItemIdField = typeof(RuntimeItem) - .GetField("m_RuntimeID", BindingFlags.Instance | BindingFlags.NonPublic); - - private static readonly FieldInfo s_RuntimeSocketAttachmentField = typeof(RuntimeSocket) - .GetField("m_AttachmentRuntimeItem", BindingFlags.Instance | BindingFlags.NonPublic); - - static NetworkInventoryController() - { - if (s_RuntimeItemIdField == null || s_RuntimeSocketAttachmentField == null) - { - Debug.LogWarning( - "[NetworkInventoryController] Reflection dependencies for RuntimeItem/RuntimeSocket could not be resolved. " + - "Inventory runtime reconstruction may degrade until patch signatures are updated for this GC2 version."); - } - } - - // ════════════════════════════════════════════════════════════════════════════════════════ - // SERVER-SIDE: PROCESS REQUESTS - // ════════════════════════════════════════════════════════════════════════════════════════ - - #region Server Processing - - /// - /// [Server] Process content add request. - /// - public NetworkContentAddResponse ProcessContentAddRequest(NetworkContentAddRequest request, uint clientNetworkId) - { - if (!m_IsServer) - { - return new NetworkContentAddResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.NotAuthorized - }; - } - - // Client-originated arbitrary runtime payloads are not allowed. - if (request.ItemHash == 0) - { - return new NetworkContentAddResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.SecurityViolation - }; - } - - if (!TryResolveItem(request.ItemHash, request.ItemIdString, out Item item)) - { - return new NetworkContentAddResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.IdentityMismatch - }; - } - - RuntimeItem runtimeItem = new RuntimeItem(item); - - // Try to add - Vector2Int resultPosition; - if (request.Position.x >= 0 && request.Position.y >= 0) - { - bool success = m_Bag.Content.Add(runtimeItem, request.Position, request.AllowStack); - resultPosition = success ? request.Position : TBagContent.INVALID; - } - else - { - resultPosition = m_Bag.Content.Add(runtimeItem, request.AllowStack); - } - - if (resultPosition == TBagContent.INVALID) - { - return new NetworkContentAddResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.InsufficientSpace - }; - } - - // Update map - TrackRuntimeItemRecursive(runtimeItem); - - // Broadcast - var broadcast = new NetworkItemAddedBroadcast - { - BagNetworkId = NetworkId, - Item = ConvertToNetworkItem(runtimeItem), - Position = resultPosition, - StackCount = m_Bag.Content.GetContent(resultPosition)?.Count ?? 1 - }; - - NetworkInventoryManager.Instance?.BroadcastItemAdded(broadcast); - OnItemAdded?.Invoke(broadcast); - - return new NetworkContentAddResponse - { - RequestId = request.RequestId, - Authorized = true, - RejectionReason = InventoryRejectionReason.None, - ResultPosition = resultPosition, - AssignedRuntimeId = runtimeItem.RuntimeID.Hash, - AssignedRuntimeIdString = runtimeItem.RuntimeID.String - }; - } - - /// - /// [Server] Process content remove request. - /// - public NetworkContentRemoveResponse ProcessContentRemoveRequest(NetworkContentRemoveRequest request, uint clientNetworkId) - { - if (!m_IsServer) - { - return new NetworkContentRemoveResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.NotAuthorized - }; - } - - RuntimeItem removed; - Vector2Int position; - - if (request.UsePosition) - { - position = request.Position; - removed = m_Bag.Content.Remove(position); - } - else - { - if (!m_RuntimeItemMap.TryGetValue(request.RuntimeIdHash, out var runtimeItem)) - { - return new NetworkContentRemoveResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.RuntimeItemNotFound - }; - } - - position = m_Bag.Content.FindPosition(runtimeItem.RuntimeID); - removed = m_Bag.Content.Remove(runtimeItem); - } - - if (removed == null) - { - return new NetworkContentRemoveResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.RuntimeItemNotFound - }; - } - - UntrackRuntimeItemRecursive(removed); - - // Broadcast - var cell = m_Bag.Content.GetContent(position); - var removeBroadcast = new NetworkItemRemovedBroadcast - { - BagNetworkId = NetworkId, - RuntimeIdHash = removed.RuntimeID.Hash, - Position = position, - RemainingStackCount = cell?.Count ?? 0 - }; - - NetworkInventoryManager.Instance?.BroadcastItemRemoved(removeBroadcast); - OnItemRemoved?.Invoke(removeBroadcast); - - return new NetworkContentRemoveResponse - { - RequestId = request.RequestId, - Authorized = true, - RejectionReason = InventoryRejectionReason.None, - RemovedItem = ConvertToNetworkItem(removed) - }; - } - - /// - /// [Server] Process content move request. - /// - public NetworkContentMoveResponse ProcessContentMoveRequest(NetworkContentMoveRequest request, uint clientNetworkId) - { - if (!m_IsServer) - { - return new NetworkContentMoveResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.NotAuthorized - }; - } - - if (!m_Bag.Content.CanMove(request.FromPosition, request.ToPosition, request.AllowStack)) - { - return new NetworkContentMoveResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.InvalidPosition - }; - } - - var moveCell = m_Bag.Content.GetContent(request.FromPosition); - long runtimeIdHash = moveCell?.RootRuntimeItemID.Hash ?? 0; - - bool moveSuccess = m_Bag.Content.Move(request.FromPosition, request.ToPosition, request.AllowStack); - - if (!moveSuccess) - { - return new NetworkContentMoveResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.InvalidOperation - }; - } - - // Broadcast - var moveBroadcast = new NetworkItemMovedBroadcast - { - BagNetworkId = NetworkId, - RuntimeIdHash = runtimeIdHash, - FromPosition = request.FromPosition, - ToPosition = request.ToPosition - }; - - NetworkInventoryManager.Instance?.BroadcastItemMoved(moveBroadcast); - OnItemMoved?.Invoke(moveBroadcast); - - return new NetworkContentMoveResponse - { - RequestId = request.RequestId, - Authorized = true, - RejectionReason = InventoryRejectionReason.None, - FinalPosition = request.ToPosition - }; - } - - /// - /// [Server] Process content use request. - /// - public NetworkContentUseResponse ProcessContentUseRequest(NetworkContentUseRequest request, uint clientNetworkId) - { - if (!m_IsServer) - { - return new NetworkContentUseResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.NotAuthorized - }; - } - - bool useSuccess; - bool wasConsumed = false; - - if (request.UsePosition) - { - var useCell = m_Bag.Content.GetContent(request.Position); - if (useCell == null || useCell.Available) - { - return new NetworkContentUseResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.RuntimeItemNotFound - }; - } - - var useItem = useCell.RootRuntimeItem; - wasConsumed = useItem.Item.Usage.ConsumeWhenUse; - useSuccess = m_Bag.Content.Use(request.Position); - } - else - { - if (!m_RuntimeItemMap.TryGetValue(request.RuntimeIdHash, out var useItem)) - { - return new NetworkContentUseResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.RuntimeItemNotFound - }; - } - - wasConsumed = useItem.Item.Usage.ConsumeWhenUse; - useSuccess = m_Bag.Content.Use(useItem); - - if (useSuccess && wasConsumed) - { - UntrackRuntimeItemRecursive(useItem); - } - } - - if (!useSuccess) - { - return new NetworkContentUseResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.CannotUse - }; - } - - // Broadcast - var useBroadcast = new NetworkItemUsedBroadcast - { - BagNetworkId = NetworkId, - RuntimeIdHash = request.RuntimeIdHash, - WasConsumed = wasConsumed - }; - - NetworkInventoryManager.Instance?.BroadcastItemUsed(useBroadcast); - OnItemUsed?.Invoke(useBroadcast); - - return new NetworkContentUseResponse - { - RequestId = request.RequestId, - Authorized = true, - RejectionReason = InventoryRejectionReason.None, - WasConsumed = wasConsumed - }; - } - - /// - /// [Server] Process content drop request. - /// - public NetworkContentDropResponse ProcessContentDropRequest(NetworkContentDropRequest request, uint clientNetworkId) - { - LogPickupDebug( - $"{name}: server drop request received req={request.RequestId} client={clientNetworkId} actor={request.ActorNetworkId} targetBag={request.TargetBagNetworkId} runtime={request.RuntimeIdHash} position={request.DropPosition} controllerBag={NetworkId} hasRuntime={m_RuntimeItemMap.ContainsKey(request.RuntimeIdHash)}", - this); - - if (!m_IsServer) - { - LogPickupWarning($"{name}: drop rejected not server req={request.RequestId} runtime={request.RuntimeIdHash}", this); - return new NetworkContentDropResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.NotAuthorized - }; - } - - if (!m_RuntimeItemMap.TryGetValue(request.RuntimeIdHash, out var dropItem)) - { - LogPickupWarning( - $"{name}: drop rejected runtime not found req={request.RequestId} runtime={request.RuntimeIdHash} trackedItems={m_RuntimeItemMap.Count}", - this); - return new NetworkContentDropResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.RuntimeItemNotFound - }; - } - - if (!dropItem.Item.CanDrop) - { - LogPickupWarning( - $"{name}: drop rejected item cannot drop req={request.RequestId} item={DescribeRuntimeItem(dropItem)}", - this); - return new NetworkContentDropResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.CannotDrop - }; - } - - Vector2Int sourcePosition = m_Bag.Content.FindPosition(dropItem.RuntimeID); - NetworkRuntimeItem droppedItem = ConvertToNetworkItem(dropItem); - GameObject dropped; - m_IsApplyingNetworkState = true; - try - { - dropped = m_Bag.Content.Drop(dropItem, request.DropPosition); - } - finally - { - m_IsApplyingNetworkState = false; - } - - if (dropped == null) - { - LogPickupWarning( - $"{name}: drop rejected GC2 Content.Drop returned null req={request.RequestId} item={DescribeRuntimeItem(dropItem)} position={request.DropPosition}", - this); - return new NetworkContentDropResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.CannotDrop - }; - } - - UntrackRuntimeItemRecursive(dropItem); - - var removeBroadcast = new NetworkItemRemovedBroadcast - { - BagNetworkId = NetworkId, - RuntimeIdHash = request.RuntimeIdHash, - Position = sourcePosition, - RemainingStackCount = m_Bag.Content.GetContent(sourcePosition)?.Count ?? 0 - }; - - NetworkInventoryManager.Instance?.BroadcastItemRemoved(removeBroadcast); - OnItemRemoved?.Invoke(removeBroadcast); - - var dropBroadcast = new NetworkItemDroppedBroadcast - { - SourceBagNetworkId = NetworkId, - Item = droppedItem, - Position = request.DropPosition - }; - - NetworkInventoryManager.Instance?.BroadcastItemDropped(dropBroadcast); - RememberDroppedItemInstance(request.RuntimeIdHash, dropped, NetworkId, droppedItem, request.DropPosition); - RememberServerDroppedWorldItem(request.RuntimeIdHash, NetworkId, droppedItem, request.DropPosition); - LogPickupDebug( - $"{name}: drop accepted req={request.RequestId} item={DescribeNetworkItem(droppedItem)} sourcePosition={sourcePosition} dropPosition={request.DropPosition} instance={(dropped != null ? dropped.name : "null")}", - this); - CacheCurrentSyncState(); - - return new NetworkContentDropResponse - { - RequestId = request.RequestId, - Authorized = true, - RejectionReason = InventoryRejectionReason.None, - DroppedCount = 1 - }; - } - - /// - /// [Server] Process transfer from this bag to another registered bag. - /// - public NetworkTransferResponse ProcessTransferRequest( - NetworkTransferRequest request, - NetworkInventoryController destination, - uint clientNetworkId) - { - LogPickupDebug( - $"{name}: server transfer request received req={request.RequestId} client={clientNetworkId} actor={request.ActorNetworkId} sourceBag={request.SourceBagNetworkId} destinationBag={request.DestinationBagNetworkId} runtime={request.RuntimeIdHash} destination={request.DestinationPosition} sourceControllerBag={NetworkId} destinationControllerBag={(destination != null ? destination.NetworkId : 0)} trackedItems={m_RuntimeItemMap.Count}", - this); - - if (!m_IsServer || destination == null || !destination.m_IsServer) - { - LogPickupWarning( - $"{name}: transfer rejected not server-authoritative req={request.RequestId} runtime={request.RuntimeIdHash} sourceServer={m_IsServer} destination={(destination != null ? destination.name : "null")} destinationServer={(destination != null && destination.m_IsServer)}", - this); - return new NetworkTransferResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.NotAuthorized - }; - } - - if (destination == this) - { - LogPickupWarning($"{name}: transfer rejected source and destination are the same req={request.RequestId}", this); - return new NetworkTransferResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.InvalidOperation - }; - } - - if (!m_RuntimeItemMap.TryGetValue(request.RuntimeIdHash, out RuntimeItem runtimeItem)) - { - LogPickupWarning( - $"{name}: transfer rejected runtime not found req={request.RequestId} runtime={request.RuntimeIdHash} trackedItems={m_RuntimeItemMap.Count}", - this); - return new NetworkTransferResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.RuntimeItemNotFound - }; - } - - Vector2Int sourcePosition = m_Bag.Content.FindPosition(runtimeItem.RuntimeID); - RuntimeItem removed = m_Bag.Content.Remove(runtimeItem); - if (removed == null) - { - LogPickupWarning( - $"{name}: transfer rejected GC2 Content.Remove returned null req={request.RequestId} item={DescribeRuntimeItem(runtimeItem)}", - this); - return new NetworkTransferResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.RuntimeItemNotFound - }; - } - - UntrackRuntimeItemRecursive(removed); - - Vector2Int finalPosition; - if (request.DestinationPosition.x >= 0 && request.DestinationPosition.y >= 0) - { - bool added = destination.m_Bag.Content.Add( - removed, - request.DestinationPosition, - request.AllowStack); - finalPosition = added ? request.DestinationPosition : TBagContent.INVALID; - } - else - { - finalPosition = destination.m_Bag.Content.Add(removed, request.AllowStack); - } - - if (finalPosition == TBagContent.INVALID) - { - LogPickupWarning( - $"{name}: transfer rejected destination has insufficient space req={request.RequestId} item={DescribeRuntimeItem(removed)} destinationBag={destination.NetworkId} requested={request.DestinationPosition}", - this); - - if (sourcePosition.x >= 0 && sourcePosition.y >= 0) - { - m_Bag.Content.Add(removed, sourcePosition, true); - } - else - { - m_Bag.Content.Add(removed, true); - } - - TrackRuntimeItemRecursive(removed); - - return new NetworkTransferResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.InsufficientSpace - }; - } - - destination.TrackRuntimeItemRecursive(removed); - CacheCurrentSyncState(); - destination.CacheCurrentSyncState(); - - NetworkInventoryManager manager = NetworkInventoryManager.Instance; - if (manager != null) - { - manager.BroadcastFullSnapshot(GetFullSnapshot()); - manager.BroadcastFullSnapshot(destination.GetFullSnapshot()); - } - - LogPickupDebug( - $"{name}: transfer accepted req={request.RequestId} item={DescribeRuntimeItem(removed)} sourcePosition={sourcePosition} destinationBag={destination.NetworkId} finalPosition={finalPosition}", - this); - - return new NetworkTransferResponse - { - RequestId = request.RequestId, - Authorized = true, - RejectionReason = InventoryRejectionReason.None, - FinalPosition = finalPosition - }; - } - - private void BroadcastServerDropFromLocalMutation(NetworkRuntimeItem droppedItem, long runtimeIdHash, Vector3 position) - { - if (!m_IsServer || droppedItem.ItemHash == 0) return; - - NetworkInventoryManager manager = NetworkInventoryManager.Instance; - if (manager == null) return; - - var removeBroadcast = new NetworkItemRemovedBroadcast - { - BagNetworkId = NetworkId, - RuntimeIdHash = runtimeIdHash, - Position = TBagContent.INVALID, - RemainingStackCount = 0 - }; - - manager.BroadcastItemRemoved(removeBroadcast); - - manager.BroadcastItemDropped(new NetworkItemDroppedBroadcast - { - SourceBagNetworkId = NetworkId, - Item = droppedItem, - Position = position - }); - - RememberServerDroppedWorldItem(runtimeIdHash, NetworkId, droppedItem, position); - CacheCurrentSyncState(); - } - - public NetworkPickupResponse ProcessPickupRequest(NetworkPickupRequest request, uint clientNetworkId) - { - LogPickupDebug( - $"{name}: server pickup request received req={request.RequestId} client={clientNetworkId} actor={request.ActorNetworkId} pickerBag={request.PickerBagNetworkId} sourceBag={request.SourceBagNetworkId} runtime={request.RuntimeIdHash} destination={request.DestinationPosition} controllerBag={NetworkId} knownDropped={s_ServerDroppedWorldItems.ContainsKey(request.RuntimeIdHash)} knownDropCount={s_ServerDroppedWorldItems.Count}", - this); - - if (!m_IsServer) - { - LogPickupWarning($"{name}: pickup rejected not server req={request.RequestId} runtime={request.RuntimeIdHash}", this); - return new NetworkPickupResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.NotAuthorized - }; - } - - if (request.PickerBagNetworkId != NetworkId) - { - LogPickupWarning( - $"{name}: pickup rejected bag mismatch req={request.RequestId} runtime={request.RuntimeIdHash} requestPickerBag={request.PickerBagNetworkId} controllerBag={NetworkId}", - this); - return new NetworkPickupResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.BagNotFound - }; - } - - if (!TryGetServerDroppedWorldItem(request.RuntimeIdHash, out ServerDroppedWorldItem droppedWorldItem)) - { - // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT - if (TryProcessWorldObjectPickupRequest(request, clientNetworkId, out NetworkPickupResponse worldObjectResponse)) - { - return worldObjectResponse; - } - - LogPickupWarning( - $"{name}: pickup rejected dropped runtime not found req={request.RequestId} runtime={request.RuntimeIdHash} knownDropCount={s_ServerDroppedWorldItems.Count}", - this); - return new NetworkPickupResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.RuntimeItemNotFound - }; - } - - if (request.SourceBagNetworkId != 0 && droppedWorldItem.SourceBagNetworkId != request.SourceBagNetworkId) - { - LogPickupWarning( - $"{name}: pickup rejected source mismatch req={request.RequestId} runtime={request.RuntimeIdHash} requestSource={request.SourceBagNetworkId} rememberedSource={droppedWorldItem.SourceBagNetworkId}", - this); - return new NetworkPickupResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.IdentityMismatch - }; - } - - RuntimeItem runtimeItem = ReconstructRuntimeItem(droppedWorldItem.Item); - if (runtimeItem == null) - { - LogPickupWarning( - $"{name}: pickup rejected failed reconstruct req={request.RequestId} droppedItem={DescribeNetworkItem(droppedWorldItem.Item)}", - this); - return new NetworkPickupResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.IdentityMismatch - }; - } - - Vector2Int finalPosition; - m_IsApplyingNetworkState = true; - try - { - if (request.DestinationPosition.x >= 0 && request.DestinationPosition.y >= 0) - { - bool added = m_Bag.Content.Add(runtimeItem, request.DestinationPosition, true); - finalPosition = added ? request.DestinationPosition : TBagContent.INVALID; - } - else - { - finalPosition = m_Bag.Content.Add(runtimeItem, true); - } - } - finally - { - m_IsApplyingNetworkState = false; - } - - if (finalPosition == TBagContent.INVALID) - { - LogPickupWarning( - $"{name}: pickup rejected insufficient space req={request.RequestId} item={DescribeRuntimeItem(runtimeItem)} requestedDestination={request.DestinationPosition}", - this); - return new NetworkPickupResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.InsufficientSpace - }; - } - - s_ServerDroppedWorldItems.Remove(request.RuntimeIdHash); - TrackRuntimeItemRecursive(runtimeItem); - - NetworkInventoryManager manager = NetworkInventoryManager.Instance; - if (manager != null) - { - var addBroadcast = new NetworkItemAddedBroadcast - { - BagNetworkId = NetworkId, - Item = ConvertToNetworkItem(runtimeItem), - Position = finalPosition, - StackCount = m_Bag.Content.GetContent(finalPosition)?.Count ?? 1 - }; - - manager.BroadcastItemAdded(addBroadcast); - OnItemAdded?.Invoke(addBroadcast); - - LogPickupDebug( - $"{name}: pickup accepted broadcasting item add req={request.RequestId} pickerBag={NetworkId} sourceBag={droppedWorldItem.SourceBagNetworkId} item={DescribeRuntimeItem(runtimeItem)} finalPosition={finalPosition}", - this); - - manager.BroadcastDroppedItemRemoved(new NetworkDroppedItemRemovedBroadcast - { - SourceBagNetworkId = droppedWorldItem.SourceBagNetworkId, - RuntimeIdHash = request.RuntimeIdHash, - Position = droppedWorldItem.Position - }); - } - - bool destroyedLocalDrop = TryDestroyDroppedItemInstance(request.RuntimeIdHash); - LogPickupDebug( - $"{name}: pickup completed req={request.RequestId} runtime={request.RuntimeIdHash} destroyedServerDropInstance={destroyedLocalDrop} remainingServerDrops={s_ServerDroppedWorldItems.Count}", - this); - CacheCurrentSyncState(); - - return new NetworkPickupResponse - { - RequestId = request.RequestId, - Authorized = true, - RejectionReason = InventoryRejectionReason.None, - PickedUpItem = ConvertToNetworkItem(runtimeItem), - PlacedPosition = finalPosition - }; - } - - // [LOCAL-EDIT] #PILFER-INVENTORY-SERVER-LOOT - public NetworkLootResponse ProcessLootRequest(NetworkLootRequest request, uint clientNetworkId) - { - Debug.Log( - $"[NetworkInventoryLootDebug] {name}: server loot request received req={request.RequestId} client={clientNetworkId} actor={request.ActorNetworkId} container={request.ContainerBagNetworkId} controllerBag={NetworkId} world={IsWorldInventory}"); - - if (!m_IsServer) - { - return BuildLootResponse(request, false, false, InventoryRejectionReason.NotAuthorized, NetworkLootFailure.None); - } - - if (request.ContainerBagNetworkId != NetworkId) - { - return BuildLootResponse(request, false, false, InventoryRejectionReason.BagNotFound, NetworkLootFailure.ContainerBagNotFound); - } - - if (!IsWorldInventory) - { - return BuildLootResponse(request, false, false, InventoryRejectionReason.InvalidOperation, NetworkLootFailure.ContainerIsNotWorldInventory); - } - - NetworkLootContainer lootContainer = GetComponent() ?? - GetComponentInParent() ?? - GetComponentInChildren(); - if (lootContainer == null) - { - return BuildLootResponse(request, false, false, InventoryRejectionReason.InvalidOperation, NetworkLootFailure.LootContainerMissing); - } - - if (lootContainer.LootTable == null) - { - return BuildLootResponse(request, false, false, InventoryRejectionReason.ItemNotFound, NetworkLootFailure.LootTableMissing); - } - - if (lootContainer.GenerateOnce && lootContainer.HasGenerated) - { - BroadcastFullState(); - return BuildLootResponse(request, true, false, InventoryRejectionReason.None, NetworkLootFailure.AlreadyGenerated); - } - - bool generated = lootContainer.LootTable.Run(m_Bag); - if (!generated) - { - return BuildLootResponse(request, false, false, InventoryRejectionReason.ItemNotFound, NetworkLootFailure.LootRollFailed); - } - - lootContainer.MarkGenerated(); - TrackAllCurrentRuntimeItemsForServerLoot(); - CacheCurrentSyncState(); - BroadcastFullState(); - - int cells = GetFullSnapshot().Cells?.Length ?? 0; - if (lootContainer.LogDiagnostics) - { - Debug.Log( - $"[NetworkInventoryLootDebug] {name}: generated server loot container={NetworkId} generated={generated} cells={cells}", - this); - } - - return BuildLootResponse(request, true, true, InventoryRejectionReason.None, NetworkLootFailure.None); - } - - // [LOCAL-EDIT] #PILFER-INVENTORY-SERVER-LOOT - private void TrackAllCurrentRuntimeItemsForServerLoot() - { - foreach (Cell cell in m_Bag.Content.CellList) - { - if (cell == null || cell.Available) continue; - - TrackRuntimeItemRecursive(cell.RootRuntimeItem); - foreach (IdString runtimeId in cell.List) - { - RuntimeItem runtimeItem = m_Bag.Content.GetRuntimeItem(runtimeId); - TrackRuntimeItemRecursive(runtimeItem); - } - } - } - - // [LOCAL-EDIT] #PILFER-INVENTORY-SERVER-LOOT - private static NetworkLootResponse BuildLootResponse( - NetworkLootRequest request, - bool authorized, - bool generated, - InventoryRejectionReason reason, - NetworkLootFailure failure) - { - return new NetworkLootResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - ContainerBagNetworkId = request.ContainerBagNetworkId, - Authorized = authorized, - Generated = generated, - RejectionReason = reason, - LootFailure = failure - }; - } - - private void BroadcastServerPickupFromDroppedItemIfNeeded(RuntimeItem item) - { - if (!m_IsServer || item == null) return; - long removedDropRuntimeHash = item.RuntimeID.Hash; - if (!TryTakeServerDroppedWorldItem(removedDropRuntimeHash, out ServerDroppedWorldItem droppedWorldItem)) - { - if (!TryTakeServerDroppedWorldItemForLocalPickup( - item, - transform.position, - out removedDropRuntimeHash, - out droppedWorldItem)) - { - LogPickupDebug( - $"{name}: server local add is not a remembered dropped item={DescribeRuntimeItem(item)} bag={NetworkId} knownDropCount={s_ServerDroppedWorldItems.Count}", - this); - return; - } - - LogPickupDebug( - $"{name}: server local pickup matched remembered drop by item/proximity item={DescribeRuntimeItem(item)} rememberedRuntime={removedDropRuntimeHash} sourceBag={droppedWorldItem.SourceBagNetworkId} dropPosition={droppedWorldItem.Position}", - this); - } - - TrackRuntimeItemRecursive(item); - - Vector2Int position = m_Bag.Content.FindPosition(item.RuntimeID); - NetworkInventoryManager manager = NetworkInventoryManager.Instance; - if (manager == null) return; - - var addBroadcast = new NetworkItemAddedBroadcast - { - BagNetworkId = NetworkId, - Item = ConvertToNetworkItem(item), - Position = position, - StackCount = position != TBagContent.INVALID - ? m_Bag.Content.GetContent(position)?.Count ?? 1 - : 1 - }; - - manager.BroadcastItemAdded(addBroadcast); - OnItemAdded?.Invoke(addBroadcast); - - manager.BroadcastDroppedItemRemoved(new NetworkDroppedItemRemovedBroadcast - { - SourceBagNetworkId = droppedWorldItem.SourceBagNetworkId, - RuntimeIdHash = removedDropRuntimeHash, - Position = droppedWorldItem.Position - }); - - bool destroyedLocalDrop = TryDestroyDroppedItemInstance(removedDropRuntimeHash); - LogPickupDebug( - $"{name}: server local pickup broadcast item={DescribeRuntimeItem(item)} removedDropRuntime={removedDropRuntimeHash} sourceBag={droppedWorldItem.SourceBagNetworkId} destinationBag={NetworkId} destroyedServerDropInstance={destroyedLocalDrop}", - this); - CacheCurrentSyncState(); - } - - private static void RememberServerDroppedWorldItem( - long runtimeIdHash, - uint sourceBagNetworkId, - NetworkRuntimeItem item, - Vector3 position) - { - if (runtimeIdHash == 0) return; - - PruneServerDroppedWorldItems(); - s_ServerDroppedWorldItems[runtimeIdHash] = new ServerDroppedWorldItem - { - SourceBagNetworkId = sourceBagNetworkId, - Item = item, - Position = position, - Time = Time.unscaledTime - }; - - LogPickupDebug( - $"remembered server dropped world item runtime={runtimeIdHash} sourceBag={sourceBagNetworkId} item={DescribeNetworkItem(item)} position={position} knownDropCount={s_ServerDroppedWorldItems.Count}"); - } - - private static bool TryGetServerDroppedWorldItem(long runtimeIdHash, out ServerDroppedWorldItem droppedWorldItem) - { - PruneServerDroppedWorldItems(); - return s_ServerDroppedWorldItems.TryGetValue(runtimeIdHash, out droppedWorldItem); - } - - private static bool TryTakeServerDroppedWorldItem(long runtimeIdHash, out ServerDroppedWorldItem droppedWorldItem) - { - PruneServerDroppedWorldItems(); - - if (s_ServerDroppedWorldItems.TryGetValue(runtimeIdHash, out droppedWorldItem)) - { - s_ServerDroppedWorldItems.Remove(runtimeIdHash); - return true; - } - - return false; - } - - private static bool TryTakeServerDroppedWorldItemForLocalPickup( - RuntimeItem localItem, - Vector3 pickerPosition, - out long runtimeIdHash, - out ServerDroppedWorldItem droppedWorldItem) - { - PruneServerDroppedWorldItems(); - runtimeIdHash = 0; - droppedWorldItem = default; - - if (localItem?.Item == null || s_ServerDroppedWorldItems.Count == 0) - { - return false; - } - - int itemHash = localItem.ItemID.Hash; - float bestDistance = float.MaxValue; - long bestRuntimeIdHash = 0; - ServerDroppedWorldItem bestItem = default; - - foreach (KeyValuePair entry in s_ServerDroppedWorldItems) - { - ServerDroppedWorldItem candidate = entry.Value; - if (candidate.Item.ItemHash != itemHash) continue; - - float distance = Vector3.SqrMagnitude(candidate.Position - pickerPosition); - if (distance >= bestDistance) continue; - - bestDistance = distance; - bestRuntimeIdHash = entry.Key; - bestItem = candidate; - } - - if (bestRuntimeIdHash == 0 || bestDistance > 16f) - { - return false; - } - - s_ServerDroppedWorldItems.Remove(bestRuntimeIdHash); - runtimeIdHash = bestRuntimeIdHash; - droppedWorldItem = bestItem; - return true; - } - - private static void PruneServerDroppedWorldItems() - { - if (s_ServerDroppedWorldItems.Count == 0) return; - - s_SharedRuntimeIdBuffer.Clear(); - float now = Time.unscaledTime; - foreach (KeyValuePair entry in s_ServerDroppedWorldItems) - { - if (now - entry.Value.Time <= 600f) continue; - s_SharedRuntimeIdBuffer.Add(entry.Key); - } - - for (int i = 0; i < s_SharedRuntimeIdBuffer.Count; i++) - { - s_ServerDroppedWorldItems.Remove(s_SharedRuntimeIdBuffer[i]); - } - } - - private void BroadcastServerSocketAttach(RuntimeItem parent, RuntimeItem attachment, IdString socketId) - { - if (!m_IsServer || parent == null || attachment == null) return; - - var broadcast = new NetworkSocketChangeBroadcast - { - BagNetworkId = NetworkId, - ParentRuntimeIdHash = parent.RuntimeID.Hash, - SocketHash = socketId.Hash, - HasAttachment = true, - Attachment = ConvertToNetworkItem(attachment) - }; - - NetworkInventoryManager.Instance?.BroadcastSocketChange(broadcast); - OnSocketChanged?.Invoke(broadcast); - CacheCurrentSyncState(); - } - - private void BroadcastServerSocketDetach(RuntimeItem parent) - { - if (!m_IsServer || parent == null) return; - - NetworkInventoryManager.Instance?.BroadcastFullSnapshot(GetFullSnapshot()); - CacheCurrentSyncState(); - } - - /// - /// [Server] Process equipment request. - /// - public async Task ProcessEquipmentRequest(NetworkEquipmentRequest request, uint clientNetworkId) - { - if (!m_IsServer) - { - return new NetworkEquipmentResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.NotAuthorized - }; - } - - bool equipSuccess = false; - int equippedIndex = -1; - - switch (request.Action) - { - case EquipmentAction.Equip: - case EquipmentAction.EquipToSlot: - case EquipmentAction.EquipToIndex: - { - if (!m_RuntimeItemMap.TryGetValue(request.RuntimeIdHash, out var equipItem)) - { - return new NetworkEquipmentResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.RuntimeItemNotFound - }; - } - - if (request.Action == EquipmentAction.EquipToIndex) - { - equipSuccess = await m_Bag.Equipment.EquipToIndex(equipItem, request.SlotOrIndex); - equippedIndex = request.SlotOrIndex; - } - else if (request.Action == EquipmentAction.EquipToSlot) - { - equipSuccess = await m_Bag.Equipment.Equip(equipItem, request.SlotOrIndex); - equippedIndex = m_Bag.Equipment.GetEquippedIndex(equipItem); - } - else - { - equipSuccess = await m_Bag.Equipment.Equip(equipItem); - equippedIndex = m_Bag.Equipment.GetEquippedIndex(equipItem); - } - - if (equipSuccess) - { - var equipBroadcast = new NetworkItemEquippedBroadcast - { - BagNetworkId = NetworkId, - RuntimeIdHash = request.RuntimeIdHash, - EquipmentIndex = equippedIndex - }; - NetworkInventoryManager.Instance?.BroadcastItemEquipped(equipBroadcast); - OnItemEquipped?.Invoke(equipBroadcast); - } - break; - } - - case EquipmentAction.Unequip: - { - if (!m_RuntimeItemMap.TryGetValue(request.RuntimeIdHash, out var unequipItem)) - { - return new NetworkEquipmentResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.RuntimeItemNotFound - }; - } - - equippedIndex = m_Bag.Equipment.GetEquippedIndex(unequipItem); - equipSuccess = await m_Bag.Equipment.Unequip(unequipItem); - - if (equipSuccess) - { - var unequipBroadcast = new NetworkItemUnequippedBroadcast - { - BagNetworkId = NetworkId, - RuntimeIdHash = request.RuntimeIdHash, - EquipmentIndex = equippedIndex - }; - NetworkInventoryManager.Instance?.BroadcastItemUnequipped(unequipBroadcast); - OnItemUnequipped?.Invoke(unequipBroadcast); - } - break; - } - - case EquipmentAction.UnequipFromIndex: - { - var slotId = m_Bag.Equipment.GetSlotRootRuntimeItemID(request.SlotOrIndex); - long runtimeIdHash = slotId.Hash; - - equipSuccess = await m_Bag.Equipment.UnequipFromIndex(request.SlotOrIndex); - equippedIndex = request.SlotOrIndex; - - if (equipSuccess) - { - var unequipIdxBroadcast = new NetworkItemUnequippedBroadcast - { - BagNetworkId = NetworkId, - RuntimeIdHash = runtimeIdHash, - EquipmentIndex = equippedIndex - }; - NetworkInventoryManager.Instance?.BroadcastItemUnequipped(unequipIdxBroadcast); - OnItemUnequipped?.Invoke(unequipIdxBroadcast); - } - break; - } - } - - return new NetworkEquipmentResponse - { - RequestId = request.RequestId, - Authorized = equipSuccess, - RejectionReason = equipSuccess ? InventoryRejectionReason.None : InventoryRejectionReason.CannotEquip, - EquippedIndex = equippedIndex - }; - } - - /// - /// [Server] Process socket request. - /// - public NetworkSocketResponse ProcessSocketRequest(NetworkSocketRequest request, uint clientNetworkId) - { - if (!m_IsServer) - { - return new NetworkSocketResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.NotAuthorized - }; - } - - if (!m_RuntimeItemMap.TryGetValue(request.ParentRuntimeIdHash, out var parentItem)) - { - return new NetworkSocketResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.RuntimeItemNotFound - }; - } - - bool socketSuccess = false; - NetworkRuntimeItem detachedItem = default; - NetworkRuntimeItem attachedItem = default; - int usedSocketHash = request.SocketHash; - IdString socketId = IdString.EMPTY; - - if (request.Action == SocketAction.AttachToSocket || request.Action == SocketAction.DetachFromSocket) - { - if (!TryResolveSocketId(parentItem, request.SocketHash, request.SocketIdString, out socketId)) - { - return new NetworkSocketResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.IdentityMismatch - }; - } - - usedSocketHash = socketId.Hash; - } - - switch (request.Action) - { - case SocketAction.Attach: - case SocketAction.AttachToSocket: - { - if (!m_RuntimeItemMap.TryGetValue(request.AttachmentRuntimeIdHash, out var attachment)) - { - return new NetworkSocketResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.RuntimeItemNotFound - }; - } - - if (request.Action == SocketAction.AttachToSocket) - { - socketSuccess = m_Bag.Equipment.AttachTo(parentItem, attachment, socketId); - } - else - { - socketSuccess = m_Bag.Equipment.AttachTo(parentItem, attachment); - } - - if (socketSuccess) - { - attachedItem = ConvertToNetworkItem(attachment); - } - break; - } - - case SocketAction.Detach: - case SocketAction.DetachFromSocket: - { - RuntimeItem detached; - if (request.Action == SocketAction.DetachFromSocket) - { - detached = m_Bag.Equipment.DetachFrom(parentItem, socketId); - } - else - { - if (!m_RuntimeItemMap.TryGetValue(request.AttachmentRuntimeIdHash, out var detachAttachment)) - { - return new NetworkSocketResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.RuntimeItemNotFound - }; - } - detached = m_Bag.Equipment.DetachFrom(parentItem, detachAttachment); - } - - socketSuccess = detached != null; - if (socketSuccess) - { - detachedItem = ConvertToNetworkItem(detached); - } - break; - } - } - - if (socketSuccess) - { - var socketBroadcast = new NetworkSocketChangeBroadcast - { - BagNetworkId = NetworkId, - ParentRuntimeIdHash = request.ParentRuntimeIdHash, - SocketHash = usedSocketHash, - HasAttachment = request.Action == SocketAction.Attach || request.Action == SocketAction.AttachToSocket, - Attachment = attachedItem - }; - NetworkInventoryManager.Instance?.BroadcastSocketChange(socketBroadcast); - OnSocketChanged?.Invoke(socketBroadcast); - } - - return new NetworkSocketResponse - { - RequestId = request.RequestId, - Authorized = socketSuccess, - RejectionReason = socketSuccess ? InventoryRejectionReason.None : InventoryRejectionReason.CannotAttach, - UsedSocketHash = usedSocketHash, - DetachedItem = detachedItem - }; - } - - /// - /// [Server] Process wealth request. - /// - public NetworkWealthResponse ProcessWealthRequest(NetworkWealthRequest request, uint clientNetworkId) - { - if (!m_IsServer) - { - return new NetworkWealthResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.NotAuthorized - }; - } - - if (!TryResolveCurrencyId(request.CurrencyHash, request.CurrencyIdString, out IdString currencyId)) - { - return new NetworkWealthResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.IdentityMismatch - }; - } - - int oldValue = m_Bag.Wealth.Get(currencyId); - int newValue; - - switch (request.Action) - { - case WealthAction.Set: - m_Bag.Wealth.Set(currencyId, request.Value); - newValue = request.Value; - break; - - case WealthAction.Add: - m_Bag.Wealth.Add(currencyId, request.Value); - newValue = oldValue + request.Value; - break; - - case WealthAction.Subtract: - if (oldValue < request.Value) - { - return new NetworkWealthResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.InsufficientFunds - }; - } - m_Bag.Wealth.Subtract(currencyId, request.Value); - newValue = oldValue - request.Value; - break; - - default: - return new NetworkWealthResponse - { - RequestId = request.RequestId, - Authorized = false, - RejectionReason = InventoryRejectionReason.InvalidOperation - }; - } - - // Broadcast - var wealthBroadcast = new NetworkWealthChangeBroadcast - { - BagNetworkId = NetworkId, - CurrencyHash = request.CurrencyHash, - NewValue = newValue, - Change = newValue - oldValue - }; - NetworkInventoryManager.Instance?.BroadcastWealthChange(wealthBroadcast); - OnWealthChanged?.Invoke(wealthBroadcast); - - return new NetworkWealthResponse - { - RequestId = request.RequestId, - Authorized = true, - RejectionReason = InventoryRejectionReason.None, - NewValue = newValue, - OldValue = oldValue - }; - } - - #endregion - - // ════════════════════════════════════════════════════════════════════════════════════════ - // BROADCAST RECEIVERS - // ════════════════════════════════════════════════════════════════════════════════════════ - - #region Broadcast Receivers - - public void ReceiveItemAddedBroadcast(NetworkItemAddedBroadcast broadcast) - { - if (m_IsServer) return; - - m_IsApplyingNetworkState = true; - try - { - if (broadcast.Item.RuntimeIdHash != 0 && - m_PendingPickupLocalRuntimeByServerRuntime.TryGetValue( - broadcast.Item.RuntimeIdHash, - out long provisionalRuntimeHash)) - { - m_PendingPickupLocalRuntimeByServerRuntime.Remove(broadcast.Item.RuntimeIdHash); - - if (provisionalRuntimeHash != broadcast.Item.RuntimeIdHash && - m_RuntimeItemMap.TryGetValue(provisionalRuntimeHash, out RuntimeItem provisionalItem)) - { - Vector2Int provisionalPosition = m_Bag.Content.FindPosition(provisionalItem.RuntimeID); - m_Bag.Content.Remove(provisionalItem); - UntrackRuntimeItemRecursive(provisionalItem); - - LogPickupDebug( - $"{name}: removed provisional local pickup item before authoritative add localRuntime={provisionalRuntimeHash} serverRuntime={broadcast.Item.RuntimeIdHash} provisionalPosition={provisionalPosition}", - this); - } - else - { - LogPickupDebug( - $"{name}: authoritative pickup add matched existing runtime serverRuntime={broadcast.Item.RuntimeIdHash}", - this); - } - } - - if (broadcast.Item.RuntimeIdHash != 0 && - (m_RuntimeItemMap.ContainsKey(broadcast.Item.RuntimeIdHash) || - m_Bag.Content.Contains(new IdString(broadcast.Item.RuntimeIdString)))) - { - LogPickupDebug( - $"{name}: item add broadcast skipped duplicate bag={broadcast.BagNetworkId} item={DescribeNetworkItem(broadcast.Item)} position={broadcast.Position}", - this); - return; - } - - // Reconstruct and apply - var runtimeItem = ReconstructRuntimeItem(broadcast.Item); - if (runtimeItem != null) - { - bool addedAtPosition = m_Bag.Content.Add(runtimeItem, broadcast.Position, true); - TrackRuntimeItemRecursive(runtimeItem); - LogPickupDebug( - $"{name}: item add broadcast applied bag={broadcast.BagNetworkId} item={DescribeRuntimeItem(runtimeItem)} requestedPosition={broadcast.Position} addedAtPosition={addedAtPosition} tracked={m_RuntimeItemMap.ContainsKey(runtimeItem.RuntimeID.Hash)}", - this); - } - else - { - LogPickupWarning( - $"{name}: item add broadcast failed reconstruct bag={broadcast.BagNetworkId} item={DescribeNetworkItem(broadcast.Item)}", - this); - } - } - finally - { - m_IsApplyingNetworkState = false; - } - - OnItemAdded?.Invoke(broadcast); - } - - public void ReceiveItemRemovedBroadcast(NetworkItemRemovedBroadcast broadcast) - { - if (m_IsServer) return; - - m_IsApplyingNetworkState = true; - try - { - if (m_RuntimeItemMap.TryGetValue(broadcast.RuntimeIdHash, out var runtimeItem)) - { - m_Bag.Content.Remove(runtimeItem); - UntrackRuntimeItemRecursive(runtimeItem); - } - } - finally - { - m_IsApplyingNetworkState = false; - } - - OnItemRemoved?.Invoke(broadcast); - } - - public void ReceiveItemMovedBroadcast(NetworkItemMovedBroadcast broadcast) - { - if (m_IsServer) return; - - m_IsApplyingNetworkState = true; - try - { - m_Bag.Content.Move(broadcast.FromPosition, broadcast.ToPosition, true); - } - finally - { - m_IsApplyingNetworkState = false; - } - - OnItemMoved?.Invoke(broadcast); - } - - public void ReceiveItemUsedBroadcast(NetworkItemUsedBroadcast broadcast) - { - if (m_IsServer) return; - - m_IsApplyingNetworkState = true; - try - { - if (broadcast.WasConsumed && m_RuntimeItemMap.TryGetValue(broadcast.RuntimeIdHash, out var runtimeItem)) - { - m_Bag.Content.Remove(runtimeItem); - UntrackRuntimeItemRecursive(runtimeItem); - } - } - finally - { - m_IsApplyingNetworkState = false; - } - - OnItemUsed?.Invoke(broadcast); - } - - public void ReceiveItemDroppedBroadcast(NetworkItemDroppedBroadcast broadcast) - { - if (m_IsServer) return; - if (TryAdoptPredictedDroppedItemInstance(broadcast)) - { - LogPickupDebug( - $"{name}: dropped item broadcast adopted local predicted drop sourceBag={broadcast.SourceBagNetworkId} item={DescribeNetworkItem(broadcast.Item)} position={broadcast.Position}", - this); - return; - } - - RuntimeItem runtimeItem = ReconstructRuntimeItem(broadcast.Item); - if (runtimeItem == null) - { - LogPickupWarning( - $"{name}: dropped item broadcast failed reconstruct sourceBag={broadcast.SourceBagNetworkId} item={DescribeNetworkItem(broadcast.Item)}", - this); - return; - } - - m_IsApplyingNetworkState = true; - try - { - GameObject instance = Item.Drop(runtimeItem, broadcast.Position, Quaternion.identity); - RememberDroppedItemInstance( - broadcast.Item.RuntimeIdHash, - instance, - broadcast.SourceBagNetworkId, - broadcast.Item, - broadcast.Position); - LogPickupDebug( - $"{name}: dropped item broadcast spawned instance sourceBag={broadcast.SourceBagNetworkId} item={DescribeRuntimeItem(runtimeItem)} position={broadcast.Position} instance={(instance != null ? instance.name : "null")}", - this); - } - finally - { - m_IsApplyingNetworkState = false; - } - } - - public void ReceiveDroppedItemRemovedBroadcast(NetworkDroppedItemRemovedBroadcast broadcast) - { - if (m_IsServer) return; - bool destroyed = TryDestroyDroppedItemInstance(broadcast, out long destroyedRuntimeIdHash); - LogPickupDebug( - $"{name}: dropped item remove broadcast sourceBag={broadcast.SourceBagNetworkId} runtime={broadcast.RuntimeIdHash} destroyed={destroyed} destroyedRuntime={destroyedRuntimeIdHash} position={broadcast.Position}", - this); - } - - public void ReceiveItemEquippedBroadcast(NetworkItemEquippedBroadcast broadcast) - { - if (m_IsServer) return; - - m_IsApplyingNetworkState = true; - try - { - if (m_RuntimeItemMap.TryGetValue(broadcast.RuntimeIdHash, out var runtimeItem)) - { - _ = m_Bag.Equipment.EquipToIndex(runtimeItem, broadcast.EquipmentIndex); - } - } - finally - { - m_IsApplyingNetworkState = false; - } - - OnItemEquipped?.Invoke(broadcast); - } - - public void ReceiveItemUnequippedBroadcast(NetworkItemUnequippedBroadcast broadcast) - { - if (m_IsServer) return; - - m_IsApplyingNetworkState = true; - try - { - _ = m_Bag.Equipment.UnequipFromIndex(broadcast.EquipmentIndex); - } - finally - { - m_IsApplyingNetworkState = false; - } - - OnItemUnequipped?.Invoke(broadcast); - } - - public void ReceiveSocketChangeBroadcast(NetworkSocketChangeBroadcast broadcast) - { - if (m_IsServer) return; - - m_IsApplyingNetworkState = true; - try - { - if (!m_RuntimeItemMap.TryGetValue(broadcast.ParentRuntimeIdHash, out RuntimeItem parentItem)) - { - OnSocketChanged?.Invoke(broadcast); - return; - } - - if (!TryResolveRuntimeSocketId(parentItem, broadcast.SocketHash, null, out IdString socketId) || - !parentItem.Sockets.TryGetValue(socketId, out RuntimeSocket socket)) - { - OnSocketChanged?.Invoke(broadcast); - return; - } - - RuntimeItem previousAttachment = socket.Attachment; - RuntimeItem nextAttachment = null; - if (broadcast.HasAttachment) - { - RuntimeItem attachment = ReconstructRuntimeItem(broadcast.Attachment); - if (attachment != null) - { - if (m_Bag.Content.Contains(attachment)) - { - m_Bag.Content.Remove(attachment); - } - - if (s_RuntimeSocketAttachmentField != null) - { - s_RuntimeSocketAttachmentField.SetValue(socket, attachment); - } - - TrackRuntimeItemRecursive(attachment); - nextAttachment = attachment; - } - } - else if (s_RuntimeSocketAttachmentField != null) - { - s_RuntimeSocketAttachmentField.SetValue(socket, null); - } - - if (previousAttachment != null && - (nextAttachment == null || previousAttachment.RuntimeID.Hash != nextAttachment.RuntimeID.Hash)) - { - UntrackRuntimeItemRecursive(previousAttachment); - } - } - finally - { - m_IsApplyingNetworkState = false; - } - - OnSocketChanged?.Invoke(broadcast); - } - - public void ReceiveWealthChangeBroadcast(NetworkWealthChangeBroadcast broadcast) - { - if (m_IsServer) return; - - m_IsApplyingNetworkState = true; - try - { - if (TryResolveCurrencyIdByHash(broadcast.CurrencyHash, out IdString currencyId)) - { - m_Bag.Wealth.Set(currencyId, broadcast.NewValue); - } - } - finally - { - m_IsApplyingNetworkState = false; - } - - OnWealthChanged?.Invoke(broadcast); - } - - public void ReceiveFullSnapshot(NetworkInventorySnapshot snapshot) - { - if (m_IsServer) return; - - if (snapshot.BagNetworkId != 0 && snapshot.BagNetworkId != NetworkId) - { - return; - } - - m_IsApplyingNetworkState = true; - try - { - ApplyFullSnapshot(snapshot); - } - finally - { - m_IsApplyingNetworkState = false; - } - - if (m_LogAllChanges) - { - Debug.Log($"[NetworkInventoryController] Received full snapshot: {snapshot.Cells?.Length ?? 0} cells"); - } - } - - public void ReceiveDelta(NetworkInventoryDelta delta) - { - if (m_IsServer) return; - - if (delta.BagNetworkId != 0 && delta.BagNetworkId != NetworkId) - { - return; - } - - const uint maskCells = 1u << 0; - const uint maskEquipment = 1u << 1; - const uint maskWealth = 1u << 2; - - m_IsApplyingNetworkState = true; - try - { - if ((delta.ChangeMask & maskCells) != 0 && delta.ChangedCells != null) - { - ApplyCellDelta(delta.ChangedCells); - } - - if ((delta.ChangeMask & maskEquipment) != 0 && delta.ChangedEquipment != null) - { - ApplyEquipmentDelta(delta.ChangedEquipment); - } - - if ((delta.ChangeMask & maskWealth) != 0 && delta.ChangedWealth != null) - { - ApplyWealthDelta(delta.ChangedWealth); - } - } - finally - { - m_IsApplyingNetworkState = false; - } - - CacheCurrentSyncState(); - - if (m_LogAllChanges) - { - Debug.Log( - $"[NetworkInventoryController] Applied partial delta (mask={delta.ChangeMask}) " + - $"cells={delta.ChangedCells?.Length ?? 0} " + - $"equipment={delta.ChangedEquipment?.Length ?? 0} " + - $"wealth={delta.ChangedWealth?.Length ?? 0}"); - } - } - - #endregion - } -} -#endif diff --git a/NetworkInventoryController.WorldObjectPickup.cs b/NetworkInventoryController.WorldObjectPickup.cs deleted file mode 100644 index 114aa91..0000000 --- a/NetworkInventoryController.WorldObjectPickup.cs +++ /dev/null @@ -1,264 +0,0 @@ -#if GC2_INVENTORY -using UnityEngine; -using GameCreator.Runtime.Inventory; - -namespace Arawn.GameCreator2.Networking.Inventory -{ - public partial class NetworkInventoryController - { - // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT - public void RequestWorldObjectPickup(NetworkWorldObject worldObject, Vector2Int destinationPosition) - { - if (worldObject == null) return; - if (m_IsRemoteClient) return; - if (!m_IsLocalClient && !m_IsServer) return; - - if (!worldObject.AllowPickup || worldObject.Item == null) - { - LogPickupWarning( - $"{name}: world object pickup skipped invalid source prop={worldObject.NetworkId} allow={worldObject.AllowPickup} item={(worldObject.Item != null ? worldObject.Item.ID.String : "null")}", - this); - return; - } - - if (!TryGetLocalActorNetworkId(out uint actorNetworkId)) - { - LogPickupWarning($"{name}: world object pickup skipped no local actor network id prop={worldObject.NetworkId}", this); - return; - } - - var request = new NetworkPickupRequest - { - RequestId = GetNextRequestId(), - ActorNetworkId = actorNetworkId, - CorrelationId = NetworkCorrelation.Compose(actorNetworkId, m_LastIssuedRequestId), - PickerBagNetworkId = NetworkId, - PropNetworkId = worldObject.NetworkId, - SourceBagNetworkId = 0, - RuntimeIdHash = 0, - DestinationPosition = destinationPosition - }; - - LogPickupDebug( - $"{name}: sending world object pickup request req={request.RequestId} actor={actorNetworkId} pickerBag={NetworkId} prop={request.PropNetworkId} item={worldObject.Item.ID.String} destination={destinationPosition} server={m_IsServer} local={m_IsLocalClient}", - this); - - if (m_IsServer) - { - NetworkPickupResponse response = ProcessPickupRequest(request, NetworkId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - if (!response.Authorized) - { - OnOperationRejected?.Invoke(response.RejectionReason, "World object pickup"); - } - return; - } - - NetworkInventoryManager.Instance?.SendPickupRequest(request); - } - - // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT - private bool TryProcessWorldObjectPickupRequest( - NetworkPickupRequest request, - uint clientNetworkId, - out NetworkPickupResponse response) - { - response = default; - - if (request.PropNetworkId == 0) return false; - - if (!NetworkWorldObjectRegistry.TryGet(request.PropNetworkId, out NetworkWorldObject worldObject)) - { - if (NetworkWorldObjectRegistry.IsConsumed(request.PropNetworkId)) - { - // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT-CONSUMED-REGISTRY - LogPickupWarning( - $"{name}: world object pickup rejected consumed missing instance req={request.RequestId} prop={request.PropNetworkId} client={clientNetworkId}", - this); - response = BuildWorldObjectPickupResponse( - request, - false, - InventoryRejectionReason.InvalidOperation, - NetworkPickupFailure.WorldObjectConsumed, - default, - TBagContent.INVALID); - return true; - } - - LogPickupWarning( - $"{name}: world object pickup rejected prop not found req={request.RequestId} prop={request.PropNetworkId} client={clientNetworkId}", - this); - response = BuildWorldObjectPickupResponse( - request, - false, - InventoryRejectionReason.RuntimeItemNotFound, - NetworkPickupFailure.WorldObjectNotFound, - default, - TBagContent.INVALID); - return true; - } - - if (!worldObject.AllowPickup) - { - LogPickupWarning( - $"{name}: world object pickup rejected disabled req={request.RequestId} prop={request.PropNetworkId} allow={worldObject.AllowPickup} consumed={worldObject.IsConsumed} item={(worldObject.Item != null ? worldObject.Item.ID.String : "null")}", - worldObject); - response = BuildWorldObjectPickupResponse( - request, - false, - InventoryRejectionReason.InvalidOperation, - NetworkPickupFailure.WorldObjectPickupDisabled, - default, - TBagContent.INVALID); - return true; - } - - if (worldObject.Item == null) - { - LogPickupWarning( - $"{name}: world object pickup rejected missing item req={request.RequestId} prop={request.PropNetworkId} allow={worldObject.AllowPickup} consumed={worldObject.IsConsumed}", - worldObject); - response = BuildWorldObjectPickupResponse( - request, - false, - InventoryRejectionReason.ItemNotFound, - NetworkPickupFailure.WorldObjectItemMissing, - default, - TBagContent.INVALID); - return true; - } - - if (worldObject.IsConsumed) - { - LogPickupWarning( - $"{name}: world object pickup rejected consumed req={request.RequestId} prop={request.PropNetworkId} item={worldObject.Item.ID.String}", - worldObject); - response = BuildWorldObjectPickupResponse( - request, - false, - InventoryRejectionReason.InvalidOperation, - NetworkPickupFailure.WorldObjectConsumed, - default, - TBagContent.INVALID); - return true; - } - - float pickupDistance3D = worldObject.GetDistanceTo(transform.position); - float pickupHorizontalDistance = worldObject.GetHorizontalDistanceTo(transform.position); - - if (!worldObject.CanPickupFrom(transform.position)) - { - // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT-REJECT-DIAGNOSTICS - LogPickupWarning( - $"{name}: world object pickup rejected out of range req={request.RequestId} prop={request.PropNetworkId} pickerPosition={transform.position} propPosition={worldObject.transform.position} distance3D={pickupDistance3D} horizontalDistance={pickupHorizontalDistance} radius={worldObject.PickupRadius}", - worldObject); - response = BuildWorldObjectPickupResponse( - request, - false, - InventoryRejectionReason.InvalidPosition, - NetworkPickupFailure.WorldObjectOutOfRange, - default, - TBagContent.INVALID); - return true; - } - - RuntimeItem runtimeItem = worldObject.CreatePickupRuntimeItem(); - if (runtimeItem == null) - { - response = BuildWorldObjectPickupResponse( - request, - false, - InventoryRejectionReason.IdentityMismatch, - NetworkPickupFailure.WorldObjectRuntimeItemFailed, - default, - TBagContent.INVALID); - return true; - } - - Vector2Int finalPosition; - m_IsApplyingNetworkState = true; - try - { - if (request.DestinationPosition.x >= 0 && request.DestinationPosition.y >= 0) - { - bool added = m_Bag.Content.Add(runtimeItem, request.DestinationPosition, true); - finalPosition = added ? request.DestinationPosition : TBagContent.INVALID; - } - else - { - finalPosition = m_Bag.Content.Add(runtimeItem, true); - } - } - finally - { - m_IsApplyingNetworkState = false; - } - - if (finalPosition == TBagContent.INVALID) - { - response = BuildWorldObjectPickupResponse( - request, - false, - InventoryRejectionReason.InsufficientSpace, - NetworkPickupFailure.None, - default, - TBagContent.INVALID); - return true; - } - - worldObject.MarkPickedUp(); - TrackRuntimeItemRecursive(runtimeItem); - - NetworkRuntimeItem networkItem = ConvertToNetworkItem(runtimeItem); - var addBroadcast = new NetworkItemAddedBroadcast - { - BagNetworkId = NetworkId, - Item = networkItem, - Position = finalPosition, - StackCount = m_Bag.Content.GetContent(finalPosition)?.Count ?? 1 - }; - - NetworkInventoryManager.Instance?.BroadcastItemAdded(addBroadcast); - OnItemAdded?.Invoke(addBroadcast); - CacheCurrentSyncState(); - - LogPickupDebug( - $"{name}: world object pickup accepted req={request.RequestId} prop={request.PropNetworkId} item={DescribeRuntimeItem(runtimeItem)} finalPosition={finalPosition} distance3D={pickupDistance3D} horizontalDistance={pickupHorizontalDistance} radius={worldObject.PickupRadius}", - this); - - response = BuildWorldObjectPickupResponse( - request, - true, - InventoryRejectionReason.None, - NetworkPickupFailure.None, - networkItem, - finalPosition); - return true; - } - - // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT - private static NetworkPickupResponse BuildWorldObjectPickupResponse( - NetworkPickupRequest request, - bool authorized, - InventoryRejectionReason reason, - NetworkPickupFailure pickupFailure, - NetworkRuntimeItem item, - Vector2Int position) - { - return new NetworkPickupResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - PropNetworkId = request.PropNetworkId, - PickupFailure = pickupFailure, - Authorized = authorized, - RejectionReason = reason, - PickedUpItem = item, - PlacedPosition = position - }; - } - } -} -#endif diff --git a/NetworkInventoryController.cs b/NetworkInventoryController.cs deleted file mode 100644 index 4e4ea6d..0000000 --- a/NetworkInventoryController.cs +++ /dev/null @@ -1,536 +0,0 @@ -#if GC2_INVENTORY -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using UnityEngine; -using GameCreator.Runtime.Common; -using GameCreator.Runtime.Inventory; - -namespace Arawn.GameCreator2.Networking.Inventory -{ - /// - /// Server-authoritative inventory controller for GC2 Bag. - /// Intercepts all inventory operations and routes through server validation. - /// - /// - /// - /// Purpose: - /// In competitive multiplayer, inventory operations MUST be server-authoritative - /// to prevent item duplication, gold exploits, and illegal crafting. - /// - /// - /// Architecture: - /// - Clients send operation requests to server - /// - Server validates and applies changes - /// - Server broadcasts confirmed changes to all clients - /// - /// - [RequireComponent(typeof(Bag))] - [AddComponentMenu("Game Creator/Network/Inventory/Network Inventory Controller")] - [DefaultExecutionOrder(ApplicationManager.EXECUTION_ORDER_DEFAULT + 5)] - public partial class NetworkInventoryController : MonoBehaviour - { - // ════════════════════════════════════════════════════════════════════════════════════════ - // INSPECTOR - // ════════════════════════════════════════════════════════════════════════════════════════ - - [Header("Network Settings")] - [Tooltip("Apply changes optimistically before server confirmation (items only).")] - [SerializeField] private bool m_OptimisticUpdates = false; - - [Tooltip("Rollback optimistic updates if server rejects.")] - [SerializeField] private bool m_RollbackOnReject = true; - - [Tooltip("Optional stable network id for scene/world bags. Leave 0 to derive one from the scene hierarchy.")] - [SerializeField] private uint m_StaticNetworkIdOverride = 0; - - [Header("Sync Settings")] - [Tooltip("Send full state sync at this interval (seconds). 0 = never.")] - [SerializeField] private float m_FullSyncInterval = 10f; - - [Tooltip("Send delta updates at this interval (seconds).")] - [SerializeField] private float m_DeltaSyncInterval = 0.2f; - - [Header("Validation")] - [Tooltip("Log rejected operations for debugging.")] - [SerializeField] private bool m_LogRejections = false; - - [Header("Debug")] - [SerializeField] private bool m_LogAllChanges = false; - - // ════════════════════════════════════════════════════════════════════════════════════════ - // EVENTS - // ════════════════════════════════════════════════════════════════════════════════════════ - - // Content events - public event Action OnContentAddRequested; - public event Action OnItemAdded; - public event Action OnContentRemoveRequested; - public event Action OnItemRemoved; - public event Action OnContentMoveRequested; - public event Action OnItemMoved; - public event Action OnContentUseRequested; - public event Action OnItemUsed; - - // Equipment events - public event Action OnEquipmentRequested; - public event Action OnItemEquipped; - public event Action OnItemUnequipped; - - // Socket events - public event Action OnSocketRequested; - public event Action OnSocketChanged; - - // Wealth events - public event Action OnWealthRequested; - public event Action OnWealthChanged; - - // Rejection event - public event Action OnOperationRejected; - - // ════════════════════════════════════════════════════════════════════════════════════════ - // PRIVATE FIELDS - // ════════════════════════════════════════════════════════════════════════════════════════ - - private Bag m_Bag; - private NetworkCharacter m_NetworkCharacter; - private uint m_CachedStaticNetworkId; - private bool m_IsApplyingNetworkState; - - // Network role - private bool m_IsServer; - private bool m_IsLocalClient; - private bool m_IsRemoteClient; - - // Request tracking - private ushort m_NextRequestId = 1; - private ushort m_LastIssuedRequestId = 1; - private static readonly List s_SharedKeyBuffer = new(16); - private static readonly List s_SharedRuntimeIdBuffer = new(16); - private readonly Dictionary m_PendingAdds = new(16); - private readonly Dictionary m_PendingRemoves = new(16); - private readonly Dictionary m_PendingMoves = new(16); - private readonly Dictionary m_PendingEquipment = new(8); - private readonly Dictionary m_PendingWealth = new(8); - private readonly Dictionary m_PendingPickupLocalRuntimeByServerRuntime = new(8); - - // State tracking for delta sync - private readonly Dictionary m_LastSyncedPositions = new(32); - private readonly Dictionary m_LastSyncedWealth = new(8); - private readonly Dictionary m_LastSyncedEquipment = new(8); - private float m_LastFullSync; - private float m_LastDeltaSync; - - // RuntimeItem ID mapping (for server-assigned IDs) - private readonly Dictionary m_RuntimeItemMap = new(64); - - private static readonly List s_Controllers = new(64); - private static readonly List s_PendingLocalRemovals = new(32); - private static readonly HashSet s_LocalDropRuntimeIds = new(); - private static readonly Dictionary s_DroppedItemInstances = new(); - private static readonly Dictionary s_ServerDroppedWorldItems = new(); - private static bool s_StaticHooksInstalled; - private static NetworkInventoryController s_LocalPlayerController; - - // ════════════════════════════════════════════════════════════════════════════════════════ - // STRUCTS - // ════════════════════════════════════════════════════════════════════════════════════════ - - private struct PendingContentAdd : ITimedPendingRequest - { - public NetworkContentAddRequest Request; - public float SentTime; - public float PendingSentTime => SentTime; - } - - private struct PendingContentRemove : ITimedPendingRequest - { - public NetworkContentRemoveRequest Request; - public RuntimeItem RemovedItem; // For rollback - public float SentTime; - public float PendingSentTime => SentTime; - } - - private struct PendingContentMove : ITimedPendingRequest - { - public NetworkContentMoveRequest Request; - public float SentTime; - public float PendingSentTime => SentTime; - } - - private struct PendingEquipment : ITimedPendingRequest - { - public NetworkEquipmentRequest Request; - public float SentTime; - public float PendingSentTime => SentTime; - } - - private struct PendingWealth : ITimedPendingRequest - { - public NetworkWealthRequest Request; - public int OriginalValue; - public float SentTime; - public float PendingSentTime => SentTime; - } - - private struct PendingLocalRemoval - { - public NetworkInventoryController SourceController; - public NetworkRuntimeItem Item; - public long RuntimeIdHash; - public float Time; - } - - private struct DroppedItemInstance - { - public GameObject Instance; - public uint SourceBagNetworkId; - public NetworkRuntimeItem Item; - public Vector3 Position; - } - - private struct ServerDroppedWorldItem - { - public uint SourceBagNetworkId; - public NetworkRuntimeItem Item; - public Vector3 Position; - public float Time; - } - - private static void LogPickupDebug(string message, UnityEngine.Object context = null) - { - if (context != null) Debug.Log($"[NetworkInventoryPickupDebug] {message}", context); - else Debug.Log($"[NetworkInventoryPickupDebug] {message}"); - } - - private static void LogPickupWarning(string message, UnityEngine.Object context = null) - { - if (context != null) Debug.LogWarning($"[NetworkInventoryPickupDebug] {message}", context); - else Debug.LogWarning($"[NetworkInventoryPickupDebug] {message}"); - } - - private static string DescribeRuntimeItem(RuntimeItem item) - { - if (item == null) return "null"; - return $"{item.ItemID.String} runtime={item.RuntimeID.String} hash={item.RuntimeID.Hash}"; - } - - private static string DescribeNetworkItem(NetworkRuntimeItem item) - { - return $"{item.ItemIdString} runtime={item.RuntimeIdString} hash={item.RuntimeIdHash} itemHash={item.ItemHash}"; - } - - // ════════════════════════════════════════════════════════════════════════════════════════ - // PROPERTIES - // ════════════════════════════════════════════════════════════════════════════════════════ - - /// The underlying GC2 Bag component. - public Bag Bag => m_Bag; - - /// Network ID of this bag's owner. - public uint NetworkId => m_NetworkCharacter != null - ? m_NetworkCharacter.NetworkId - : GetStaticNetworkId(); - - /// Whether this inventory is backed by a spawned NetworkCharacter. - public bool UsesNetworkCharacterId => m_NetworkCharacter != null; - - /// Whether this inventory is a scene/world bag such as a chest. - public bool IsWorldInventory => m_NetworkCharacter == null; - - /// Whether this is running on the server. - public bool IsServer => m_IsServer; - - /// Whether this is the local player's inventory. - public bool IsLocalClient => m_IsLocalClient; - - public bool OptimisticUpdates => m_OptimisticUpdates; - - public bool RollbackOnReject => m_RollbackOnReject; - - // ════════════════════════════════════════════════════════════════════════════════════════ - // UNITY LIFECYCLE - // ════════════════════════════════════════════════════════════════════════════════════════ - - private void Awake() - { - m_Bag = GetComponent(); - m_NetworkCharacter = GetComponent(); - } - - private void Start() - { - // Subscribe to GC2 events for local change detection - SubscribeToBagEvents(); - } - - private void OnDestroy() - { - UnsubscribeFromBagEvents(); - } - - private void Update() - { - if (!m_IsServer && !m_IsLocalClient) return; - - float currentTime = Time.time; - - // Server-side sync - if (m_IsServer) - { - if (m_FullSyncInterval > 0 && currentTime - m_LastFullSync > m_FullSyncInterval) - { - BroadcastFullState(); - m_LastFullSync = currentTime; - } - - if (m_DeltaSyncInterval > 0 && currentTime - m_LastDeltaSync > m_DeltaSyncInterval) - { - BroadcastDeltaState(); - m_LastDeltaSync = currentTime; - } - } - - CleanupPendingRequests(); - } - - // ════════════════════════════════════════════════════════════════════════════════════════ - // INITIALIZATION - // ════════════════════════════════════════════════════════════════════════════════════════ - - /// - /// Initialize the network inventory controller with role information. - /// - public void Initialize(bool isServer, bool isLocalClient) - { - m_IsServer = isServer; - m_IsLocalClient = isLocalClient; - m_IsRemoteClient = !isServer && !isLocalClient; - - if (m_IsLocalClient && UsesNetworkCharacterId && NetworkId != 0) - { - s_LocalPlayerController = this; - } - - InitializeStateTracking(); - - if (IsWorldInventory) - { - LogPickupDebug( - $"{name}: world inventory initialized networkId={NetworkId} path={BuildStableScenePath(transform)} server={m_IsServer} local={m_IsLocalClient} remote={m_IsRemoteClient} trackedItems={m_RuntimeItemMap.Count}", - this); - } - - if (m_LogAllChanges) - { - string role = m_IsServer ? "Server" : (m_IsLocalClient ? "LocalClient" : "RemoteClient"); - Debug.Log($"[NetworkInventoryController] {gameObject.name} initialized as {role}"); - } - } - - private void InitializeStateTracking() - { - // Build RuntimeItem map - m_RuntimeItemMap.Clear(); - foreach (var cell in m_Bag.Content.CellList) - { - if (cell == null || cell.Available) continue; - - foreach (var runtimeIdEntry in cell.List) - { - var runtimeItem = m_Bag.Content.GetRuntimeItem(runtimeIdEntry); - if (runtimeItem != null) - { - TrackRuntimeItemRecursive(runtimeItem); - } - } - } - - // Cache initial wealth - foreach (var currencyId in m_Bag.Wealth.List) - { - m_LastSyncedWealth[currencyId.Hash] = m_Bag.Wealth.Get(currencyId); - } - - CacheCurrentSyncState(); - } - - private void SubscribeToBagEvents() - { - if (!s_Controllers.Contains(this)) - { - s_Controllers.Add(this); - } - - InstallStaticInventoryHooks(); - - if (m_Bag.Content != null) - { - m_Bag.Content.EventAdd += OnLocalItemAdded; - m_Bag.Content.EventRemove += OnLocalItemRemoved; - m_Bag.Content.EventUse += OnLocalItemUsed; - } - - if (m_Bag.Equipment != null) - { - m_Bag.Equipment.EventEquip += OnLocalItemEquipped; - m_Bag.Equipment.EventUnequip += OnLocalItemUnequipped; - } - - if (m_Bag.Wealth != null) - { - m_Bag.Wealth.EventChange += OnLocalWealthChanged; - } - } - - private void UnsubscribeFromBagEvents() - { - s_Controllers.Remove(this); - if (s_LocalPlayerController == this) - { - s_LocalPlayerController = null; - } - - UninstallStaticInventoryHooksIfUnused(); - - if (m_Bag != null) - { - if (m_Bag.Content != null) - { - m_Bag.Content.EventAdd -= OnLocalItemAdded; - m_Bag.Content.EventRemove -= OnLocalItemRemoved; - m_Bag.Content.EventUse -= OnLocalItemUsed; - } - - if (m_Bag.Equipment != null) - { - m_Bag.Equipment.EventEquip -= OnLocalItemEquipped; - m_Bag.Equipment.EventUnequip -= OnLocalItemUnequipped; - } - - if (m_Bag.Wealth != null) - { - m_Bag.Wealth.EventChange -= OnLocalWealthChanged; - } - } - } - - private ushort GetNextRequestId() - { - if (m_NextRequestId == 0) - { - m_NextRequestId = 1; - } - - ushort requestId = m_NextRequestId; - m_NextRequestId++; - if (m_NextRequestId == 0) - { - m_NextRequestId = 1; - } - - m_LastIssuedRequestId = requestId; - return requestId; - } - - private static ulong GetPendingKey(uint actorNetworkId, uint correlationId, ushort requestId) - { - uint pendingCorrelation = correlationId != 0 ? correlationId : requestId; - return ((ulong)actorNetworkId << 32) | pendingCorrelation; - } - - private uint GetStaticNetworkId() - { - if (m_StaticNetworkIdOverride != 0) return m_StaticNetworkIdOverride; - if (m_CachedStaticNetworkId != 0) return m_CachedStaticNetworkId; - - string path = BuildStableScenePath(transform); - uint hash = 2166136261u; - for (int i = 0; i < path.Length; i++) - { - hash ^= path[i]; - hash *= 16777619u; - } - - m_CachedStaticNetworkId = 0x80000000u | (hash & 0x7FFFFFFFu); - if (m_CachedStaticNetworkId == 0) m_CachedStaticNetworkId = 0x80000001u; - return m_CachedStaticNetworkId; - } - - private static string BuildStableScenePath(Transform target) - { - if (target == null) return string.Empty; - - string scenePath = target.gameObject.scene.path; - if (string.IsNullOrEmpty(scenePath)) scenePath = target.gameObject.scene.name; - - string path = BuildStableScenePathSegment(target); - Transform current = target; - while (current.parent != null) - { - current = current.parent; - path = $"{BuildStableScenePathSegment(current)}/{path}"; - } - - return $"{scenePath}:{path}"; - } - - private static string BuildStableScenePathSegment(Transform target) - { - int sameNameIndex = 0; - Transform parent = target.parent; - if (parent != null) - { - for (int i = 0; i < parent.childCount; i++) - { - Transform sibling = parent.GetChild(i); - if (sibling == target) break; - if (sibling != null && sibling.name == target.name) - { - sameNameIndex++; - } - } - } - else if (target.gameObject.scene.IsValid()) - { - GameObject[] roots = target.gameObject.scene.GetRootGameObjects(); - for (int i = 0; i < roots.Length; i++) - { - GameObject root = roots[i]; - if (root == null) continue; - if (root.transform == target) break; - if (root.name == target.name) - { - sameNameIndex++; - } - } - } - - return $"{target.name}[{sameNameIndex}]"; - } - - private static void InstallStaticInventoryHooks() - { - if (s_StaticHooksInstalled) return; - - RuntimeSockets.EventAttachRuntimeItem -= HandleGlobalSocketAttached; - RuntimeSockets.EventAttachRuntimeItem += HandleGlobalSocketAttached; - RuntimeSockets.EventDetachRuntimeItem -= HandleGlobalSocketDetached; - RuntimeSockets.EventDetachRuntimeItem += HandleGlobalSocketDetached; - Item.EventInstantiate -= HandleGlobalItemInstantiated; - Item.EventInstantiate += HandleGlobalItemInstantiated; - s_StaticHooksInstalled = true; - } - - private static void UninstallStaticInventoryHooksIfUnused() - { - if (!s_StaticHooksInstalled || s_Controllers.Count > 0) return; - - RuntimeSockets.EventAttachRuntimeItem -= HandleGlobalSocketAttached; - RuntimeSockets.EventDetachRuntimeItem -= HandleGlobalSocketDetached; - Item.EventInstantiate -= HandleGlobalItemInstantiated; - s_StaticHooksInstalled = false; - } - } -} -#endif diff --git a/NetworkInventoryManager.cs b/NetworkInventoryManager.cs deleted file mode 100644 index e2468f5..0000000 --- a/NetworkInventoryManager.cs +++ /dev/null @@ -1,1815 +0,0 @@ -#if GC2_INVENTORY -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using UnityEngine; -using Arawn.GameCreator2.Networking; -using Arawn.GameCreator2.Networking.Security; - -namespace Arawn.GameCreator2.Networking.Inventory -{ - /// - /// Global manager for inventory network communication. - /// Transport-agnostic - wire up delegates to your networking solution. - /// - [AddComponentMenu("Game Creator/Network/Inventory/Network Inventory Manager")] - public class NetworkInventoryManager : NetworkSingleton - { - // ════════════════════════════════════════════════════════════════════════════════════════ - // SINGLETON (lazy-find override) - // ════════════════════════════════════════════════════════════════════════════════════════ - - /// Singleton instance. Falls back to FindFirstObjectByType if not yet assigned. - public new static NetworkInventoryManager Instance - { - get - { - if (s_Instance == null) - s_Instance = FindFirstObjectByType(); - return s_Instance; - } - } - - // ════════════════════════════════════════════════════════════════════════════════════════ - // TRANSPORT DELEGATES - Wire to your networking solution - // ════════════════════════════════════════════════════════════════════════════════════════ - - // ───────────────────────────────────────────────────────────────────────────────────────── - // CLIENT → SERVER: Content Operations - // ───────────────────────────────────────────────────────────────────────────────────────── - - public Action OnSendContentAddRequest; - public Action OnSendContentRemoveRequest; - public Action OnSendContentMoveRequest; - public Action OnSendContentUseRequest; - public Action OnSendContentDropRequest; - - // ───────────────────────────────────────────────────────────────────────────────────────── - // CLIENT → SERVER: Equipment Operations - // ───────────────────────────────────────────────────────────────────────────────────────── - - public Action OnSendEquipmentRequest; - - // ───────────────────────────────────────────────────────────────────────────────────────── - // CLIENT → SERVER: Socket Operations - // ───────────────────────────────────────────────────────────────────────────────────────── - - public Action OnSendSocketRequest; - - // ───────────────────────────────────────────────────────────────────────────────────────── - // CLIENT → SERVER: Wealth Operations - // ───────────────────────────────────────────────────────────────────────────────────────── - - public Action OnSendWealthRequest; - - // ───────────────────────────────────────────────────────────────────────────────────────── - // CLIENT → SERVER: Merchant Operations - // ───────────────────────────────────────────────────────────────────────────────────────── - - public Action OnSendMerchantRequest; - - // ───────────────────────────────────────────────────────────────────────────────────────── - // CLIENT → SERVER: Crafting Operations - // ───────────────────────────────────────────────────────────────────────────────────────── - - public Action OnSendCraftingRequest; - - // ───────────────────────────────────────────────────────────────────────────────────────── - // CLIENT → SERVER: Transfer Operations - // ───────────────────────────────────────────────────────────────────────────────────────── - - public Action OnSendTransferRequest; - public Action OnSendPickupRequest; - // [LOCAL-EDIT] #PILFER-INVENTORY-SERVER-LOOT - public Action OnSendLootRequest; - public Action OnSendCombineRequest; - - // ───────────────────────────────────────────────────────────────────────────────────────── - // SERVER → CLIENT: Responses (Single target) - // ───────────────────────────────────────────────────────────────────────────────────────── - - public Action OnSendContentAddResponse; - public Action OnSendContentRemoveResponse; - public Action OnSendContentMoveResponse; - public Action OnSendContentUseResponse; - public Action OnSendContentDropResponse; - public Action OnSendEquipmentResponse; - public Action OnSendSocketResponse; - public Action OnSendWealthResponse; - public Action OnSendMerchantResponse; - public Action OnSendCraftingResponse; - public Action OnSendTransferResponse; - public Action OnSendPickupResponse; - // [LOCAL-EDIT] #PILFER-INVENTORY-SERVER-LOOT - public Action OnSendLootResponse; - public Action OnSendCombineResponse; - - // ───────────────────────────────────────────────────────────────────────────────────────── - // SERVER → ALL CLIENTS: Broadcasts - // ───────────────────────────────────────────────────────────────────────────────────────── - - public Action OnBroadcastItemAdded; - public Action OnBroadcastItemRemoved; - public Action OnBroadcastItemDropped; - public Action OnBroadcastDroppedItemRemoved; - public Action OnBroadcastItemMoved; - public Action OnBroadcastItemUsed; - public Action OnBroadcastItemEquipped; - public Action OnBroadcastItemUnequipped; - public Action OnBroadcastSocketChange; - public Action OnBroadcastWealthChange; - public Action OnBroadcastPropertyChange; - public Action OnBroadcastFullSnapshot; - public Action OnBroadcastDelta; - - // ───────────────────────────────────────────────────────────────────────────────────────── - // SERVER → SINGLE CLIENT: Targeted - // ───────────────────────────────────────────────────────────────────────────────────────── - - public Action OnSendSnapshotToClient; - - // ════════════════════════════════════════════════════════════════════════════════════════ - // INSPECTOR - // ════════════════════════════════════════════════════════════════════════════════════════ - - [Header("Settings")] - [SerializeField] private bool m_IsServer; - - [Header("Validation")] - [SerializeField] private int m_MaxPendingRequestsPerPlayer = 50; - [SerializeField] private float m_RequestTimeout = 5f; - - [Header("Debug")] - [SerializeField] private bool m_LogNetworkMessages = false; - - // ════════════════════════════════════════════════════════════════════════════════════════ - // PRIVATE FIELDS - // ════════════════════════════════════════════════════════════════════════════════════════ - - private readonly Dictionary m_Controllers = new(32); - private readonly Dictionary m_PendingRequestCounts = new(32); - private NetworkInventoryPatchHooks m_PatchHooks; - - // Merchant controllers (separate from player bags) - private readonly Dictionary m_MerchantControllers = new(8); - - // ════════════════════════════════════════════════════════════════════════════════════════ - // PROPERTIES - // ════════════════════════════════════════════════════════════════════════════════════════ - - public bool IsServer - { - get => m_IsServer; - set - { - m_IsServer = value; - SecurityIntegration.SetModuleServerContext("Inventory", m_IsServer); - SecurityIntegration.EnsureSecurityManagerInitialized(m_IsServer, ResolveSecurityTimeProvider); - SyncPatchHooks(); - if (m_IsServer) RefreshOwnedEntityMappings(); - } - } - - public int ControllerCount => m_Controllers.Count; - - public float RequestTimeoutSeconds => m_RequestTimeout; - - // ════════════════════════════════════════════════════════════════════════════════════════ - // UNITY LIFECYCLE - // ════════════════════════════════════════════════════════════════════════════════════════ - private void OnEnable() - { - SecurityIntegration.SetModuleServerContext("Inventory", m_IsServer); - SecurityIntegration.EnsureSecurityManagerInitialized(m_IsServer, ResolveSecurityTimeProvider); - SyncPatchHooks(); - } - - private void OnDisable() - { - SecurityIntegration.SetModuleServerContext("Inventory", false); - if (m_PatchHooks != null) - { - m_PatchHooks.Initialize(false); - } - } - - - // ════════════════════════════════════════════════════════════════════════════════════════ - // REGISTRATION - // ════════════════════════════════════════════════════════════════════════════════════════ - - public void RegisterController(uint networkId, NetworkInventoryController controller) - { - if (controller == null) return; - m_Controllers[networkId] = controller; - RegisterOwnedEntityMapping(networkId); - - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Registered inventory controller: NetworkId={networkId}"); - } - - public void UnregisterController(uint networkId) - { - bool removed = m_Controllers.Remove(networkId); - if (removed) - { - SecurityIntegration.UnregisterEntity(networkId); - } - - if (removed && m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Unregistered inventory controller: NetworkId={networkId}"); - } - - public NetworkInventoryController GetController(uint networkId) - { - return m_Controllers.TryGetValue(networkId, out var controller) ? controller : null; - } - - private NetworkInventoryController GetControllerOrFallback(uint networkId, string operation) - { - NetworkInventoryController controller = GetController(networkId); - if (controller != null) return controller; - - foreach (var entry in m_Controllers) - { - if (entry.Value == null) continue; - - Debug.LogWarning( - $"[NetworkInventoryPickupDebug][Manager] {operation} using fallback controller because bag={networkId} is not registered locally. fallbackBag={entry.Key}"); - return entry.Value; - } - - Debug.LogWarning( - $"[NetworkInventoryPickupDebug][Manager] {operation} ignored because bag={networkId} is not registered locally and no fallback controller exists"); - return null; - } - - public void RegisterMerchantController(uint networkId, NetworkMerchantController controller) - { - if (controller == null) return; - m_MerchantControllers[networkId] = controller; - } - - public void UnregisterMerchantController(uint networkId) - { - m_MerchantControllers.Remove(networkId); - } - - public NetworkMerchantController GetMerchantController(uint networkId) - { - return m_MerchantControllers.TryGetValue(networkId, out var controller) ? controller : null; - } - - // ════════════════════════════════════════════════════════════════════════════════════════ - // CLIENT → SERVER: SENDING REQUESTS - // ════════════════════════════════════════════════════════════════════════════════════════ - - #region Send Requests - - public void SendContentAddRequest(NetworkContentAddRequest request) - { - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Sending add request: RequestId={request.RequestId}"); - OnSendContentAddRequest?.Invoke(request); - } - - public void SendContentRemoveRequest(NetworkContentRemoveRequest request) - { - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Sending remove request: RequestId={request.RequestId}"); - OnSendContentRemoveRequest?.Invoke(request); - } - - public void SendContentMoveRequest(NetworkContentMoveRequest request) - { - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Sending move request: RequestId={request.RequestId}"); - OnSendContentMoveRequest?.Invoke(request); - } - - public void SendContentUseRequest(NetworkContentUseRequest request) - { - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Sending use request: RequestId={request.RequestId}"); - OnSendContentUseRequest?.Invoke(request); - } - - public void SendContentDropRequest(NetworkContentDropRequest request) - { - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Sending drop request: RequestId={request.RequestId}"); - OnSendContentDropRequest?.Invoke(request); - } - - public void SendEquipmentRequest(NetworkEquipmentRequest request) - { - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Sending equipment request: RequestId={request.RequestId}, Action={request.Action}"); - OnSendEquipmentRequest?.Invoke(request); - } - - public void SendSocketRequest(NetworkSocketRequest request) - { - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Sending socket request: RequestId={request.RequestId}, Action={request.Action}"); - OnSendSocketRequest?.Invoke(request); - } - - public void SendWealthRequest(NetworkWealthRequest request) - { - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Sending wealth request: RequestId={request.RequestId}, Action={request.Action}"); - OnSendWealthRequest?.Invoke(request); - } - - public void SendMerchantRequest(NetworkMerchantRequest request) - { - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Sending merchant request: RequestId={request.RequestId}, Action={request.Action}"); - OnSendMerchantRequest?.Invoke(request); - } - - public void SendCraftingRequest(NetworkCraftingRequest request) - { - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Sending crafting request: RequestId={request.RequestId}, Action={request.Action}"); - OnSendCraftingRequest?.Invoke(request); - } - - public void SendTransferRequest(NetworkTransferRequest request) - { - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Sending transfer request: RequestId={request.RequestId}"); - OnSendTransferRequest?.Invoke(request); - } - - public void SendPickupRequest(NetworkPickupRequest request) - { - Debug.Log( - $"[NetworkInventoryPickupDebug][Manager] send pickup request req={request.RequestId} actor={request.ActorNetworkId} pickerBag={request.PickerBagNetworkId} sourceBag={request.SourceBagNetworkId} runtime={request.RuntimeIdHash} destination={request.DestinationPosition}"); - OnSendPickupRequest?.Invoke(request); - } - - // [LOCAL-EDIT] #PILFER-INVENTORY-SERVER-LOOT - public void SendLootRequest(NetworkLootRequest request) - { - Debug.Log( - $"[NetworkInventoryLootDebug][Manager] send loot request req={request.RequestId} actor={request.ActorNetworkId} container={request.ContainerBagNetworkId}"); - OnSendLootRequest?.Invoke(request); - } - - public void SendCombineRequest(NetworkCombineRequest request) - { - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Sending combine request: RequestId={request.RequestId}"); - OnSendCombineRequest?.Invoke(request); - } - - #endregion - - // ════════════════════════════════════════════════════════════════════════════════════════ - // SERVER: RECEIVING REQUESTS - // ════════════════════════════════════════════════════════════════════════════════════════ - - #region Receive Requests (Server) - - private static uint GetSenderClientId(ulong clientId) - { - return NetworkTransportBridge.TryConvertSenderClientId(clientId, out uint senderClientId) - ? senderClientId - : NetworkTransportBridge.InvalidClientId; - } - - private static NetworkRequestContext BuildContext(uint actorNetworkId, uint correlationId) - { - return NetworkRequestContext.Create(actorNetworkId, correlationId); - } - - private static InventoryRejectionReason GetSecurityRejection(uint actorNetworkId, uint correlationId) - { - return SecurityIntegration.IsProtocolContextMismatch(actorNetworkId, correlationId) - ? InventoryRejectionReason.ProtocolMismatch - : InventoryRejectionReason.SecurityViolation; - } - - private void RegisterOwnedEntityMapping(uint entityNetworkId) - { - if (!m_IsServer || entityNetworkId == 0) return; - - SecurityIntegration.RegisterEntityActor(entityNetworkId, entityNetworkId); - - var bridge = NetworkTransportBridge.Active; - if (bridge != null && - bridge.TryGetCharacterOwner(entityNetworkId, out uint ownerClientId) && - NetworkTransportBridge.IsValidClientId(ownerClientId)) - { - SecurityIntegration.RegisterEntityOwner(entityNetworkId, ownerClientId); - } - } - - private void RefreshOwnedEntityMappings() - { - foreach (var kvp in m_Controllers) - { - RegisterOwnedEntityMapping(kvp.Key); - } - } - - private bool ValidateTargetOwnership(uint senderClientId, uint actorNetworkId, uint targetBagNetworkId, string requestType) - { - NetworkInventoryController targetController = GetController(targetBagNetworkId); - if (targetController != null && targetController.IsWorldInventory) - { - return true; - } - - return SecurityIntegration.ValidateTargetEntityOwnership( - senderClientId, - actorNetworkId, - targetBagNetworkId, - "Inventory", - requestType); - } - - private static float ResolveSecurityTimeProvider() - { - var bridge = NetworkTransportBridge.Active; - return bridge != null && bridge.IsServer ? bridge.ServerTime : Time.time; - } - - private void SyncPatchHooks() - { - if (!m_IsServer) - { - if (m_PatchHooks != null) m_PatchHooks.Initialize(false); - return; - } - - if (m_PatchHooks == null) - { - m_PatchHooks = GetComponent(); - if (m_PatchHooks == null) - { - m_PatchHooks = gameObject.AddComponent(); - } - } - - m_PatchHooks.Initialize(true); - } - - public void ReceiveContentAddRequest(NetworkContentAddRequest request, ulong clientId) - { - if (!m_IsServer) return; - uint senderClientId = GetSenderClientId(clientId); - if (!SecurityIntegration.ValidateModuleRequest( - senderClientId, - BuildContext(request.ActorNetworkId, request.CorrelationId), - "Inventory", - nameof(NetworkContentAddRequest))) - { - SendContentAddResponse(senderClientId, new NetworkContentAddResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = GetSecurityRejection(request.ActorNetworkId, request.CorrelationId) - }); - return; - } - if (!ValidateTargetOwnership(senderClientId, request.ActorNetworkId, request.TargetBagNetworkId, nameof(NetworkContentAddRequest))) - { - SendContentAddResponse(senderClientId, new NetworkContentAddResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.SecurityViolation - }); - return; - } - if (!CheckRateLimit(clientId)) - { - SendContentAddResponse(senderClientId, new NetworkContentAddResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.RateLimitExceeded - }); - return; - } - - try - { - var controller = GetController(request.TargetBagNetworkId); - if (controller == null) - { - SendContentAddResponse(senderClientId, new NetworkContentAddResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.BagNotFound - }); - return; - } - - var response = controller.ProcessContentAddRequest(request, senderClientId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - SendContentAddResponse(senderClientId, response); - } - finally - { - DecrementPendingRequests(clientId); - } - } - - public void ReceiveContentRemoveRequest(NetworkContentRemoveRequest request, ulong clientId) - { - if (!m_IsServer) return; - uint senderClientId = GetSenderClientId(clientId); - if (!SecurityIntegration.ValidateModuleRequest( - senderClientId, - BuildContext(request.ActorNetworkId, request.CorrelationId), - "Inventory", - nameof(NetworkContentRemoveRequest))) - { - SendContentRemoveResponse(senderClientId, new NetworkContentRemoveResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = GetSecurityRejection(request.ActorNetworkId, request.CorrelationId) - }); - return; - } - if (!ValidateTargetOwnership(senderClientId, request.ActorNetworkId, request.TargetBagNetworkId, nameof(NetworkContentRemoveRequest))) - { - SendContentRemoveResponse(senderClientId, new NetworkContentRemoveResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.SecurityViolation - }); - return; - } - if (!CheckRateLimit(clientId)) - { - SendContentRemoveResponse(senderClientId, new NetworkContentRemoveResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.RateLimitExceeded - }); - return; - } - - try - { - var controller = GetController(request.TargetBagNetworkId); - if (controller == null) - { - SendContentRemoveResponse(senderClientId, new NetworkContentRemoveResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.BagNotFound - }); - return; - } - - var response = controller.ProcessContentRemoveRequest(request, senderClientId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - SendContentRemoveResponse(senderClientId, response); - } - finally - { - DecrementPendingRequests(clientId); - } - } - - public void ReceiveContentMoveRequest(NetworkContentMoveRequest request, ulong clientId) - { - if (!m_IsServer) return; - uint senderClientId = GetSenderClientId(clientId); - if (!SecurityIntegration.ValidateModuleRequest( - senderClientId, - BuildContext(request.ActorNetworkId, request.CorrelationId), - "Inventory", - nameof(NetworkContentMoveRequest))) - { - SendContentMoveResponse(senderClientId, new NetworkContentMoveResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = GetSecurityRejection(request.ActorNetworkId, request.CorrelationId) - }); - return; - } - if (!ValidateTargetOwnership(senderClientId, request.ActorNetworkId, request.TargetBagNetworkId, nameof(NetworkContentMoveRequest))) - { - SendContentMoveResponse(senderClientId, new NetworkContentMoveResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.SecurityViolation - }); - return; - } - if (!CheckRateLimit(clientId)) - { - SendContentMoveResponse(senderClientId, new NetworkContentMoveResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.RateLimitExceeded - }); - return; - } - - try - { - var controller = GetController(request.TargetBagNetworkId); - if (controller == null) - { - SendContentMoveResponse(senderClientId, new NetworkContentMoveResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.BagNotFound - }); - return; - } - - var response = controller.ProcessContentMoveRequest(request, senderClientId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - SendContentMoveResponse(senderClientId, response); - } - finally - { - DecrementPendingRequests(clientId); - } - } - - public void ReceiveContentUseRequest(NetworkContentUseRequest request, ulong clientId) - { - if (!m_IsServer) return; - uint senderClientId = GetSenderClientId(clientId); - if (!SecurityIntegration.ValidateModuleRequest( - senderClientId, - BuildContext(request.ActorNetworkId, request.CorrelationId), - "Inventory", - nameof(NetworkContentUseRequest))) - { - SendContentUseResponse(senderClientId, new NetworkContentUseResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = GetSecurityRejection(request.ActorNetworkId, request.CorrelationId) - }); - return; - } - if (!ValidateTargetOwnership(senderClientId, request.ActorNetworkId, request.TargetBagNetworkId, nameof(NetworkContentUseRequest))) - { - SendContentUseResponse(senderClientId, new NetworkContentUseResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.SecurityViolation - }); - return; - } - if (!CheckRateLimit(clientId)) - { - SendContentUseResponse(senderClientId, new NetworkContentUseResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.RateLimitExceeded - }); - return; - } - - try - { - var controller = GetController(request.TargetBagNetworkId); - if (controller == null) - { - SendContentUseResponse(senderClientId, new NetworkContentUseResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.BagNotFound - }); - return; - } - - var response = controller.ProcessContentUseRequest(request, senderClientId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - SendContentUseResponse(senderClientId, response); - } - finally - { - DecrementPendingRequests(clientId); - } - } - - public void ReceiveContentDropRequest(NetworkContentDropRequest request, ulong clientId) - { - if (!m_IsServer) return; - uint senderClientId = GetSenderClientId(clientId); - if (!SecurityIntegration.ValidateModuleRequest( - senderClientId, - BuildContext(request.ActorNetworkId, request.CorrelationId), - "Inventory", - nameof(NetworkContentDropRequest))) - { - SendContentDropResponse(senderClientId, new NetworkContentDropResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = GetSecurityRejection(request.ActorNetworkId, request.CorrelationId) - }); - return; - } - if (!ValidateTargetOwnership(senderClientId, request.ActorNetworkId, request.TargetBagNetworkId, nameof(NetworkContentDropRequest))) - { - SendContentDropResponse(senderClientId, new NetworkContentDropResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.SecurityViolation - }); - return; - } - if (!CheckRateLimit(clientId)) - { - SendContentDropResponse(senderClientId, new NetworkContentDropResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.RateLimitExceeded - }); - return; - } - - try - { - var controller = GetController(request.TargetBagNetworkId); - if (controller == null) - { - SendContentDropResponse(senderClientId, new NetworkContentDropResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.BagNotFound - }); - return; - } - - var response = controller.ProcessContentDropRequest(request, senderClientId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - SendContentDropResponse(senderClientId, response); - } - finally - { - DecrementPendingRequests(clientId); - } - } - - public async Task ReceiveEquipmentRequest(NetworkEquipmentRequest request, ulong clientId) - { - if (!m_IsServer) return; - uint senderClientId = GetSenderClientId(clientId); - if (!SecurityIntegration.ValidateModuleRequest( - senderClientId, - BuildContext(request.ActorNetworkId, request.CorrelationId), - "Inventory", - nameof(NetworkEquipmentRequest))) - { - SendEquipmentResponse(senderClientId, new NetworkEquipmentResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = GetSecurityRejection(request.ActorNetworkId, request.CorrelationId) - }); - return; - } - if (!ValidateTargetOwnership(senderClientId, request.ActorNetworkId, request.TargetBagNetworkId, nameof(NetworkEquipmentRequest))) - { - SendEquipmentResponse(senderClientId, new NetworkEquipmentResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.SecurityViolation - }); - return; - } - if (!CheckRateLimit(clientId)) - { - SendEquipmentResponse(senderClientId, new NetworkEquipmentResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.RateLimitExceeded - }); - return; - } - - try - { - var controller = GetController(request.TargetBagNetworkId); - if (controller == null) - { - SendEquipmentResponse(senderClientId, new NetworkEquipmentResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.BagNotFound - }); - return; - } - - try - { - var response = await controller.ProcessEquipmentRequest(request, senderClientId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - SendEquipmentResponse(senderClientId, response); - } - catch (Exception ex) - { - Debug.LogError($"[NetworkInventory] ReceiveEquipmentRequest failed: {ex.Message}\n{ex.StackTrace}"); - SendEquipmentResponse(senderClientId, new NetworkEquipmentResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.InternalError - }); - } - } - finally - { - DecrementPendingRequests(clientId); - } - } - - public void ReceiveSocketRequest(NetworkSocketRequest request, ulong clientId) - { - if (!m_IsServer) return; - uint senderClientId = GetSenderClientId(clientId); - if (!SecurityIntegration.ValidateModuleRequest( - senderClientId, - BuildContext(request.ActorNetworkId, request.CorrelationId), - "Inventory", - nameof(NetworkSocketRequest))) - { - SendSocketResponse(senderClientId, new NetworkSocketResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = GetSecurityRejection(request.ActorNetworkId, request.CorrelationId) - }); - return; - } - if (!ValidateTargetOwnership(senderClientId, request.ActorNetworkId, request.TargetBagNetworkId, nameof(NetworkSocketRequest))) - { - SendSocketResponse(senderClientId, new NetworkSocketResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.SecurityViolation - }); - return; - } - if (!CheckRateLimit(clientId)) - { - SendSocketResponse(senderClientId, new NetworkSocketResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.RateLimitExceeded - }); - return; - } - - try - { - var controller = GetController(request.TargetBagNetworkId); - if (controller == null) - { - SendSocketResponse(senderClientId, new NetworkSocketResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.BagNotFound - }); - return; - } - - var response = controller.ProcessSocketRequest(request, senderClientId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - SendSocketResponse(senderClientId, response); - } - finally - { - DecrementPendingRequests(clientId); - } - } - - public void ReceiveWealthRequest(NetworkWealthRequest request, ulong clientId) - { - if (!m_IsServer) return; - uint senderClientId = GetSenderClientId(clientId); - if (!SecurityIntegration.ValidateModuleRequest( - senderClientId, - BuildContext(request.ActorNetworkId, request.CorrelationId), - "Inventory", - nameof(NetworkWealthRequest))) - { - SendWealthResponse(senderClientId, new NetworkWealthResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = GetSecurityRejection(request.ActorNetworkId, request.CorrelationId) - }); - return; - } - if (!ValidateTargetOwnership(senderClientId, request.ActorNetworkId, request.TargetBagNetworkId, nameof(NetworkWealthRequest))) - { - SendWealthResponse(senderClientId, new NetworkWealthResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.SecurityViolation - }); - return; - } - if (!CheckRateLimit(clientId)) - { - SendWealthResponse(senderClientId, new NetworkWealthResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.RateLimitExceeded - }); - return; - } - - try - { - var controller = GetController(request.TargetBagNetworkId); - if (controller == null) - { - SendWealthResponse(senderClientId, new NetworkWealthResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.BagNotFound - }); - return; - } - - var response = controller.ProcessWealthRequest(request, senderClientId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - SendWealthResponse(senderClientId, response); - } - finally - { - DecrementPendingRequests(clientId); - } - } - - public void ReceiveTransferRequest(NetworkTransferRequest request, ulong clientId) - { - if (!m_IsServer) return; - uint senderClientId = GetSenderClientId(clientId); - NetworkInventoryController sourceController = GetController(request.SourceBagNetworkId); - NetworkInventoryController destinationController = GetController(request.DestinationBagNetworkId); - Debug.Log( - $"[NetworkInventoryPickupDebug][Manager] receive transfer request req={request.RequestId} senderConnection={clientId} senderClient={senderClientId} actor={request.ActorNetworkId} sourceBag={request.SourceBagNetworkId} sourceFound={sourceController != null} sourceWorld={(sourceController != null && sourceController.IsWorldInventory)} destinationBag={request.DestinationBagNetworkId} destinationFound={destinationController != null} destinationWorld={(destinationController != null && destinationController.IsWorldInventory)} runtime={request.RuntimeIdHash} destination={request.DestinationPosition}"); - - if (!SecurityIntegration.ValidateModuleRequest( - senderClientId, - BuildContext(request.ActorNetworkId, request.CorrelationId), - "Inventory", - nameof(NetworkTransferRequest))) - { - SendTransferResponse(senderClientId, new NetworkTransferResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = GetSecurityRejection(request.ActorNetworkId, request.CorrelationId) - }); - return; - } - - bool sourceAuthorized = ValidateTargetOwnership( - senderClientId, - request.ActorNetworkId, - request.SourceBagNetworkId, - nameof(NetworkTransferRequest)); - - bool destinationAuthorized = ValidateTargetOwnership( - senderClientId, - request.ActorNetworkId, - request.DestinationBagNetworkId, - nameof(NetworkTransferRequest)); - - if (!sourceAuthorized || !destinationAuthorized) - { - Debug.LogWarning( - $"[NetworkInventoryPickupDebug][Manager] transfer rejected by ownership req={request.RequestId} senderClient={senderClientId} actor={request.ActorNetworkId} sourceBag={request.SourceBagNetworkId} sourceAuthorized={sourceAuthorized} destinationBag={request.DestinationBagNetworkId} destinationAuthorized={destinationAuthorized}"); - - SendTransferResponse(senderClientId, new NetworkTransferResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.SecurityViolation - }); - return; - } - - // TODO: Support authorized world/container-to-world/container reorganization once container access locks and concurrent looting rules are defined. - if (sourceController != null && destinationController != null && - sourceController.IsWorldInventory && destinationController.IsWorldInventory) - { - Debug.LogWarning( - $"[NetworkInventoryPickupDebug][Manager] transfer rejected world-to-world deferred req={request.RequestId} sourceBag={request.SourceBagNetworkId} destinationBag={request.DestinationBagNetworkId}"); - - SendTransferResponse(senderClientId, new NetworkTransferResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.InvalidOperation - }); - return; - } - - if (!CheckRateLimit(clientId)) - { - SendTransferResponse(senderClientId, new NetworkTransferResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.RateLimitExceeded - }); - return; - } - - try - { - NetworkInventoryController source = GetController(request.SourceBagNetworkId); - NetworkInventoryController destination = GetController(request.DestinationBagNetworkId); - if (source == null || destination == null) - { - Debug.LogWarning( - $"[NetworkInventoryPickupDebug][Manager] transfer rejected bag not found req={request.RequestId} sourceBag={request.SourceBagNetworkId} sourceFound={source != null} destinationBag={request.DestinationBagNetworkId} destinationFound={destination != null}"); - - SendTransferResponse(senderClientId, new NetworkTransferResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.BagNotFound - }); - return; - } - - NetworkTransferResponse response = source.ProcessTransferRequest(request, destination, senderClientId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - SendTransferResponse(senderClientId, response); - } - finally - { - DecrementPendingRequests(clientId); - } - } - - public void ReceivePickupRequest(NetworkPickupRequest request, ulong clientId) - { - if (!m_IsServer) return; - uint senderClientId = GetSenderClientId(clientId); - Debug.Log( - $"[NetworkInventoryPickupDebug][Manager] receive pickup request req={request.RequestId} senderConnection={clientId} senderClient={senderClientId} actor={request.ActorNetworkId} pickerBag={request.PickerBagNetworkId} sourceBag={request.SourceBagNetworkId} runtime={request.RuntimeIdHash}"); - if (!SecurityIntegration.ValidateModuleRequest( - senderClientId, - BuildContext(request.ActorNetworkId, request.CorrelationId), - "Inventory", - nameof(NetworkPickupRequest))) - { - Debug.LogWarning( - $"[NetworkInventoryPickupDebug][Manager] pickup rejected by security req={request.RequestId} senderClient={senderClientId} actor={request.ActorNetworkId} reason={GetSecurityRejection(request.ActorNetworkId, request.CorrelationId)}"); - SendPickupResponse(senderClientId, new NetworkPickupResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = GetSecurityRejection(request.ActorNetworkId, request.CorrelationId) - }); - return; - } - - if (!ValidateTargetOwnership(senderClientId, request.ActorNetworkId, request.PickerBagNetworkId, nameof(NetworkPickupRequest))) - { - Debug.LogWarning( - $"[NetworkInventoryPickupDebug][Manager] pickup rejected by ownership req={request.RequestId} senderClient={senderClientId} actor={request.ActorNetworkId} pickerBag={request.PickerBagNetworkId}"); - SendPickupResponse(senderClientId, new NetworkPickupResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.SecurityViolation - }); - return; - } - - if (!CheckRateLimit(clientId)) - { - Debug.LogWarning( - $"[NetworkInventoryPickupDebug][Manager] pickup rejected by rate limit req={request.RequestId} senderConnection={clientId}"); - SendPickupResponse(senderClientId, new NetworkPickupResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.RateLimitExceeded - }); - return; - } - - try - { - NetworkInventoryController picker = GetController(request.PickerBagNetworkId); - if (picker == null) - { - Debug.LogWarning( - $"[NetworkInventoryPickupDebug][Manager] pickup rejected picker bag not found req={request.RequestId} pickerBag={request.PickerBagNetworkId}"); - SendPickupResponse(senderClientId, new NetworkPickupResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - Authorized = false, - RejectionReason = InventoryRejectionReason.BagNotFound - }); - return; - } - - NetworkPickupResponse response = picker.ProcessPickupRequest(request, senderClientId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - Debug.Log( - $"[NetworkInventoryPickupDebug][Manager] pickup processed req={request.RequestId} authorized={response.Authorized} reason={response.RejectionReason} pickupFailure={response.PickupFailure} prop={response.PropNetworkId} senderClient={senderClientId} placed={response.PlacedPosition}"); - SendPickupResponse(senderClientId, response); - } - finally - { - DecrementPendingRequests(clientId); - } - } - - // [LOCAL-EDIT] #PILFER-INVENTORY-SERVER-LOOT - public void ReceiveLootRequest(NetworkLootRequest request, ulong clientId) - { - if (!m_IsServer) return; - uint senderClientId = GetSenderClientId(clientId); - Debug.Log( - $"[NetworkInventoryLootDebug][Manager] receive loot request req={request.RequestId} senderConnection={clientId} senderClient={senderClientId} actor={request.ActorNetworkId} container={request.ContainerBagNetworkId}"); - - if (!SecurityIntegration.ValidateModuleRequest( - senderClientId, - BuildContext(request.ActorNetworkId, request.CorrelationId), - "Inventory", - nameof(NetworkLootRequest))) - { - SendLootResponse(senderClientId, new NetworkLootResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - ContainerBagNetworkId = request.ContainerBagNetworkId, - Authorized = false, - RejectionReason = GetSecurityRejection(request.ActorNetworkId, request.CorrelationId), - LootFailure = NetworkLootFailure.None - }); - return; - } - - if (!ValidateTargetOwnership(senderClientId, request.ActorNetworkId, request.ActorNetworkId, nameof(NetworkLootRequest))) - { - SendLootResponse(senderClientId, new NetworkLootResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - ContainerBagNetworkId = request.ContainerBagNetworkId, - Authorized = false, - RejectionReason = InventoryRejectionReason.SecurityViolation, - LootFailure = NetworkLootFailure.None - }); - return; - } - - if (!CheckRateLimit(clientId)) - { - SendLootResponse(senderClientId, new NetworkLootResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - ContainerBagNetworkId = request.ContainerBagNetworkId, - Authorized = false, - RejectionReason = InventoryRejectionReason.RateLimitExceeded, - LootFailure = NetworkLootFailure.None - }); - return; - } - - try - { - NetworkInventoryController container = GetController(request.ContainerBagNetworkId); - if (container == null) - { - SendLootResponse(senderClientId, new NetworkLootResponse - { - RequestId = request.RequestId, - ActorNetworkId = request.ActorNetworkId, - CorrelationId = request.CorrelationId, - ContainerBagNetworkId = request.ContainerBagNetworkId, - Authorized = false, - RejectionReason = InventoryRejectionReason.BagNotFound, - LootFailure = NetworkLootFailure.ContainerBagNotFound - }); - return; - } - - NetworkLootResponse response = container.ProcessLootRequest(request, senderClientId); - response.ActorNetworkId = request.ActorNetworkId; - response.CorrelationId = request.CorrelationId; - Debug.Log( - $"[NetworkInventoryLootDebug][Manager] loot processed req={request.RequestId} authorized={response.Authorized} generated={response.Generated} reason={response.RejectionReason} lootFailure={response.LootFailure} senderClient={senderClientId} container={response.ContainerBagNetworkId}"); - SendLootResponse(senderClientId, response); - } - finally - { - DecrementPendingRequests(clientId); - } - } - - #endregion - - // ════════════════════════════════════════════════════════════════════════════════════════ - // SERVER: SEND RESPONSES - // ════════════════════════════════════════════════════════════════════════════════════════ - - #region Send Responses (Server) - - private void SendContentAddResponse(uint targetNetworkId, NetworkContentAddResponse response) - { - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Sending add response: RequestId={response.RequestId}, Authorized={response.Authorized}"); - OnSendContentAddResponse?.Invoke(targetNetworkId, response); - } - - private void SendContentRemoveResponse(uint targetNetworkId, NetworkContentRemoveResponse response) - { - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Sending remove response: RequestId={response.RequestId}, Authorized={response.Authorized}"); - OnSendContentRemoveResponse?.Invoke(targetNetworkId, response); - } - - private void SendContentMoveResponse(uint targetNetworkId, NetworkContentMoveResponse response) - { - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Sending move response: RequestId={response.RequestId}, Authorized={response.Authorized}"); - OnSendContentMoveResponse?.Invoke(targetNetworkId, response); - } - - private void SendContentUseResponse(uint targetNetworkId, NetworkContentUseResponse response) - { - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Sending use response: RequestId={response.RequestId}, Authorized={response.Authorized}"); - OnSendContentUseResponse?.Invoke(targetNetworkId, response); - } - - private void SendContentDropResponse(uint targetNetworkId, NetworkContentDropResponse response) - { - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Sending drop response: RequestId={response.RequestId}, Authorized={response.Authorized}"); - OnSendContentDropResponse?.Invoke(targetNetworkId, response); - } - - private void SendEquipmentResponse(uint targetNetworkId, NetworkEquipmentResponse response) - { - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Sending equipment response: RequestId={response.RequestId}, Authorized={response.Authorized}"); - OnSendEquipmentResponse?.Invoke(targetNetworkId, response); - } - - private void SendSocketResponse(uint targetNetworkId, NetworkSocketResponse response) - { - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Sending socket response: RequestId={response.RequestId}, Authorized={response.Authorized}"); - OnSendSocketResponse?.Invoke(targetNetworkId, response); - } - - private void SendWealthResponse(uint targetNetworkId, NetworkWealthResponse response) - { - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Sending wealth response: RequestId={response.RequestId}, Authorized={response.Authorized}"); - OnSendWealthResponse?.Invoke(targetNetworkId, response); - } - - private void SendTransferResponse(uint targetNetworkId, NetworkTransferResponse response) - { - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Sending transfer response: RequestId={response.RequestId}, Authorized={response.Authorized}"); - OnSendTransferResponse?.Invoke(targetNetworkId, response); - } - - private void SendPickupResponse(uint targetNetworkId, NetworkPickupResponse response) - { - Debug.Log( - $"[NetworkInventoryPickupDebug][Manager] send pickup response req={response.RequestId} target={targetNetworkId} authorized={response.Authorized} reason={response.RejectionReason} pickupFailure={response.PickupFailure} prop={response.PropNetworkId} placed={response.PlacedPosition}"); - OnSendPickupResponse?.Invoke(targetNetworkId, response); - } - - // [LOCAL-EDIT] #PILFER-INVENTORY-SERVER-LOOT - private void SendLootResponse(uint targetNetworkId, NetworkLootResponse response) - { - Debug.Log( - $"[NetworkInventoryLootDebug][Manager] send loot response req={response.RequestId} target={targetNetworkId} authorized={response.Authorized} generated={response.Generated} reason={response.RejectionReason} lootFailure={response.LootFailure} container={response.ContainerBagNetworkId}"); - OnSendLootResponse?.Invoke(targetNetworkId, response); - } - - #endregion - - // ════════════════════════════════════════════════════════════════════════════════════════ - // SERVER: BROADCASTING - // ════════════════════════════════════════════════════════════════════════════════════════ - - #region Broadcasting (Server) - - public void BroadcastItemAdded(NetworkItemAddedBroadcast broadcast) - { - if (!m_IsServer) return; - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Broadcasting item added: BagId={broadcast.BagNetworkId}"); - OnBroadcastItemAdded?.Invoke(broadcast); - } - - public void BroadcastItemRemoved(NetworkItemRemovedBroadcast broadcast) - { - if (!m_IsServer) return; - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Broadcasting item removed: BagId={broadcast.BagNetworkId}"); - OnBroadcastItemRemoved?.Invoke(broadcast); - } - - public void BroadcastItemDropped(NetworkItemDroppedBroadcast broadcast) - { - if (!m_IsServer) return; - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Broadcasting item dropped: BagId={broadcast.SourceBagNetworkId}"); - OnBroadcastItemDropped?.Invoke(broadcast); - } - - public void BroadcastDroppedItemRemoved(NetworkDroppedItemRemovedBroadcast broadcast) - { - if (!m_IsServer) return; - Debug.Log( - $"[NetworkInventoryPickupDebug][Manager] broadcast dropped item removed sourceBag={broadcast.SourceBagNetworkId} runtime={broadcast.RuntimeIdHash} position={broadcast.Position}"); - OnBroadcastDroppedItemRemoved?.Invoke(broadcast); - } - - public void BroadcastItemMoved(NetworkItemMovedBroadcast broadcast) - { - if (!m_IsServer) return; - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Broadcasting item moved: BagId={broadcast.BagNetworkId}"); - OnBroadcastItemMoved?.Invoke(broadcast); - } - - public void BroadcastItemUsed(NetworkItemUsedBroadcast broadcast) - { - if (!m_IsServer) return; - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Broadcasting item used: BagId={broadcast.BagNetworkId}"); - OnBroadcastItemUsed?.Invoke(broadcast); - } - - public void BroadcastItemEquipped(NetworkItemEquippedBroadcast broadcast) - { - if (!m_IsServer) return; - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Broadcasting item equipped: BagId={broadcast.BagNetworkId}, Index={broadcast.EquipmentIndex}"); - OnBroadcastItemEquipped?.Invoke(broadcast); - } - - public void BroadcastItemUnequipped(NetworkItemUnequippedBroadcast broadcast) - { - if (!m_IsServer) return; - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Broadcasting item unequipped: BagId={broadcast.BagNetworkId}, Index={broadcast.EquipmentIndex}"); - OnBroadcastItemUnequipped?.Invoke(broadcast); - } - - public void BroadcastSocketChange(NetworkSocketChangeBroadcast broadcast) - { - if (!m_IsServer) return; - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Broadcasting socket change: BagId={broadcast.BagNetworkId}"); - OnBroadcastSocketChange?.Invoke(broadcast); - } - - public void BroadcastWealthChange(NetworkWealthChangeBroadcast broadcast) - { - if (!m_IsServer) return; - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Broadcasting wealth change: BagId={broadcast.BagNetworkId}, Change={broadcast.Change}"); - OnBroadcastWealthChange?.Invoke(broadcast); - } - - public void BroadcastPropertyChange(NetworkPropertyChangeBroadcast broadcast) - { - if (!m_IsServer) return; - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Broadcasting property change: BagId={broadcast.BagNetworkId}"); - OnBroadcastPropertyChange?.Invoke(broadcast); - } - - public void BroadcastFullSnapshot(NetworkInventorySnapshot snapshot) - { - if (!m_IsServer) return; - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Broadcasting full snapshot: BagId={snapshot.BagNetworkId}, Cells={snapshot.Cells?.Length ?? 0}"); - OnBroadcastFullSnapshot?.Invoke(snapshot); - } - - public void BroadcastDelta(NetworkInventoryDelta delta) - { - if (!m_IsServer) return; - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Broadcasting delta: BagId={delta.BagNetworkId}"); - OnBroadcastDelta?.Invoke(delta); - } - - public void SendSnapshotToClient(ulong clientId, NetworkInventorySnapshot snapshot) - { - if (!m_IsServer) return; - if (m_LogNetworkMessages) - Debug.Log($"[NetworkInventoryManager] Sending snapshot to client {clientId}: BagId={snapshot.BagNetworkId}"); - OnSendSnapshotToClient?.Invoke(clientId, snapshot); - } - - #endregion - - // ════════════════════════════════════════════════════════════════════════════════════════ - // CLIENT: RECEIVING BROADCASTS - // ════════════════════════════════════════════════════════════════════════════════════════ - - #region Receive Broadcasts (Client) - - public void ReceiveItemAddedBroadcast(NetworkItemAddedBroadcast broadcast) - { - var controller = GetController(broadcast.BagNetworkId); - controller?.ReceiveItemAddedBroadcast(broadcast); - } - - public void ReceiveItemRemovedBroadcast(NetworkItemRemovedBroadcast broadcast) - { - var controller = GetController(broadcast.BagNetworkId); - controller?.ReceiveItemRemovedBroadcast(broadcast); - } - - public void ReceiveItemDroppedBroadcast(NetworkItemDroppedBroadcast broadcast) - { - var controller = GetControllerOrFallback(broadcast.SourceBagNetworkId, "receive dropped item broadcast"); - controller?.ReceiveItemDroppedBroadcast(broadcast); - } - - public void ReceiveDroppedItemRemovedBroadcast(NetworkDroppedItemRemovedBroadcast broadcast) - { - var controller = GetControllerOrFallback(broadcast.SourceBagNetworkId, "receive dropped item removed broadcast"); - controller?.ReceiveDroppedItemRemovedBroadcast(broadcast); - } - - public void ReceiveItemMovedBroadcast(NetworkItemMovedBroadcast broadcast) - { - var controller = GetController(broadcast.BagNetworkId); - controller?.ReceiveItemMovedBroadcast(broadcast); - } - - public void ReceiveItemUsedBroadcast(NetworkItemUsedBroadcast broadcast) - { - var controller = GetController(broadcast.BagNetworkId); - controller?.ReceiveItemUsedBroadcast(broadcast); - } - - public void ReceiveItemEquippedBroadcast(NetworkItemEquippedBroadcast broadcast) - { - var controller = GetController(broadcast.BagNetworkId); - controller?.ReceiveItemEquippedBroadcast(broadcast); - } - - public void ReceiveItemUnequippedBroadcast(NetworkItemUnequippedBroadcast broadcast) - { - var controller = GetController(broadcast.BagNetworkId); - controller?.ReceiveItemUnequippedBroadcast(broadcast); - } - - public void ReceiveSocketChangeBroadcast(NetworkSocketChangeBroadcast broadcast) - { - var controller = GetController(broadcast.BagNetworkId); - controller?.ReceiveSocketChangeBroadcast(broadcast); - } - - public void ReceiveWealthChangeBroadcast(NetworkWealthChangeBroadcast broadcast) - { - var controller = GetController(broadcast.BagNetworkId); - controller?.ReceiveWealthChangeBroadcast(broadcast); - } - - public void ReceiveFullSnapshot(NetworkInventorySnapshot snapshot) - { - var controller = GetController(snapshot.BagNetworkId); - if (controller == null) - { - Debug.LogWarning( - $"[NetworkInventoryPickupDebug][Manager] full snapshot ignored because no controller is registered for bag={snapshot.BagNetworkId} registeredControllers={m_Controllers.Count}"); - return; - } - - controller.ReceiveFullSnapshot(snapshot); - } - - public void ReceiveDelta(NetworkInventoryDelta delta) - { - var controller = GetController(delta.BagNetworkId); - controller?.ReceiveDelta(delta); - } - - #endregion - - // ════════════════════════════════════════════════════════════════════════════════════════ - // CLIENT: RECEIVING RESPONSES - // ════════════════════════════════════════════════════════════════════════════════════════ - - #region Receive Responses (Client) - - public void ReceiveContentAddResponse(NetworkContentAddResponse response, uint targetNetworkId) - { - uint actorId = response.ActorNetworkId != 0 ? response.ActorNetworkId : targetNetworkId; - var controller = GetController(actorId); - controller?.ReceiveContentAddResponse(response); - } - - public void ReceiveContentRemoveResponse(NetworkContentRemoveResponse response, uint targetNetworkId) - { - uint actorId = response.ActorNetworkId != 0 ? response.ActorNetworkId : targetNetworkId; - var controller = GetController(actorId); - controller?.ReceiveContentRemoveResponse(response); - } - - public void ReceiveContentMoveResponse(NetworkContentMoveResponse response, uint targetNetworkId) - { - uint actorId = response.ActorNetworkId != 0 ? response.ActorNetworkId : targetNetworkId; - var controller = GetController(actorId); - controller?.ReceiveContentMoveResponse(response); - } - - public void ReceiveContentUseResponse(NetworkContentUseResponse response, uint targetNetworkId) - { - uint actorId = response.ActorNetworkId != 0 ? response.ActorNetworkId : targetNetworkId; - var controller = GetController(actorId); - controller?.ReceiveContentUseResponse(response); - } - - public void ReceiveContentDropResponse(NetworkContentDropResponse response, uint targetNetworkId) - { - uint actorId = response.ActorNetworkId != 0 ? response.ActorNetworkId : targetNetworkId; - var controller = GetController(actorId); - controller?.ReceiveContentDropResponse(response); - } - - public void ReceiveEquipmentResponse(NetworkEquipmentResponse response, uint targetNetworkId) - { - uint actorId = response.ActorNetworkId != 0 ? response.ActorNetworkId : targetNetworkId; - var controller = GetController(actorId); - controller?.ReceiveEquipmentResponse(response); - } - - public void ReceiveSocketResponse(NetworkSocketResponse response, uint targetNetworkId) - { - uint actorId = response.ActorNetworkId != 0 ? response.ActorNetworkId : targetNetworkId; - var controller = GetController(actorId); - controller?.ReceiveSocketResponse(response); - } - - public void ReceiveWealthResponse(NetworkWealthResponse response, uint targetNetworkId) - { - uint actorId = response.ActorNetworkId != 0 ? response.ActorNetworkId : targetNetworkId; - var controller = GetController(actorId); - controller?.ReceiveWealthResponse(response); - } - - public void ReceiveTransferResponse(NetworkTransferResponse response, uint targetNetworkId) - { - if (!response.Authorized && m_LogNetworkMessages) - { - Debug.LogWarning($"[NetworkInventoryManager] Transfer rejected: {response.RejectionReason}"); - } - } - - public void ReceivePickupResponse(NetworkPickupResponse response, uint targetNetworkId) - { - Debug.Log( - $"[NetworkInventoryPickupDebug][Manager] receive pickup response target={targetNetworkId} req={response.RequestId} authorized={response.Authorized} reason={response.RejectionReason} pickupFailure={response.PickupFailure} prop={response.PropNetworkId} placed={response.PlacedPosition}"); - - if (!response.Authorized) - { - // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT-REJECT-DIAGNOSTICS - Debug.LogWarning($"[NetworkInventoryManager] Pickup rejected: {response.RejectionReason} pickupFailure={response.PickupFailure} prop={response.PropNetworkId}"); - } - } - - // [LOCAL-EDIT] #PILFER-INVENTORY-SERVER-LOOT - public void ReceiveLootResponse(NetworkLootResponse response, uint targetNetworkId) - { - Debug.Log( - $"[NetworkInventoryLootDebug][Manager] receive loot response target={targetNetworkId} req={response.RequestId} authorized={response.Authorized} generated={response.Generated} reason={response.RejectionReason} lootFailure={response.LootFailure} container={response.ContainerBagNetworkId}"); - - if (!response.Authorized) - { - Debug.LogWarning($"[NetworkInventoryManager] Loot rejected: {response.RejectionReason} lootFailure={response.LootFailure} container={response.ContainerBagNetworkId}"); - } - } - - #endregion - - // ════════════════════════════════════════════════════════════════════════════════════════ - // CUSTOM VALIDATION EXTENSION POINTS - // ════════════════════════════════════════════════════════════════════════════════════════ - - /// Custom validator for add operations. - public Func CustomAddValidator; - - /// Custom validator for remove operations. - public Func CustomRemoveValidator; - - /// Custom validator for merchant operations. - public Func CustomMerchantValidator; - - /// Custom validator for crafting operations. - public Func CustomCraftingValidator; - - // ════════════════════════════════════════════════════════════════════════════════════════ - // HELPERS - // ════════════════════════════════════════════════════════════════════════════════════════ - - private bool CheckRateLimit(ulong clientId) - { - if (!m_PendingRequestCounts.TryGetValue(clientId, out int count)) - count = 0; - - if (count >= m_MaxPendingRequestsPerPlayer) - { - Debug.LogWarning($"[NetworkInventoryManager] Client {clientId} exceeded rate limit"); - return false; - } - - m_PendingRequestCounts[clientId] = count + 1; - return true; - } - - private void DecrementPendingRequests(ulong clientId) - { - if (m_PendingRequestCounts.TryGetValue(clientId, out int count)) - { - m_PendingRequestCounts[clientId] = Math.Max(0, count - 1); - } - } - - public IEnumerable GetRegisteredNetworkIds() => m_Controllers.Keys; - - public void SendInitialState(ulong clientId) - { - if (!m_IsServer) return; - foreach (var kvp in m_Controllers) - { - var snapshot = kvp.Value.GetFullSnapshot(); - SendSnapshotToClient(clientId, snapshot); - } - } - - public void ForceFullSync() - { - if (!m_IsServer) return; - foreach (var kvp in m_Controllers) - { - var snapshot = kvp.Value.GetFullSnapshot(); - BroadcastFullSnapshot(snapshot); - } - } - - public void ClearControllers() - { - m_Controllers.Clear(); - m_MerchantControllers.Clear(); - if (m_LogNetworkMessages) - Debug.Log("[NetworkInventoryManager] All controllers cleared"); - } - } - - /// - /// Placeholder for merchant-specific network controller. - /// - public class NetworkMerchantController : MonoBehaviour - { - // Would contain merchant-specific networking logic - // Similar to NetworkInventoryController but for merchant operations - } -} -#endif diff --git a/NetworkInventoryPatchHooks.cs b/NetworkInventoryPatchHooks.cs deleted file mode 100644 index f6a1257..0000000 --- a/NetworkInventoryPatchHooks.cs +++ /dev/null @@ -1,153 +0,0 @@ -#if GC2_INVENTORY -using System; -using System.Reflection; -using UnityEngine; -using GameCreator.Runtime.Common; -using GameCreator.Runtime.Inventory; - -namespace Arawn.GameCreator2.Networking.Inventory -{ - /// - /// Runtime installer for Inventory patch delegates. Enables patched-mode validation on server. - /// - public class NetworkInventoryPatchHooks : NetworkSingleton - { - private bool m_IsServer; - private bool m_Installed; - - public bool IsPatchActive => m_Installed && IsInventoryPatched(); - - public void Initialize(bool isServer) - { - m_IsServer = isServer; - if (m_IsServer) InstallHooks(); - else UninstallHooks(); - } - - protected override void OnSingletonCleanup() - { - UninstallHooks(); - } - - public static bool IsInventoryPatched() - { - return - HasPublicStaticField( - typeof(TBagContent), - "NetworkAddValidator", - typeof(Func)) && - HasPublicStaticField( - typeof(TBagContent), - "NetworkRemoveValidator", - typeof(Func)) && - HasPublicStaticField( - typeof(TBagContent), - "NetworkMoveValidator", - typeof(Func)) && - HasPublicStaticField( - typeof(TBagContent), - "NetworkDropValidator", - typeof(Func)) && - HasPublicStaticField( - typeof(TBagContent), - "NetworkUseValidator", - typeof(Func)) && - HasPublicStaticField( - typeof(BagWealth), - "NetworkAddValidator", - typeof(Func)) && - HasPublicStaticField( - typeof(BagWealth), - "NetworkSetValidator", - typeof(Func)) && - HasPublicStaticProperty(typeof(TBagContent), "IsNetworkingActive", typeof(bool)) && - HasPublicStaticProperty(typeof(BagWealth), "IsNetworkingActive", typeof(bool)) && - HasInstanceMethod(typeof(TBagContent), "UseDirect", typeof(RuntimeItem)) && - HasInstanceMethod(typeof(TBagContent), "DropDirect", typeof(RuntimeItem), typeof(Vector3)) && - HasInstanceMethod(typeof(BagWealth), "SetDirect", typeof(IdString), typeof(int)) && - HasInstanceMethod(typeof(BagWealth), "AddDirect", typeof(IdString), typeof(int)); - } - - private void InstallHooks() - { - if (m_Installed) return; - if (!IsInventoryPatched()) - { - Debug.LogWarning("[NetworkInventoryPatchHooks] Inventory runtime patch markers were not detected. Falling back to interception mode."); - return; - } - - SetStaticField(typeof(TBagContent), "NetworkAddValidator", new Func(ValidateAdd)); - SetStaticField(typeof(TBagContent), "NetworkRemoveValidator", new Func(ValidateRemove)); - SetStaticField(typeof(TBagContent), "NetworkMoveValidator", new Func(ValidateMove)); - SetStaticField(typeof(TBagContent), "NetworkDropValidator", new Func(ValidateDrop)); - SetStaticField(typeof(TBagContent), "NetworkUseValidator", new Func(ValidateUse)); - - SetStaticField(typeof(BagWealth), "NetworkAddValidator", new Func(ValidateWealthAdd)); - SetStaticField(typeof(BagWealth), "NetworkSetValidator", new Func(ValidateWealthSet)); - - m_Installed = true; - } - - private void UninstallHooks() - { - if (!m_Installed) return; - - SetStaticField(typeof(TBagContent), "NetworkAddValidator", null); - SetStaticField(typeof(TBagContent), "NetworkRemoveValidator", null); - SetStaticField(typeof(TBagContent), "NetworkMoveValidator", null); - SetStaticField(typeof(TBagContent), "NetworkDropValidator", null); - SetStaticField(typeof(TBagContent), "NetworkUseValidator", null); - - SetStaticField(typeof(BagWealth), "NetworkAddValidator", null); - SetStaticField(typeof(BagWealth), "NetworkSetValidator", null); - - m_Installed = false; - } - - private bool ValidateAdd(TBagContent _, RuntimeItem __, Vector2Int ___, bool ____) => m_IsServer; - private bool ValidateRemove(TBagContent _, RuntimeItem __) => m_IsServer; - private bool ValidateMove(TBagContent _, Vector2Int __, Vector2Int ___, bool ____) => m_IsServer; - private bool ValidateDrop(TBagContent _, RuntimeItem __, Vector3 ___) => m_IsServer; - private bool ValidateUse(TBagContent _, RuntimeItem __) => m_IsServer; - private bool ValidateWealthAdd(BagWealth _, IdString __, int ___) => m_IsServer; - private bool ValidateWealthSet(BagWealth _, IdString __, int ___) => m_IsServer; - - private static void SetStaticField(Type type, string fieldName, object value) - { - FieldInfo field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.Static); - if (field == null) - { - Debug.LogWarning($"[NetworkInventoryPatchHooks] Missing patched field {type.Name}.{fieldName}. GC2 update likely changed signatures."); - return; - } - - field.SetValue(null, value); - } - - private static bool HasPublicStaticField(Type type, string fieldName, Type expectedFieldType) - { - FieldInfo field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.Static); - return field != null && expectedFieldType.IsAssignableFrom(field.FieldType); - } - - private static bool HasPublicStaticProperty(Type type, string propertyName, Type expectedPropertyType) - { - PropertyInfo property = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Static); - return property != null && expectedPropertyType.IsAssignableFrom(property.PropertyType); - } - - private static bool HasInstanceMethod(Type type, string methodName, params Type[] parameterTypes) - { - MethodInfo method = type.GetMethod( - methodName, - BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, - null, - parameterTypes, - null); - - return method != null; - } - } -} -#endif diff --git a/NetworkInventoryTypes.cs b/NetworkInventoryTypes.cs deleted file mode 100644 index bbc46fa..0000000 --- a/NetworkInventoryTypes.cs +++ /dev/null @@ -1,917 +0,0 @@ -#if GC2_INVENTORY -using System; -using UnityEngine; - -namespace Arawn.GameCreator2.Networking.Inventory -{ - // ════════════════════════════════════════════════════════════════════════════════════════════ - // ENUMS - // ════════════════════════════════════════════════════════════════════════════════════════════ - - /// - /// Types of inventory content operations. - /// - public enum InventoryContentAction : byte - { - Add = 0, - AddAtPosition = 1, - Remove = 2, - RemoveAtPosition = 3, - Move = 4, - Use = 5, - Drop = 6, - Sort = 7 - } - - /// - /// Types of equipment operations. - /// - public enum EquipmentAction : byte - { - Equip = 0, - EquipToSlot = 1, - EquipToIndex = 2, - Unequip = 3, - UnequipFromIndex = 4 - } - - /// - /// Types of socket operations. - /// - public enum SocketAction : byte - { - Attach = 0, - AttachToSocket = 1, - Detach = 2, - DetachFromSocket = 3 - } - - /// - /// Types of wealth operations. - /// - public enum WealthAction : byte - { - Set = 0, - Add = 1, - Subtract = 2 - } - - /// - /// Types of merchant operations. - /// - public enum MerchantAction : byte - { - BuyFromMerchant = 0, - SellToMerchant = 1 - } - - /// - /// Types of crafting operations. - /// - public enum CraftingAction : byte - { - Craft = 0, - Dismantle = 1, - Combine = 2 - } - - /// - /// Reasons for inventory operation rejection. - /// - public enum InventoryRejectionReason : byte - { - None = 0, - NotAuthorized = 1, - BagNotFound = 2, - ItemNotFound = 3, - RuntimeItemNotFound = 4, - InsufficientSpace = 5, - InvalidPosition = 6, - CannotStack = 7, - ItemEquipped = 8, - CannotEquip = 9, - CannotUnequip = 10, - InsufficientFunds = 11, - MerchantNotFound = 12, - CannotBuy = 13, - CannotSell = 14, - InsufficientIngredients = 15, - CannotCraft = 16, - CannotDismantle = 17, - SocketNotFound = 18, - CannotAttach = 19, - CannotDetach = 20, - CooldownActive = 21, - CannotUse = 22, - CannotDrop = 23, - RateLimitExceeded = 24, - InvalidOperation = 25, - NotOwner = 26, - ProtocolMismatch = 27, - SecurityViolation = 28, - IdentityMismatch = 29, - InternalError = 30, - RequestTimeout = 31 - } - - // [LOCAL-EDIT] #INVENTORY-WORLD-OBJECT-REJECT-DIAGNOSTICS - public enum NetworkPickupFailure : byte - { - None = 0, - WorldObjectNotFound = 1, - WorldObjectPickupDisabled = 2, - WorldObjectItemMissing = 3, - WorldObjectConsumed = 4, - WorldObjectOutOfRange = 5, - WorldObjectRuntimeItemFailed = 6 - } - - // [LOCAL-EDIT] #INVENTORY-SERVER-LOOT - public enum NetworkLootFailure : byte - { - None = 0, - ContainerBagNotFound = 1, - ContainerIsNotWorldInventory = 2, - LootContainerMissing = 3, - LootTableMissing = 4, - AlreadyGenerated = 5, - LootRollFailed = 6 - } - - /// - /// Source of inventory modification (for auditing/validation). - /// - public enum InventoryModificationSource : byte - { - Direct = 0, - Pickup = 1, - Loot = 2, - Trade = 3, - Merchant = 4, - Craft = 5, - Quest = 6, - Ability = 7, - StatusEffect = 8, - Admin = 9 - } - - // ════════════════════════════════════════════════════════════════════════════════════════════ - // NETWORK RUNTIME ITEM REPRESENTATION - // ════════════════════════════════════════════════════════════════════════════════════════════ - - /// - /// Minimal network representation of a RuntimeProperty. - /// ~16 bytes - /// - [Serializable] - public struct NetworkRuntimeProperty - { - public int PropertyHash; // 4 bytes - IdString hash - public string PropertyIdString; // Variable - Deterministic property ID - public float Number; // 4 bytes - public string Text; // Variable (null for most properties) - - public static NetworkRuntimeProperty FromProperty(int hash, float number, string text) - { - return new NetworkRuntimeProperty - { - PropertyHash = hash, - Number = number, - Text = text - }; - } - } - - /// - /// Minimal network representation of a RuntimeSocket. - /// ~12 bytes without attachment - /// - [Serializable] - public struct NetworkRuntimeSocket - { - public int SocketHash; // 4 bytes - public string SocketIdString; // Variable - Deterministic socket ID - public bool HasAttachment; // 1 byte - public NetworkRuntimeItem Attachment; // Variable (null if no attachment) - } - - /// - /// Network representation of a RuntimeItem. - /// This is the core data structure for syncing items. - /// - [Serializable] - public struct NetworkRuntimeItem - { - public int ItemHash; // 4 bytes - Item.ID hash - public string ItemIdString; // Variable - Deterministic item ID string - public long RuntimeIdHash; // 8 bytes - RuntimeItem.RuntimeID hash (use long for uniqueness) - public string RuntimeIdString; // Variable - Full RuntimeID string for reconstruction - public NetworkRuntimeProperty[] Properties; // Variable - public NetworkRuntimeSocket[] Sockets; // Variable - - /// - /// Estimated serialization size in bytes. - /// - public int EstimatedSize - { - get - { - int size = 12; // Base fields - size += (ItemIdString?.Length ?? 0) * 2; - size += (RuntimeIdString?.Length ?? 0) * 2; - size += (Properties?.Length ?? 0) * 16; - size += (Sockets?.Length ?? 0) * 12; - return size; - } - } - } - - /// - /// Network representation of a Cell (inventory slot with stacked items). - /// - [Serializable] - public struct NetworkCell - { - public Vector2Int Position; // 8 bytes - public int ItemHash; // 4 bytes - Item type - public int StackCount; // 4 bytes - public NetworkRuntimeItem RootItem; // Variable - The root item of the stack - public long[] StackedRuntimeIds; // Variable - RuntimeIDs of stacked items - public string[] StackedRuntimeIdStrings; // Variable - RuntimeID strings of stacked items - } - - // ════════════════════════════════════════════════════════════════════════════════════════════ - // CONTENT REQUESTS / RESPONSES - // ════════════════════════════════════════════════════════════════════════════════════════════ - - /// - /// Request to add item to bag content. - /// ~40 bytes + item data - /// - [Serializable] - public struct NetworkContentAddRequest - { - public ushort RequestId; // 2 bytes - public uint ActorNetworkId; // 4 bytes - public uint CorrelationId; // 4 bytes - public uint TargetBagNetworkId; // 4 bytes - public int ItemHash; // 4 bytes - Item type to create, or 0 if providing RuntimeItem - public string ItemIdString; // Variable - Deterministic item ID string - public NetworkRuntimeItem RuntimeItem; // Variable - If adding existing runtime item - public Vector2Int Position; // 8 bytes - (-1,-1) for auto-placement - public bool AllowStack; // 1 byte - public InventoryModificationSource Source; // 1 byte - public int SourceHash; // 4 bytes - } - - /// - /// Response to content add request. - /// ~24 bytes - /// - [Serializable] - public struct NetworkContentAddResponse - { - public ushort RequestId; // 2 bytes - public uint ActorNetworkId; // 4 bytes - public uint CorrelationId; // 4 bytes - public bool Authorized; // 1 byte - public InventoryRejectionReason RejectionReason; // 1 byte - public Vector2Int ResultPosition; // 8 bytes - Where item was placed - public long AssignedRuntimeId; // 8 bytes - Server-assigned RuntimeID hash - public string AssignedRuntimeIdString; // Variable - } - - /// - /// Request to remove item from bag. - /// ~24 bytes - /// - [Serializable] - public struct NetworkContentRemoveRequest - { - public ushort RequestId; // 2 bytes - public uint ActorNetworkId; // 4 bytes - public uint CorrelationId; // 4 bytes - public uint TargetBagNetworkId; // 4 bytes - public long RuntimeIdHash; // 8 bytes - RuntimeItem to remove - public Vector2Int Position; // 8 bytes - Or position to remove from - public bool UsePosition; // 1 byte - Whether to use position instead of RuntimeID - public InventoryModificationSource Source; // 1 byte - } - - /// - /// Response to content remove request. - /// ~20 bytes - /// - [Serializable] - public struct NetworkContentRemoveResponse - { - public ushort RequestId; // 2 bytes - public uint ActorNetworkId; // 4 bytes - public uint CorrelationId; // 4 bytes - public bool Authorized; // 1 byte - public InventoryRejectionReason RejectionReason; // 1 byte - public NetworkRuntimeItem RemovedItem; // Variable - The item that was removed - } - - /// - /// Request to move item within bag. - /// ~24 bytes - /// - [Serializable] - public struct NetworkContentMoveRequest - { - public ushort RequestId; // 2 bytes - public uint ActorNetworkId; // 4 bytes - public uint CorrelationId; // 4 bytes - public uint TargetBagNetworkId; // 4 bytes - public Vector2Int FromPosition; // 8 bytes - public Vector2Int ToPosition; // 8 bytes - public bool AllowStack; // 1 byte - } - - /// - /// Response to content move request. - /// ~8 bytes - /// - [Serializable] - public struct NetworkContentMoveResponse - { - public ushort RequestId; // 2 bytes - public uint ActorNetworkId; // 4 bytes - public uint CorrelationId; // 4 bytes - public bool Authorized; // 1 byte - public InventoryRejectionReason RejectionReason; // 1 byte - public Vector2Int FinalPosition; // 8 bytes - } - - /// - /// Request to use an item. - /// ~20 bytes - /// - [Serializable] - public struct NetworkContentUseRequest - { - public ushort RequestId; // 2 bytes - public uint ActorNetworkId; // 4 bytes - public uint CorrelationId; // 4 bytes - public uint TargetBagNetworkId; // 4 bytes - public long RuntimeIdHash; // 8 bytes - public Vector2Int Position; // 8 bytes - Alternative to RuntimeID - public bool UsePosition; // 1 byte - } - - /// - /// Response to use request. - /// ~8 bytes - /// - [Serializable] - public struct NetworkContentUseResponse - { - public ushort RequestId; // 2 bytes - public uint ActorNetworkId; // 4 bytes - public uint CorrelationId; // 4 bytes - public bool Authorized; // 1 byte - public InventoryRejectionReason RejectionReason; // 1 byte - public bool WasConsumed; // 1 byte - } - - /// - /// Request to drop an item. - /// ~32 bytes - /// - [Serializable] - public struct NetworkContentDropRequest - { - public ushort RequestId; // 2 bytes - public uint ActorNetworkId; // 4 bytes - public uint CorrelationId; // 4 bytes - public uint TargetBagNetworkId; // 4 bytes - public long RuntimeIdHash; // 8 bytes - public Vector3 DropPosition; // 12 bytes - public int MaxAmount; // 4 bytes - For dropping from stack - } - - /// - /// Response to drop request. - /// ~8 bytes - /// - [Serializable] - public struct NetworkContentDropResponse - { - public ushort RequestId; // 2 bytes - public uint ActorNetworkId; // 4 bytes - public uint CorrelationId; // 4 bytes - public bool Authorized; // 1 byte - public InventoryRejectionReason RejectionReason; // 1 byte - public int DroppedCount; // 4 bytes - // Prop spawning handled separately via NetworkObject spawn - } - - // ════════════════════════════════════════════════════════════════════════════════════════════ - // EQUIPMENT REQUESTS / RESPONSES - // ════════════════════════════════════════════════════════════════════════════════════════════ - - /// - /// Request to equip/unequip item. - /// ~20 bytes - /// - [Serializable] - public struct NetworkEquipmentRequest - { - public ushort RequestId; // 2 bytes - public uint ActorNetworkId; // 4 bytes - public uint CorrelationId; // 4 bytes - public uint TargetBagNetworkId; // 4 bytes - public long RuntimeIdHash; // 8 bytes - public EquipmentAction Action; // 1 byte - public int SlotOrIndex; // 4 bytes - Slot number or equipment index - } - - /// - /// Response to equipment request. - /// ~8 bytes - /// - [Serializable] - public struct NetworkEquipmentResponse - { - public ushort RequestId; // 2 bytes - public uint ActorNetworkId; // 4 bytes - public uint CorrelationId; // 4 bytes - public bool Authorized; // 1 byte - public InventoryRejectionReason RejectionReason; // 1 byte - public int EquippedIndex; // 4 bytes - Final equipment index - } - - // ════════════════════════════════════════════════════════════════════════════════════════════ - // SOCKET REQUESTS / RESPONSES - // ════════════════════════════════════════════════════════════════════════════════════════════ - - /// - /// Request to attach/detach socket. - /// ~28 bytes - /// - [Serializable] - public struct NetworkSocketRequest - { - public ushort RequestId; // 2 bytes - public uint ActorNetworkId; // 4 bytes - public uint CorrelationId; // 4 bytes - public uint TargetBagNetworkId; // 4 bytes - public long ParentRuntimeIdHash; // 8 bytes - Parent item - public long AttachmentRuntimeIdHash; // 8 bytes - Attachment item (for attach) or socket contents (for detach) - public int SocketHash; // 4 bytes - Specific socket (0 for auto) - public string SocketIdString; // Variable - Deterministic socket ID string - public SocketAction Action; // 1 byte - } - - /// - /// Response to socket request. - /// ~16 bytes - /// - [Serializable] - public struct NetworkSocketResponse - { - public ushort RequestId; // 2 bytes - public uint ActorNetworkId; // 4 bytes - public uint CorrelationId; // 4 bytes - public bool Authorized; // 1 byte - public InventoryRejectionReason RejectionReason; // 1 byte - public int UsedSocketHash; // 4 bytes - Which socket was used - public NetworkRuntimeItem DetachedItem; // Variable - If detaching - } - - // ════════════════════════════════════════════════════════════════════════════════════════════ - // WEALTH REQUESTS / RESPONSES - // ════════════════════════════════════════════════════════════════════════════════════════════ - - /// - /// Request to modify wealth. - /// ~20 bytes - /// - [Serializable] - public struct NetworkWealthRequest - { - public ushort RequestId; // 2 bytes - public uint ActorNetworkId; // 4 bytes - public uint CorrelationId; // 4 bytes - public uint TargetBagNetworkId; // 4 bytes - public int CurrencyHash; // 4 bytes - public string CurrencyIdString; // Variable - Deterministic currency ID string - public int Value; // 4 bytes - public WealthAction Action; // 1 byte - public InventoryModificationSource Source; // 1 byte - public int SourceHash; // 4 bytes - } - - /// - /// Response to wealth request. - /// ~12 bytes - /// - [Serializable] - public struct NetworkWealthResponse - { - public ushort RequestId; // 2 bytes - public uint ActorNetworkId; // 4 bytes - public uint CorrelationId; // 4 bytes - public bool Authorized; // 1 byte - public InventoryRejectionReason RejectionReason; // 1 byte - public int NewValue; // 4 bytes - public int OldValue; // 4 bytes - } - - // ════════════════════════════════════════════════════════════════════════════════════════════ - // MERCHANT REQUESTS / RESPONSES - // ════════════════════════════════════════════════════════════════════════════════════════════ - - /// - /// Request to buy/sell from merchant. - /// ~24 bytes - /// - [Serializable] - public struct NetworkMerchantRequest - { - public ushort RequestId; // 2 bytes - public uint ActorNetworkId; // 4 bytes - public uint CorrelationId; // 4 bytes - public uint ClientBagNetworkId; // 4 bytes - public uint MerchantNetworkId; // 4 bytes - NetworkId of merchant's Bag - public long RuntimeIdHash; // 8 bytes - Item to buy/sell - public MerchantAction Action; // 1 byte - public int Amount; // 4 bytes - For stacked purchases - } - - /// - /// Response to merchant request. - /// ~16 bytes - /// - [Serializable] - public struct NetworkMerchantResponse - { - public ushort RequestId; // 2 bytes - public uint ActorNetworkId; // 4 bytes - public uint CorrelationId; // 4 bytes - public bool Authorized; // 1 byte - public InventoryRejectionReason RejectionReason; // 1 byte - public int TotalPrice; // 4 bytes - public int NewClientWealth; // 4 bytes - public int NewMerchantWealth; // 4 bytes - } - - // ════════════════════════════════════════════════════════════════════════════════════════════ - // CRAFTING REQUESTS / RESPONSES - // ════════════════════════════════════════════════════════════════════════════════════════════ - - /// - /// Request to craft/dismantle. - /// ~20 bytes - /// - [Serializable] - public struct NetworkCraftingRequest - { - public ushort RequestId; // 2 bytes - public uint ActorNetworkId; // 4 bytes - public uint CorrelationId; // 4 bytes - public uint InputBagNetworkId; // 4 bytes - public uint OutputBagNetworkId; // 4 bytes - public int ItemHash; // 4 bytes - Item to craft (for Craft action) - public string ItemIdString; // Variable - Deterministic crafted item ID string - public long RuntimeIdHash; // 8 bytes - RuntimeItem to dismantle (for Dismantle) - public CraftingAction Action; // 1 byte - } - - /// - /// Response to crafting request. - /// ~12 bytes + created item - /// - [Serializable] - public struct NetworkCraftingResponse - { - public ushort RequestId; // 2 bytes - public uint ActorNetworkId; // 4 bytes - public uint CorrelationId; // 4 bytes - public bool Authorized; // 1 byte - public InventoryRejectionReason RejectionReason; // 1 byte - public NetworkRuntimeItem CreatedItem; // Variable - The crafted item - public NetworkRuntimeItem[] ReturnedItems; // Variable - Dismantle returns - } - - // ════════════════════════════════════════════════════════════════════════════════════════════ - // BROADCASTS - // ════════════════════════════════════════════════════════════════════════════════════════════ - - /// - /// Broadcast when item is added to bag. - /// - [Serializable] - public struct NetworkItemAddedBroadcast - { - public uint BagNetworkId; - public NetworkRuntimeItem Item; - public Vector2Int Position; - public int StackCount; - } - - /// - /// Broadcast when item is removed from bag. - /// - [Serializable] - public struct NetworkItemRemovedBroadcast - { - public uint BagNetworkId; - public long RuntimeIdHash; - public Vector2Int Position; - public int RemainingStackCount; - } - - /// - /// Broadcast when an item is dropped into the world. - /// - [Serializable] - public struct NetworkItemDroppedBroadcast - { - public uint SourceBagNetworkId; - public NetworkRuntimeItem Item; - public Vector3 Position; - } - - /// - /// Broadcast when a previously dropped world item is picked up or otherwise removed. - /// - [Serializable] - public struct NetworkDroppedItemRemovedBroadcast - { - public uint SourceBagNetworkId; - public long RuntimeIdHash; - public Vector3 Position; - } - - /// - /// Broadcast when item is moved within bag. - /// - [Serializable] - public struct NetworkItemMovedBroadcast - { - public uint BagNetworkId; - public long RuntimeIdHash; - public Vector2Int FromPosition; - public Vector2Int ToPosition; - } - - /// - /// Broadcast when item is used. - /// - [Serializable] - public struct NetworkItemUsedBroadcast - { - public uint BagNetworkId; - public long RuntimeIdHash; - public bool WasConsumed; - } - - /// - /// Broadcast when item is equipped. - /// - [Serializable] - public struct NetworkItemEquippedBroadcast - { - public uint BagNetworkId; - public long RuntimeIdHash; - public int EquipmentIndex; - } - - /// - /// Broadcast when item is unequipped. - /// - [Serializable] - public struct NetworkItemUnequippedBroadcast - { - public uint BagNetworkId; - public long RuntimeIdHash; - public int EquipmentIndex; - } - - /// - /// Broadcast when socket attachment changes. - /// - [Serializable] - public struct NetworkSocketChangeBroadcast - { - public uint BagNetworkId; - public long ParentRuntimeIdHash; - public int SocketHash; - public bool HasAttachment; - public NetworkRuntimeItem Attachment; // If attached - } - - /// - /// Broadcast when wealth changes. - /// - [Serializable] - public struct NetworkWealthChangeBroadcast - { - public uint BagNetworkId; - public int CurrencyHash; - public int NewValue; - public int Change; - } - - /// - /// Broadcast when property value changes. - /// - [Serializable] - public struct NetworkPropertyChangeBroadcast - { - public uint BagNetworkId; - public long RuntimeIdHash; - public int PropertyHash; - public float NewNumber; - public string NewText; - } - - // ════════════════════════════════════════════════════════════════════════════════════════════ - // FULL STATE SYNC - // ════════════════════════════════════════════════════════════════════════════════════════════ - - /// - /// Full inventory snapshot for initial sync or reconnection. - /// - [Serializable] - public struct NetworkInventorySnapshot - { - public uint BagNetworkId; - public float Timestamp; - public int BagType; // Grid vs List - public Vector2Int BagSize; // For grid bags - public int MaxWeight; - public NetworkCell[] Cells; // All occupied cells - public NetworkEquipmentSlot[] Equipment; // All equipment slots - public NetworkWealthEntry[] Wealth; // All currencies - } - - /// - /// Equipment slot state for snapshot. - /// - [Serializable] - public struct NetworkEquipmentSlot - { - public int SlotIndex; - public int BaseItemHash; // What type of item can go here - public bool IsOccupied; - public long EquippedRuntimeIdHash; - } - - /// - /// Wealth entry for snapshot. - /// - [Serializable] - public struct NetworkWealthEntry - { - public int CurrencyHash; - public int Amount; - } - - /// - /// Delta update for efficient sync. - /// - [Serializable] - public struct NetworkInventoryDelta - { - public uint BagNetworkId; - public float Timestamp; - public uint ChangeMask; // Bit flags for what changed - public NetworkCell[] ChangedCells; - public NetworkEquipmentSlot[] ChangedEquipment; - public NetworkWealthEntry[] ChangedWealth; - } - - // ════════════════════════════════════════════════════════════════════════════════════════════ - // TRANSFER BETWEEN BAGS - // ════════════════════════════════════════════════════════════════════════════════════════════ - - /// - /// Request to transfer item between two bags (trade, loot, etc.). - /// - [Serializable] - public struct NetworkTransferRequest - { - public ushort RequestId; - public uint ActorNetworkId; - public uint CorrelationId; - public uint SourceBagNetworkId; - public uint DestinationBagNetworkId; - public long RuntimeIdHash; - public Vector2Int DestinationPosition; // (-1,-1) for auto - public bool AllowStack; - public InventoryModificationSource Source; - } - - /// - /// Response to transfer request. - /// - [Serializable] - public struct NetworkTransferResponse - { - public ushort RequestId; - public uint ActorNetworkId; - public uint CorrelationId; - public bool Authorized; - public InventoryRejectionReason RejectionReason; - public Vector2Int FinalPosition; - } - - // ════════════════════════════════════════════════════════════════════════════════════════════ - // LOOT / PICKUP - // ════════════════════════════════════════════════════════════════════════════════════════════ - - /// - /// Request to pick up a dropped Prop. - /// - [Serializable] - public struct NetworkPickupRequest - { - public ushort RequestId; - public uint ActorNetworkId; - public uint CorrelationId; - public uint PickerBagNetworkId; - public uint PropNetworkId; // NetworkId of the Prop object - public uint SourceBagNetworkId; // Bag that originally dropped/spawned the prop - public long RuntimeIdHash; // Runtime item represented by the dropped prop - public Vector2Int DestinationPosition; - } - - /// - /// Response to pickup request. - /// - [Serializable] - public struct NetworkPickupResponse - { - public ushort RequestId; - public uint ActorNetworkId; - public uint CorrelationId; - // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT-REJECT-DIAGNOSTICS - public uint PropNetworkId; - public NetworkPickupFailure PickupFailure; - public bool Authorized; - public InventoryRejectionReason RejectionReason; - public NetworkRuntimeItem PickedUpItem; - public Vector2Int PlacedPosition; - } - - // [LOCAL-EDIT] #INVENTORY-SERVER-LOOT - [Serializable] - public struct NetworkLootRequest - { - public ushort RequestId; - public uint ActorNetworkId; - public uint CorrelationId; - public uint ContainerBagNetworkId; - } - - // [LOCAL-EDIT] #INVENTORY-SERVER-LOOT - [Serializable] - public struct NetworkLootResponse - { - public ushort RequestId; - public uint ActorNetworkId; - public uint CorrelationId; - public uint ContainerBagNetworkId; - public bool Authorized; - public bool Generated; - public InventoryRejectionReason RejectionReason; - public NetworkLootFailure LootFailure; - } - - // ════════════════════════════════════════════════════════════════════════════════════════════ - // COMBINE (Two items into one) - // ════════════════════════════════════════════════════════════════════════════════════════════ - - /// - /// Request to combine two items (if crafting.AllowToCombine). - /// - [Serializable] - public struct NetworkCombineRequest - { - public ushort RequestId; - public uint ActorNetworkId; - public uint CorrelationId; - public uint BagNetworkId; - public Vector2Int PositionA; - public Vector2Int PositionB; - } - - /// - /// Response to combine request. - /// - [Serializable] - public struct NetworkCombineResponse - { - public ushort RequestId; - public uint ActorNetworkId; - public uint CorrelationId; - public bool Authorized; - public InventoryRejectionReason RejectionReason; - public NetworkRuntimeItem ResultItem; - public Vector2Int ResultPosition; - } -} -#endif diff --git a/NetworkLootContainer.cs b/NetworkLootContainer.cs deleted file mode 100644 index 9ced1e3..0000000 --- a/NetworkLootContainer.cs +++ /dev/null @@ -1,41 +0,0 @@ -#if GC2_INVENTORY -using GameCreator.Runtime.Inventory; -using UnityEngine; - -namespace Arawn.GameCreator2.Networking.Inventory -{ - // [LOCAL-EDIT] #INVENTORY-SERVER-LOOT - // Adds server-authoritative GC2 loot-container generation on top of Arawn's inventory networking layer. - // Must be attached to any GameObject that will send requests for Loot Table generation. - // Works alongside InstructionNetworkLootRequest.cs - [AddComponentMenu("Game Creator/Network/Inventory/Network Loot Container")] - [DisallowMultipleComponent] - public sealed class NetworkLootContainer : MonoBehaviour - { - [Header("Loot")] - [SerializeField] private LootTable m_LootTable; - [SerializeField] private bool m_GenerateOnce = true; - - [Header("Debug")] - [SerializeField] private bool m_LogDiagnostics; - - private bool m_HasGenerated; - - public LootTable LootTable => m_LootTable; - public bool GenerateOnce => m_GenerateOnce; - public bool HasGenerated => m_HasGenerated; - public bool LogDiagnostics => m_LogDiagnostics; - - public bool CanGenerate() - { - if (m_LootTable == null) return false; - return !m_GenerateOnce || !m_HasGenerated; - } - - public void MarkGenerated() - { - m_HasGenerated = true; - } - } -} -#endif diff --git a/NetworkWorldObject.cs b/NetworkWorldObject.cs deleted file mode 100644 index fb4f303..0000000 --- a/NetworkWorldObject.cs +++ /dev/null @@ -1,229 +0,0 @@ -#if GC2_INVENTORY -using System.Collections.Generic; -using GameCreator.Runtime.Inventory; -using UnityEngine; - -namespace Arawn.GameCreator2.Networking.Inventory -{ - public enum NetworkWorldObjectKind - { - Generic = 0, - PickupItem = 1, - // For future expansion - Door = 2, - Lever = 3, - Trap = 4, - Portal = 5, - Shrine = 6 - } - - /// - /// Stable network identity for scene-authored world objects. Pickup behavior is the first - /// implemented use; future object interactions can reuse the same identity and registry. - /// - [AddComponentMenu("Game Creator/Network/Inventory/Network World Object")] - [DisallowMultipleComponent] - public sealed class NetworkWorldObject : MonoBehaviour - { - // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT - [Header("Network Id")] - [SerializeField] private bool m_UseAutomaticNetworkId = true; - [SerializeField] private uint m_ManualNetworkId; - [SerializeField] private string m_NetworkIdSalt = string.Empty; - - [Header("World Object")] - [SerializeField] private NetworkWorldObjectKind m_Kind = NetworkWorldObjectKind.PickupItem; - - [Header("Pickup")] - [SerializeField] private bool m_AllowPickup = true; - [SerializeField] private Item m_Item; - [SerializeField] private float m_PickupRadius = 2f; - [SerializeField] private bool m_DisableOnPickup = true; - [SerializeField] private bool m_DestroyOnPickup; - - [Header("Debug")] - [SerializeField] private bool m_LogDiagnostics; - - private uint m_CachedNetworkId; - private bool m_IsConsumed; - - public uint NetworkId => ResolveNetworkId(); - public NetworkWorldObjectKind Kind => m_Kind; - public bool AllowPickup => m_AllowPickup; - public Item Item => m_Item; - public float PickupRadius => Mathf.Max(0f, m_PickupRadius); - public bool IsConsumed => m_IsConsumed; - - private void OnEnable() - { - NetworkWorldObjectRegistry.Register(this); - } - - private void OnDisable() - { - NetworkWorldObjectRegistry.Unregister(this); - } - - public bool CanPickupFrom(Vector3 pickerPosition) - { - if (!m_AllowPickup || m_IsConsumed || m_Item == null) return false; - if (m_Kind != NetworkWorldObjectKind.PickupItem) return false; - - float radius = PickupRadius; - if (radius <= 0f) return true; - - // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT-RANGE-DIAGNOSTICS - return GetHorizontalDistanceTo(pickerPosition) <= radius; - } - - public float GetDistanceTo(Vector3 pickerPosition) - { - // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT-RANGE-DIAGNOSTICS - return Vector3.Distance(transform.position, pickerPosition); - } - - public float GetHorizontalDistanceTo(Vector3 pickerPosition) - { - // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT-RANGE-DIAGNOSTICS - Vector3 position = transform.position; - float deltaX = position.x - pickerPosition.x; - float deltaZ = position.z - pickerPosition.z; - return Mathf.Sqrt(deltaX * deltaX + deltaZ * deltaZ); - } - - public RuntimeItem CreatePickupRuntimeItem() - { - return m_Item != null ? new RuntimeItem(m_Item) : null; - } - - public void MarkPickedUp() - { - if (m_IsConsumed) return; - m_IsConsumed = true; - NetworkWorldObjectRegistry.MarkConsumed(NetworkId); - - if (m_LogDiagnostics) - { - Debug.Log( - $"[NetworkWorldObject] picked up object={name} networkId={NetworkId} item={m_Item?.ID.String}", - this); - } - - if (m_DestroyOnPickup) - { - Destroy(gameObject); - return; - } - - if (m_DisableOnPickup) - { - gameObject.SetActive(false); - } - } - - private uint ResolveNetworkId() - { - if (!m_UseAutomaticNetworkId && m_ManualNetworkId != 0) return m_ManualNetworkId; - if (m_CachedNetworkId != 0) return m_CachedNetworkId; - - string path = BuildStableScenePath(transform); - if (!string.IsNullOrEmpty(m_NetworkIdSalt)) path = $"{path}:{m_NetworkIdSalt}"; - - uint hash = 2166136261u; - for (int i = 0; i < path.Length; i++) - { - hash ^= path[i]; - hash *= 16777619u; - } - - m_CachedNetworkId = hash != 0 ? hash : 1u; - return m_CachedNetworkId; - } - - private static string BuildStableScenePath(Transform target) - { - if (target == null) return string.Empty; - - string scenePath = target.gameObject.scene.path; - if (string.IsNullOrEmpty(scenePath)) scenePath = target.gameObject.scene.name; - - string path = BuildStableScenePathSegment(target); - Transform current = target; - while (current.parent != null) - { - current = current.parent; - path = $"{BuildStableScenePathSegment(current)}/{path}"; - } - - return $"{scenePath}:{path}"; - } - - private static string BuildStableScenePathSegment(Transform target) - { - int sameNameIndex = 0; - Transform parent = target.parent; - if (parent != null) - { - for (int i = 0; i < parent.childCount; i++) - { - Transform sibling = parent.GetChild(i); - if (sibling == target) break; - if (sibling != null && sibling.name == target.name) sameNameIndex++; - } - } - else if (target.gameObject.scene.IsValid()) - { - GameObject[] roots = target.gameObject.scene.GetRootGameObjects(); - for (int i = 0; i < roots.Length; i++) - { - GameObject root = roots[i]; - if (root == null) continue; - if (root.transform == target) break; - if (root.name == target.name) sameNameIndex++; - } - } - - return $"{target.name}[{sameNameIndex}]"; - } - } - - public static class NetworkWorldObjectRegistry - { - // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT - private static readonly Dictionary s_Objects = new(128); - // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT-CONSUMED-REGISTRY - private static readonly HashSet s_ConsumedObjectIds = new(128); - - public static void Register(NetworkWorldObject worldObject) - { - if (worldObject == null || worldObject.NetworkId == 0) return; - s_Objects[worldObject.NetworkId] = worldObject; - } - - public static void Unregister(NetworkWorldObject worldObject) - { - if (worldObject == null || worldObject.NetworkId == 0) return; - if (!s_Objects.TryGetValue(worldObject.NetworkId, out NetworkWorldObject existing)) return; - if (existing == worldObject) s_Objects.Remove(worldObject.NetworkId); - } - - public static void MarkConsumed(uint networkId) - { - // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT-CONSUMED-REGISTRY - if (networkId != 0) s_ConsumedObjectIds.Add(networkId); - } - - public static bool IsConsumed(uint networkId) - { - // [LOCAL-EDIT] #PILFER-INVENTORY-WORLD-OBJECT-CONSUMED-REGISTRY - return networkId != 0 && s_ConsumedObjectIds.Contains(networkId); - } - - public static bool TryGet(uint networkId, out NetworkWorldObject worldObject) - { - worldObject = null; - return networkId != 0 && s_Objects.TryGetValue(networkId, out worldObject) && worldObject != null; - } - } -} -#endif