Files
2026-06-22 16:18:34 +02:00

153 lines
4.8 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>
/// Base registry that makes many local WorldObjects multiplayer-safe without any of them being a
/// NetworkObject. It owns the shared plumbing: the synced set of "inactive" ids (claimed pickups,
/// depleted harvestables), the local id → object lookup, the catch-up on join, and hide/show.
/// Concrete registries (PickableRegistry, HarvestableRegistry) inherit this and add their own
/// interaction RPCs and state. One instance per type per scene.
/// </summary>
[RequireComponent(typeof(NetworkObject))]
public abstract class WorldObjectRegistry : NetworkBehaviour
{
#region State
/// <summary>
/// Ids of objects currently removed from the world (claimed / depleted), synced to all clients.
/// </summary>
private readonly SyncHashSet<int> inactiveIds = new SyncHashSet<int>();
/// <summary>
/// Local lookup of registered objects by id.
/// </summary>
private readonly Dictionary<int, WorldObject> registered = new Dictionary<int, WorldObject>();
#endregion
#region Unity Lifecycle
/// <summary>
/// Hook for subclasses to set their typed singleton; base does nothing.
/// </summary>
protected virtual void Awake() { }
/// <summary>
/// Hook for subclasses to clear their typed singleton; base does nothing.
/// </summary>
protected virtual void OnDestroy() { }
#endregion
#region Network Lifecycle
public override void OnStartNetwork()
{
base.OnStartNetwork();
inactiveIds.OnChange += OnInactiveChanged;
}
public override void OnStartClient()
{
base.OnStartClient();
// Catch up on objects already inactive before we joined (silent — no effects).
foreach (int id in inactiveIds.Collection)
HideById(id, false);
}
public override void OnStopNetwork()
{
base.OnStopNetwork();
inactiveIds.OnChange -= OnInactiveChanged;
}
#endregion
#region Public API
/// <summary>
/// Registers a world object, hiding it immediately if it is already inactive.
/// </summary>
public void RegisterObject(WorldObject obj)
{
if (obj == null) return;
registered[obj.Id] = obj;
if (inactiveIds.Contains(obj.Id))
obj.HideAsInactive(false);
}
/// <summary>
/// Returns whether the object with this id has been removed from the world.
/// </summary>
public bool IsInactive(int id) => inactiveIds.Contains(id);
#endregion
#region Protected Helpers
/// <summary>
/// Looks up a registered object by id.
/// </summary>
protected bool TryGetObject(int id, out WorldObject obj) => registered.TryGetValue(id, out obj);
/// <summary>
/// Server-side: marks an object inactive (replicates → all clients hide it).
/// </summary>
protected void MarkInactive(int id)
{
if (!inactiveIds.Contains(id)) inactiveIds.Add(id);
}
/// <summary>
/// Server-side: marks an object active again (replicates → all clients show it).
/// </summary>
protected void MarkActive(int id)
{
if (inactiveIds.Contains(id)) inactiveIds.Remove(id);
}
/// <summary>
/// Returns the PlayerInventory on the player object owned by the given connection.
/// </summary>
protected PlayerInventory ResolveInventory(NetworkConnection conn)
{
NetworkObject playerObject = conn != null ? conn.FirstObject : null;
return playerObject != null ? playerObject.GetComponent<PlayerInventory>() : null;
}
#endregion
#region Internal Helpers
/// <summary>
/// Hides or shows an object when its id enters or leaves the inactive set.
/// </summary>
private void OnInactiveChanged(SyncHashSetOperation op, int id, bool asServer)
{
if (op == SyncHashSetOperation.Add) HideById(id, true);
else if (op == SyncHashSetOperation.Remove) ShowById(id);
}
private void HideById(int id, bool fresh)
{
if (registered.TryGetValue(id, out WorldObject obj) && obj != null)
obj.HideAsInactive(fresh);
}
private void ShowById(int id)
{
if (registered.TryGetValue(id, out WorldObject obj) && obj != null)
obj.ShowActive();
}
#endregion
}
}