using System; using Unity.Netcode; using UnityEngine; public enum CannonState : byte { Empty, Loaded, Countdown } public struct CannonShotState : INetworkSerializable, IEquatable { public uint Sequence; public MissileType MissileType; public CannonShotState(uint sequence, MissileType missileType) { Sequence = sequence; MissileType = missileType; } public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { serializer.SerializeValue(ref Sequence); serializer.SerializeValue(ref MissileType); } public bool Equals(CannonShotState other) => Sequence == other.Sequence && MissileType == other.MissileType; } [DisallowMultipleComponent] [RequireComponent(typeof(NetworkObject))] public sealed class NetworkCannonController : NetworkBehaviour { private const ulong EmptyRocketId = ulong.MaxValue; [Header("Configuration")] [SerializeField] private CannonConfig config; [Header("Rocket Slot")] [SerializeField] private CannonRocketSlot rocketSlot; [SerializeField] private Transform rocketSnapPoint; [SerializeField, Min(0.5f)] private float maximumTakeDistance = 4f; [SerializeField, Min(0f)] private float minimumReinsertBlockDuration = 0.1f; [Header("Output")] [SerializeField] private MapController mapController; [SerializeField] private NetworkShockwaveEmitter shotShockwave; private readonly NetworkVariable state = new( CannonState.Empty, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); private readonly NetworkVariable loadedRocketId = new( EmptyRocketId, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); private readonly NetworkVariable selectedDelay = new( 5f, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); private readonly NetworkVariable fireAt = new( 0d, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); private readonly NetworkVariable lastShot = new( default, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); private NetworkRocketItem loadedRocket; private IWorkstationItemControl loadedRocketControl; private ulong blockedRocketId = EmptyRocketId; private Collider[] blockedRocketColliders; private double unblockAfter; public CannonState State => state.Value; public CannonConfig Config => config; public NetworkRocketItem LoadedRocket => loadedRocket != null ? loadedRocket : ResolveLoadedRocket(); public float SelectedDelay => selectedDelay.Value; public double FireAt => fireAt.Value; public float RemainingSeconds => State == CannonState.Countdown && NetworkManager != null ? Mathf.Max(0f, (float)(fireAt.Value - NetworkManager.ServerTime.Time)) : 0f; public bool CanTakeRocket => IsSpawned && State == CannonState.Loaded && loadedRocketId.Value != EmptyRocketId; public CannonShotState LastShot => lastShot.Value; public event Action Changed; public event Action ShotFired; private void Awake() { if (rocketSnapPoint == null && rocketSlot != null) rocketSnapPoint = rocketSlot.SnapPoint; rocketSlot?.Configure(this); } public override void OnNetworkSpawn() { state.OnValueChanged += HandleStateChanged; loadedRocketId.OnValueChanged += HandleRocketIdChanged; selectedDelay.OnValueChanged += HandleDelayChanged; fireAt.OnValueChanged += HandleFireAtChanged; lastShot.OnValueChanged += HandleShotChanged; rocketSlot?.Configure(this); if (IsServer) { state.Value = CannonState.Empty; loadedRocketId.Value = EmptyRocketId; selectedDelay.Value = config != null ? config.DefaultDelay : 5f; fireAt.Value = 0d; } Changed?.Invoke(); } public override void OnNetworkDespawn() { state.OnValueChanged -= HandleStateChanged; loadedRocketId.OnValueChanged -= HandleRocketIdChanged; selectedDelay.OnValueChanged -= HandleDelayChanged; fireAt.OnValueChanged -= HandleFireAtChanged; lastShot.OnValueChanged -= HandleShotChanged; } private void Update() { if (!IsServer || !IsSpawned) return; UpdateReinsertBlock(); if (State == CannonState.Countdown && NetworkManager != null && NetworkManager.ServerTime.Time >= fireAt.Value) { FireOnServer(); } } private void LateUpdate() { if (!IsServer || !IsSpawned || LoadedRocket == null || rocketSnapPoint == null) return; loadedRocket.transform.SetPositionAndRotation( rocketSnapPoint.position, rocketSnapPoint.rotation); } public bool TryInsertRocket(NetworkRocketItem rocket) { if (!IsServer || !IsSpawned || State != CannonState.Empty || rocket == null || !rocket.IsSpawned || rocket.Definition == null || rocket.NetworkObjectId == blockedRocketId || (config != null && !config.AcceptDefectiveRockets && rocket.IsDefective) || (config != null && !config.AcceptCriticallyDamagedRockets && rocket.IsCriticallyDamaged) || !WorkstationItemControlUtility.TryGetControl( rocket.GetComponent(), out IWorkstationItemControl control) || !control.TryAcquireExternalControl()) { return false; } loadedRocket = rocket; loadedRocketControl = control; loadedRocketId.Value = rocket.NetworkObjectId; state.Value = CannonState.Loaded; SnapLoadedRocket(); return true; } public void RequestTakeRocket() { if (CanTakeRocket) RequestTakeRocketRpc(); } public void RequestSetShotDelay(float seconds) { if (!IsSpawned) return; if (IsServer) SetShotDelayOnServer(seconds); else RequestSetShotDelayRpc(seconds); } public void RequestAdjustShotDelay(float delta) { RequestSetShotDelay(SelectedDelay + delta); } public void RequestIncreaseShotDelay() { RequestAdjustShotDelay(config != null ? config.AdjustmentStep : 1f); } public void RequestDecreaseShotDelay() { RequestAdjustShotDelay(-(config != null ? config.AdjustmentStep : 1f)); } public void RequestStartCountdown() { if (!IsSpawned) return; if (IsServer) TryStartCountdownOnServer(); else RequestStartCountdownRpc(); } public void SetFireRequested(bool isActivated) { if (isActivated && IsServer && IsSpawned) TryStartCountdownOnServer(); } [ContextMenu("Debug/Start Countdown (Server Play Mode)")] private void StartCountdownFromContextMenu() { if (Application.isPlaying) TryStartCountdownOnServer(); } [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] private void RequestSetShotDelayRpc(float seconds) => SetShotDelayOnServer(seconds); [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] private void RequestStartCountdownRpc() => TryStartCountdownOnServer(); [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] private void RequestTakeRocketRpc(RpcParams rpcParams = default) { if (!CanTakeRocket || rocketSlot == null || !TryGetPlayerObject(rpcParams.Receive.SenderClientId, out NetworkObject player)) return; float maxDistanceSquared = maximumTakeDistance * maximumTakeDistance; if ((player.transform.position - rocketSlot.InteractionPosition).sqrMagnitude > maxDistanceSquared) return; NetworkRocketItem rocket = LoadedRocket; IWorkstationItemControl control = loadedRocketControl ?? ResolveLoadedControl(); if (rocket == null || control == null || !control.TryReleaseExternalControlToPlayer(rpcParams.Receive.SenderClientId)) return; blockedRocketId = rocket.NetworkObjectId; blockedRocketColliders = rocket.GetComponentsInChildren(true); unblockAfter = GetNetworkTime() + minimumReinsertBlockDuration; ClearRocketSlot(); } private void SetShotDelayOnServer(float seconds) { if (!IsServer || State == CannonState.Countdown) return; selectedDelay.Value = config != null ? config.ClampDelay(seconds) : Mathf.Clamp(seconds, 0f, 60f); } private bool TryStartCountdownOnServer() { if (!IsServer || !IsSpawned || State != CannonState.Loaded || LoadedRocket == null || NetworkManager == null) return false; fireAt.Value = NetworkManager.ServerTime.Time + selectedDelay.Value; state.Value = CannonState.Countdown; return true; } private void FireOnServer() { NetworkRocketItem rocket = LoadedRocket; if (rocket == null || rocket.Definition == null) { ClearRocketSlot(); return; } MissileType missileType = rocket.Definition.MissileType; mapController?.RequestShot(missileType); shotShockwave?.EmitShockwave(); lastShot.Value = new CannonShotState(lastShot.Value.Sequence + 1, missileType); NetworkObject rocketObject = rocket.NetworkObject; loadedRocket = null; loadedRocketControl = null; loadedRocketId.Value = EmptyRocketId; fireAt.Value = 0d; state.Value = CannonState.Empty; if (rocketObject != null && rocketObject.IsSpawned) rocketObject.Despawn(true); } private void ClearRocketSlot() { loadedRocket = null; loadedRocketControl = null; loadedRocketId.Value = EmptyRocketId; fireAt.Value = 0d; state.Value = CannonState.Empty; } private void SnapLoadedRocket() { if (loadedRocket != null && rocketSnapPoint != null) { loadedRocket.transform.SetPositionAndRotation( rocketSnapPoint.position, rocketSnapPoint.rotation); } } private NetworkRocketItem ResolveLoadedRocket() { if (loadedRocketId.Value == EmptyRocketId || NetworkManager == null || NetworkManager.SpawnManager == null || !NetworkManager.SpawnManager.SpawnedObjects.TryGetValue( loadedRocketId.Value, out NetworkObject networkObject)) return null; loadedRocket = networkObject.GetComponent(); return loadedRocket; } private IWorkstationItemControl ResolveLoadedControl() { NetworkRocketItem rocket = LoadedRocket; WorkstationItemControlUtility.TryGetControl( rocket != null ? rocket.GetComponent() : null, out loadedRocketControl); return loadedRocketControl; } private bool TryGetPlayerObject(ulong clientId, out NetworkObject player) { player = null; return NetworkManager != null && NetworkManager.ConnectedClients.TryGetValue(clientId, out NetworkClient client) && (player = client.PlayerObject) != null; } private void UpdateReinsertBlock() { if (blockedRocketId == EmptyRocketId || rocketSlot == null || GetNetworkTime() < unblockAfter || rocketSlot.OverlapsAny(blockedRocketColliders, 0.05f)) return; blockedRocketId = EmptyRocketId; blockedRocketColliders = null; } private double GetNetworkTime() => NetworkManager != null ? NetworkManager.ServerTime.Time : Time.timeAsDouble; private void HandleStateChanged(CannonState _, CannonState __) => Changed?.Invoke(); private void HandleRocketIdChanged(ulong _, ulong __) { loadedRocket = null; loadedRocketControl = null; Changed?.Invoke(); } private void HandleDelayChanged(float _, float __) => Changed?.Invoke(); private void HandleFireAtChanged(double _, double __) => Changed?.Invoke(); private void HandleShotChanged(CannonShotState previous, CannonShotState current) { if (current.Sequence == previous.Sequence) return; ShotFired?.Invoke(current); Changed?.Invoke(); } }