78bfdf2828
# Conflicts: # Assets/External/Animated PBR Chest Demo/Materials/WoodChest.mat # Packages/com.distantlands.cozy.core/Content/Integration/Import for BiRP.unitypackage.meta # Packages/com.distantlands.cozy.core/Content/Integration/Import for HDRP.unitypackage.meta # Packages/com.distantlands.cozy.core/Content/Integration/Import for URP.unitypackage.meta
206 lines
8.2 KiB
C#
206 lines
8.2 KiB
C#
using System.Collections.Generic;
|
|
using FishNet.Connection;
|
|
using FishNet.Object;
|
|
using FishNet.Object.Synchronizing;
|
|
using UnityEngine;
|
|
using Ashwild.Inventory;
|
|
using Ashwild.Player;
|
|
|
|
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.
|
|
///
|
|
/// Rejects — loudly — an object whose id is unusable, because both failure modes are otherwise
|
|
/// invisible at runtime and produce the exact same symptom: the interaction plays its local
|
|
/// feedback and grants nothing. A scene object left at id -1 (the id tool was never run on it, or
|
|
/// its prefab carries a baked id) would be treated as a runtime drop; two objects sharing an id
|
|
/// would overwrite each other here, so claiming one silently kills the other for good. The object
|
|
/// stays unregistered, which is what makes the interaction refuse itself instead of failing later.
|
|
/// </summary>
|
|
public void RegisterObject(WorldObject obj)
|
|
{
|
|
if (obj == null) return;
|
|
|
|
if (obj.Id < 0)
|
|
{
|
|
Debug.LogError($"[{GetType().Name}] '{obj.name}' has no baked id (id = {obj.Id}) and cannot be " +
|
|
"tracked — it will not be interactable. Run Tools ▸ Ashwild ▸ Setup World Object IDs and save the scene.", obj);
|
|
return;
|
|
}
|
|
|
|
if (registered.TryGetValue(obj.Id, out WorldObject existing) && existing != null && existing != obj)
|
|
{
|
|
Debug.LogError($"[{GetType().Name}] Duplicate world object id {obj.Id}: '{obj.name}' collides with " +
|
|
$"'{existing.name}'. Claiming one would silently disable the other. Run Tools ▸ Ashwild ▸ Setup World Object IDs.", obj);
|
|
return;
|
|
}
|
|
|
|
registered[obj.Id] = obj;
|
|
if (inactiveIds.Contains(obj.Id))
|
|
obj.HideAsInactive(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns whether this id is tracked by the registry on this client. Interactions check it before
|
|
/// asking the server, so an object the registry never accepted (bad or duplicate id) refuses up
|
|
/// front rather than playing its feedback and losing the request server-side.
|
|
/// </summary>
|
|
public bool IsRegistered(int id) => registered.ContainsKey(id);
|
|
|
|
/// <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 owned by the given connection. Delegates to the shared lookup,
|
|
/// which scans the connection's objects instead of trusting FirstObject (see NetworkPlayerLookup).
|
|
/// </summary>
|
|
protected PlayerInventory ResolveInventory(NetworkConnection conn) => NetworkPlayerLookup.ResolveInventory(conn);
|
|
|
|
/// <summary>
|
|
/// Server-side: reports an interaction the server could not carry out for a reason the client had
|
|
/// no way to foresee (an id it does not track, a player object it cannot resolve). It logs on the
|
|
/// server and tells the requesting player, because the alternative — the historic behaviour — is a
|
|
/// bare `return` that leaves the player watching a hit that gave nothing, with the only trace of it
|
|
/// in the host's console. Normal races (someone else took it first) are not reported here: the
|
|
/// object visibly disappears, which is explanation enough.
|
|
/// </summary>
|
|
protected void ReportFailure(NetworkConnection conn, int id, string reason)
|
|
{
|
|
Debug.LogWarning($"[{GetType().Name}] Interaction on id {id} refused for client " +
|
|
$"{(conn != null ? conn.ClientId : -1)}: {reason}", this);
|
|
TargetReportFailure(conn, id, reason);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs on the requesting client: surfaces the refusal in the HUD and logs it locally, so the
|
|
/// player who actually experienced it sees the diagnosis in their own console.
|
|
/// </summary>
|
|
[TargetRpc]
|
|
private void TargetReportFailure(NetworkConnection conn, int id, string reason)
|
|
{
|
|
Debug.LogError($"[{GetType().Name}] The server refused the interaction on id {id}: {reason}", this);
|
|
PlayerEvents.RaiseInteractionRefused("Interaction failed");
|
|
}
|
|
|
|
#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
|
|
}
|
|
}
|