using FishNet.Connection;
using FishNet.Object;
using UnityEngine;
namespace Ashwild.Building
{
///
/// The networked side of building: it commits placements and owns the live ghost preview.
///
/// Every committed structure — a wall as much as a chest or a campfire — is spawned as a real
/// NetworkObject owned by the server. That is the whole design: FishNet already replicates spawns,
/// catches late joiners up, and tears objects down on despawn, so there is no build list to keep,
/// no index to align, and no rebuild path to write. Each build carries its own state on its own
/// , which is what lets a stateful structure (chest, cooking station)
/// be placed by exactly the same code as a plain wall instead of needing a parallel mechanism.
///
/// This replaced an earlier data-driven registry (a SyncList of pose records that every client
/// instantiated locally). That saved FishNet's per-object bookkeeping but not the GameObjects —
/// they were instantiated locally anyway — while forcing a second, incompatible path for anything
/// with real state. One mechanism is worth the bookkeeping.
///
/// The ghost is likewise a NetworkObject, but owned by the placer so FishNet's NetworkTransform
/// replicates its motion smoothly; it is transient and one per player.
///
/// BuildManager (the local build UX) drives the owned ghost; on confirm this registry spawns the
/// structure and despawns the ghost. One instance per scene, a scene NetworkObject like the others.
///
[RequireComponent(typeof(NetworkObject))]
public class BuildRegistry : NetworkBehaviour
{
#region State
///
/// The single scene instance, so BuildManager can request ghost spawns/despawns and build commits.
///
public static BuildRegistry Instance { get; private set; }
#endregion
#region Network Lifecycle
///
/// Claims the singleton on every machine.
///
public override void OnStartNetwork()
{
base.OnStartNetwork();
Instance = this;
}
///
/// Releases the singleton when the session ends. Committed structures are NetworkObjects, so
/// FishNet despawns them itself — there is nothing to tear down here.
///
public override void OnStopNetwork()
{
base.OnStopNetwork();
if (Instance == this) Instance = null;
}
#endregion
#region Commit Build
///
/// Called by BuildManager when the local player confirms a placement: commit this buildable at
/// this pose so the real structure appears for every player.
///
public void RequestBuild(ushort buildableId, Vector3 position, Quaternion rotation)
=> RequestBuildServerRpc(buildableId, position, rotation);
///
/// Server-side: validates the id, instantiates the built prefab, seeds its identity and health,
/// then spawns it for everyone. The appear pop is sent separately to current observers only, so a
/// late joiner streaming in an existing base does not watch all of it pop at once.
///
/// The structure is spawned server-owned (no connection passed): builds are shared world state
/// and several players interact with the same chest or fire, so handing ownership to whoever
/// placed it would tie everyone's access to that player's connection.
///
[ServerRpc(RequireOwnership = false)]
private void RequestBuildServerRpc(ushort buildableId, Vector3 position, Quaternion rotation, NetworkConnection conn = null)
{
BuildableData data = BuildableDatabase.Instance != null ? BuildableDatabase.Instance.GetBuildable(buildableId) : null;
if (data == null || data.BuiltPrefab == null)
{
Debug.LogWarning($"[BuildRegistry] Buildable id {buildableId} is unknown or has no BuiltPrefab — nothing spawned.", this);
return;
}
// TODO: validate + consume the requester's resources (conn → PlayerInventory) before committing.
GameObject go = Instantiate(data.BuiltPrefab, position, rotation);
BuiltStructure structure = go.GetComponent();
if (structure == null)
{
Debug.LogError($"[BuildRegistry] BuiltPrefab '{data.BuiltPrefab.name}' has no BuiltStructure — add one (it carries the build's identity, health and demolition).", this);
Destroy(go);
return;
}
structure.InitialiseOnServer(buildableId, data.MaxHealth);
base.ServerManager.Spawn(go);
structure.PlaySpawnBumpForObservers();
}
#endregion
#region Networked Ghost Preview
///
/// Client → asks the server to spawn a ghost of this buildable, owned by us. The spawned
/// object is handed back to BuildManager through , carrying
/// the request token so a superseded spawn can be discarded.
///
public void RequestSpawnGhost(ushort buildableId, int token) => RequestSpawnGhostServerRpc(buildableId, token);
///
/// Client → asks the server to despawn a ghost we own (on confirm, cancel or switch).
///
public void RequestDespawnGhost(NetworkObject ghost)
{
if (ghost != null) RequestDespawnGhostServerRpc(ghost);
}
///
/// Server-side: spawns the buildable's ghost prefab owned by the requester, then tells that
/// client which object to drive. The prefab must be a NetworkObject registered in
/// DefaultPrefabObjects with a client-authoritative NetworkTransform.
///
[ServerRpc(RequireOwnership = false)]
private void RequestSpawnGhostServerRpc(ushort buildableId, int token, NetworkConnection conn = null)
{
if (conn == null) return;
BuildableData data = BuildableDatabase.Instance != null ? BuildableDatabase.Instance.GetBuildable(buildableId) : null;
if (data == null || data.GhostPrefab == null) return;
GameObject go = Instantiate(data.GhostPrefab);
base.ServerManager.Spawn(go, conn);
TargetAttachGhost(conn, go.GetComponent(), token);
}
///
/// Owner-side: hands the freshly spawned ghost (and its request token) to BuildManager to drive.
///
[TargetRpc]
private void TargetAttachGhost(NetworkConnection conn, NetworkObject ghost, int token)
{
if (ghost == null || BuildManager.Instance == null) return;
BuildManager.Instance.AttachGhost(ghost.gameObject, token);
}
///
/// Server-side: destroys a ghost (never pools it — a ghost is single-use). Ownership guards
/// that a client only despawns its own.
///
[ServerRpc(RequireOwnership = false)]
private void RequestDespawnGhostServerRpc(NetworkObject ghost, NetworkConnection conn = null)
{
if (ghost == null) return;
if (ghost.Owner != conn) return;
base.ServerManager.Despawn(ghost, DespawnType.Destroy);
}
///
/// Owner → replicates the ghost's current spot validity to every other client so teammates see
/// the same green (clear) / red (blocked) preview. Only the owner drives the evaluation locally,
/// so without this a remote ghost stays on its default look; BuildManager calls this only when the
/// value actually changes, so the traffic is a rare per-ghost blip, not a per-frame stream.
///
public void SetGhostValidity(NetworkObject ghost, bool valid)
{
if (ghost != null) SetGhostValidityServerRpc(ghost, valid);
}
///
/// Server-side relay: forwards the placer's validity to all observers.
///
[ServerRpc(RequireOwnership = false)]
private void SetGhostValidityServerRpc(NetworkObject ghost, bool valid, NetworkConnection conn = null)
{
if (ghost == null) return;
ObserversSetGhostValidity(ghost, valid);
}
///
/// Applies the replicated validity on every client except the placer, whose own local evaluation
/// already drives the richer verdict (and knows about affordability, which is never sent). A
/// missing ghost is ignored — it may have been despawned mid-flight.
///
[ObserversRpc]
private void ObserversSetGhostValidity(NetworkObject ghost, bool valid)
{
if (ghost == null || ghost.IsOwner) return;
BuildGhost bg = ghost.GetComponent();
if (bg != null) bg.SetRemoteValidity(valid);
}
#endregion
}
}