using System.Collections.Generic; using FishNet.Connection; using FishNet.Object; using FishNet.Object.Synchronizing; using UnityEngine; using Ashwild.Inventory; namespace Ashwild.Network { /// /// Registry for Pickables. On top of the shared WorldObjectRegistry plumbing (claimed scene ids, /// hide/show, catch-up) it adds runtime drops: a synced dictionary of active drops so every client /// spawns the same local visual, and the server-authoritative pickup/drop RPCs that grant items to /// the requesting player. One per scene. /// public class PickableRegistry : WorldObjectRegistry { #region Types /// /// Synced description of a runtime drop so all clients spawn the same local visual. /// public struct DropRecord { public ushort itemId; public int quantity; public Vector3 position; public Quaternion rotation; } #endregion #region Singleton /// /// The pickup registry for the active game scene. /// public static PickableRegistry Instance { get; private set; } #endregion #region State /// /// Active runtime drops, keyed by a negative drop id. /// private readonly SyncDictionary activeDrops = new SyncDictionary(); /// /// Local visuals instantiated for active drops, keyed by drop id. /// private readonly Dictionary dropInstances = new Dictionary(); /// /// Server-side counter for the negative drop-id space. /// private int nextDropId = -1; #endregion #region Lifecycle protected override void Awake() { base.Awake(); if (Instance != null && Instance != this) { Destroy(this); return; } Instance = this; } protected override void OnDestroy() { base.OnDestroy(); if (Instance == this) Instance = null; } public override void OnStartNetwork() { base.OnStartNetwork(); activeDrops.OnChange += OnDropsChanged; } public override void OnStartClient() { base.OnStartClient(); foreach (KeyValuePair kv in activeDrops.Collection) SpawnDropVisual(kv.Key, kv.Value); } public override void OnStopNetwork() { base.OnStopNetwork(); activeDrops.OnChange -= OnDropsChanged; } #endregion #region Public API /// /// Returns whether a pickup (scene id >= 0 or drop id < 0) is no longer available. /// public bool IsClaimed(int id) { if (id >= 0) return IsInactive(id); return !activeDrops.ContainsKey(id); } /// /// Owner-side entry: asks the server to claim a pickup and grant its item. /// [ServerRpc(RequireOwnership = false)] public void RequestPickupServerRpc(int id, NetworkConnection conn = null) { PlayerInventory inventory = ResolveInventory(conn); if (inventory == null) return; if (id >= 0) { // Scene pickup — the server reads the item from its own copy of the object. if (IsInactive(id)) return; if (!TryGetObject(id, out WorldObject obj) || obj is not Pickable pickup) return; if (pickup.ItemData == null) return; MarkInactive(id); inventory.GrantItemFromServer(pickup.ItemData, pickup.Quantity); } else { // Runtime drop — the server reads the item from the synced record. if (!activeDrops.TryGetValue(id, out DropRecord record)) return; ItemData item = ItemDatabase.Instance != null ? ItemDatabase.Instance.GetItem(record.itemId) : null; activeDrops.Remove(id); if (item != null) inventory.GrantItemFromServer(item, record.quantity); } } /// /// Owner-side entry: asks the server to spawn a dropped item into the world. /// [ServerRpc(RequireOwnership = false)] public void RequestDropServerRpc(ushort itemId, int quantity, Vector3 position, Quaternion rotation, NetworkConnection conn = null) { if (ItemDatabase.Instance == null || ItemDatabase.Instance.GetItem(itemId) == null) return; int dropId = nextDropId--; activeDrops.Add(dropId, new DropRecord { itemId = itemId, quantity = quantity, position = position, rotation = rotation }); } #endregion #region Internal Helpers /// /// Spawns or removes a drop visual when the active-drops dictionary changes. /// private void OnDropsChanged(SyncDictionaryOperation op, int id, DropRecord record, bool asServer) { if (op == SyncDictionaryOperation.Add) SpawnDropVisual(id, record); else if (op == SyncDictionaryOperation.Remove) RemoveDropVisual(id); } /// /// Instantiates the local visual for a drop (idempotent). /// private void SpawnDropVisual(int id, DropRecord record) { if (dropInstances.ContainsKey(id)) return; ItemData item = ItemDatabase.Instance != null ? ItemDatabase.Instance.GetItem(record.itemId) : null; if (item == null || item.WorldPrefab == null) return; GameObject go = Instantiate(item.WorldPrefab, record.position, record.rotation); Pickable pickup = go.GetComponent(); if (pickup != null) pickup.InitializeAsDrop(id, item, record.quantity); dropInstances[id] = pickup; } /// /// Destroys the local visual for a claimed/removed drop. /// private void RemoveDropVisual(int id) { if (dropInstances.TryGetValue(id, out Pickable pickup)) { if (pickup != null) Destroy(pickup.gameObject); dropInstances.Remove(id); } } #endregion } }