204 lines
6.6 KiB
C#
204 lines
6.6 KiB
C#
using System.Collections.Generic;
|
|
using FishNet.Connection;
|
|
using FishNet.Object;
|
|
using FishNet.Object.Synchronizing;
|
|
using UnityEngine;
|
|
using Ashwild.Inventory;
|
|
|
|
namespace Ashwild.Network
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public class PickableRegistry : WorldObjectRegistry
|
|
{
|
|
#region Types
|
|
|
|
/// <summary>
|
|
/// Synced description of a runtime drop so all clients spawn the same local visual.
|
|
/// </summary>
|
|
public struct DropRecord
|
|
{
|
|
public ushort itemId;
|
|
public int quantity;
|
|
public Vector3 position;
|
|
public Quaternion rotation;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Singleton
|
|
|
|
/// <summary>
|
|
/// The pickup registry for the active game scene.
|
|
/// </summary>
|
|
public static PickableRegistry Instance { get; private set; }
|
|
|
|
#endregion
|
|
|
|
#region State
|
|
|
|
/// <summary>
|
|
/// Active runtime drops, keyed by a negative drop id.
|
|
/// </summary>
|
|
private readonly SyncDictionary<int, DropRecord> activeDrops = new SyncDictionary<int, DropRecord>();
|
|
|
|
/// <summary>
|
|
/// Local visuals instantiated for active drops, keyed by drop id.
|
|
/// </summary>
|
|
private readonly Dictionary<int, Pickable> dropInstances = new Dictionary<int, Pickable>();
|
|
|
|
/// <summary>
|
|
/// Server-side counter for the negative drop-id space.
|
|
/// </summary>
|
|
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<int, DropRecord> kv in activeDrops.Collection)
|
|
SpawnDropVisual(kv.Key, kv.Value);
|
|
}
|
|
|
|
public override void OnStopNetwork()
|
|
{
|
|
base.OnStopNetwork();
|
|
activeDrops.OnChange -= OnDropsChanged;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Public API
|
|
|
|
/// <summary>
|
|
/// Returns whether a pickup (scene id >= 0 or drop id < 0) is no longer available.
|
|
/// </summary>
|
|
public bool IsClaimed(int id)
|
|
{
|
|
if (id >= 0) return IsInactive(id);
|
|
return !activeDrops.ContainsKey(id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Owner-side entry: asks the server to claim a pickup and grant its item.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Owner-side entry: asks the server to spawn a dropped item into the world.
|
|
/// </summary>
|
|
[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
|
|
|
|
/// <summary>
|
|
/// Spawns or removes a drop visual when the active-drops dictionary changes.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Instantiates the local visual for a drop (idempotent).
|
|
/// </summary>
|
|
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<Pickable>();
|
|
if (pickup != null) pickup.InitializeAsDrop(id, item, record.quantity);
|
|
dropInstances[id] = pickup;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Destroys the local visual for a claimed/removed drop.
|
|
/// </summary>
|
|
private void RemoveDropVisual(int id)
|
|
{
|
|
if (dropInstances.TryGetValue(id, out Pickable pickup))
|
|
{
|
|
if (pickup != null) Destroy(pickup.gameObject);
|
|
dropInstances.Remove(id);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|