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