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
}
}