using System; using System.Net; using System.Net.Sockets; using UnityEngine; namespace FloodSWE.Networking { public sealed class SweQuestControlClient : MonoBehaviour { [Header("Server")] public string serverHost = "127.0.0.1"; public int serverCommandPort = 29010; [Header("Connect")] public bool autoConnectOnEnable = true; public bool sendHelloOnConnect = true; public bool verboseLogging = false; public float ackTimeoutSeconds = 3.0f; public float helloKeepAliveIntervalSeconds = 1.0f; private UdpClient socket; private IPEndPoint serverEndpoint; private float lastAckTimeUnscaled = -1.0f; private float lastHelloSentTimeUnscaled = -1.0f; private float lastStateTimeUnscaled = -1.0f; private string lastAckMessage = ""; private SweBoundaryStateMessage lastBoundaryState; private bool connectRequested; public event Action BoundaryStateReceived; public bool HasReceivedAck => lastAckTimeUnscaled >= 0.0f; public float LastAckAgeSeconds => HasReceivedAck ? Time.unscaledTime - lastAckTimeUnscaled : float.PositiveInfinity; public string LastAckMessage => lastAckMessage; public bool HasBoundaryState => lastStateTimeUnscaled >= 0.0f; public float LastBoundaryStateAgeSeconds => HasBoundaryState ? Time.unscaledTime - lastStateTimeUnscaled : float.PositiveInfinity; public SweBoundaryStateMessage LastBoundaryState => lastBoundaryState; public bool IsConnectionAlive => HasReceivedAck && LastAckAgeSeconds <= Mathf.Max(0.1f, ackTimeoutSeconds); public bool IsWaitingForAck => connectRequested && !IsConnectionAlive && !HasReceivedAck; private void OnEnable() { if (autoConnectOnEnable && !Connect()) { enabled = false; } } private void OnDisable() { Disconnect(); } private void Update() { PollMessages(); TickKeepAlive(); } public bool Connect() { try { EnsureSocket(); ResolveServerEndpoint(); connectRequested = true; if (sendHelloOnConnect) { SendHello(); } if (verboseLogging) { Debug.Log($"SweQuestControlClient: connected to {serverEndpoint}."); } return true; } catch (Exception ex) { Debug.LogError($"SweQuestControlClient: failed to connect UDP sender. {ex.Message}"); connectRequested = false; Disconnect(); return false; } } public void Disconnect() { if (socket != null) { socket.Close(); socket = null; } serverEndpoint = null; lastAckTimeUnscaled = -1.0f; lastStateTimeUnscaled = -1.0f; lastAckMessage = ""; lastBoundaryState = null; connectRequested = false; } public void SendHello() { Send(new SweControlCommand { command = "hello", }); lastHelloSentTimeUnscaled = Time.unscaledTime; } public void SetSourceLevel(int sourceId, float level) { SetBoundaryProfile( "source_area", sourceId, true, 0.0f, 0.0f, 0.0f, Mathf.Max(0.0f, level)); } public void SetSinkLevel(int sinkId, float level) { SetBoundaryProfile( "sink", sinkId, true, 0.0f, 0.0f, 0.0f, -Mathf.Max(0.0f, level)); } public void SetBoundaryProfile( string boundaryKind, int boundaryId, bool enabled, float waterLevelM, float velocityUMps, float velocityVMps, float depthRateMps) { Send(new SweControlCommand { command = "set_boundary_profile", boundaryKind = boundaryKind, boundaryId = boundaryId, enabled = enabled ? 1 : 0, waterLevelM = waterLevelM, velocityUMps = velocityUMps, velocityVMps = velocityVMps, depthRateMps = depthRateMps, }); } public void SetBoundariesBulk(SweBoundaryProfile[] profiles, bool replaceAll = false) { if (profiles == null) { return; } Send(new SweControlCommand { command = "set_boundaries_bulk", replaceAll = replaceAll, boundaries = profiles, }); } public void RequestBoundaryConfig() { Send(new SweControlCommand { command = "get_boundary_config", }); } public void SubscribeBoundaryUpdates(bool subscribe = true) { Send(new SweControlCommand { command = "subscribe_boundary_updates", subscribe = subscribe, }); } public void SetActiveTile(string lod, int tileX, int tileY) { Send(new SweControlCommand { command = "set_active_tile", lod = lod, tileX = tileX, tileY = tileY, }); } public void SaveCheckpoint(string checkpointName) { Send(new SweControlCommand { command = "save_checkpoint", checkpoint = checkpointName, }); } public void ResetCheckpoint(string checkpointName) { Send(new SweControlCommand { command = "reset_checkpoint", checkpoint = checkpointName, }); } public void ApplyPorosityStamp(float u, float v, float radius, float porosity) { Send(new SweControlCommand { command = "apply_porosity_stamp", u = Mathf.Clamp01(u), v = Mathf.Clamp01(v), radius = Mathf.Clamp01(radius), porosity = Mathf.Clamp01(porosity), }); } public void SendDisconnect() { Send(new SweControlCommand { command = "disconnect", }); } private void ResolveServerEndpoint() { IPAddress[] addresses = Dns.GetHostAddresses(serverHost.Trim()); if (addresses.Length == 0) { throw new InvalidOperationException($"No address found for '{serverHost}'."); } serverEndpoint = new IPEndPoint(addresses[0], serverCommandPort); } private void EnsureSocket() { if (socket == null) { socket = new UdpClient(); socket.Client.Blocking = false; } } private void Send(SweControlCommand command) { if (socket == null) { return; } if (serverEndpoint == null) { try { ResolveServerEndpoint(); } catch { return; } } byte[] payload = SweUdpProtocol.EncodeControl(command); socket.Send(payload, payload.Length, serverEndpoint); } private void PollMessages() { if (socket == null) { return; } while (socket.Available > 0) { IPEndPoint sender = new IPEndPoint(IPAddress.Any, 0); byte[] payload; try { payload = socket.Receive(ref sender); } catch (SocketException) { break; } if (SweUdpProtocol.TryDecodeAck(payload, out string message)) { lastAckTimeUnscaled = Time.unscaledTime; lastAckMessage = message ?? ""; if (verboseLogging) { Debug.Log($"SweQuestControlClient: ack from {sender}: {lastAckMessage}"); } continue; } if (SweUdpProtocol.TryDecodeState(payload, out SweBoundaryStateMessage state)) { lastStateTimeUnscaled = Time.unscaledTime; lastBoundaryState = state; BoundaryStateReceived?.Invoke(state); if (verboseLogging) { int count = state != null && state.boundaries != null ? state.boundaries.Length : 0; Debug.Log( $"SweQuestControlClient: boundary state from {sender}: type={state?.messageType} entries={count}"); } continue; } } } private void TickKeepAlive() { if (!connectRequested || socket == null || serverEndpoint == null) { return; } float interval = Mathf.Max(0.1f, helloKeepAliveIntervalSeconds); if (lastHelloSentTimeUnscaled < 0.0f || (Time.unscaledTime - lastHelloSentTimeUnscaled) >= interval) { SendHello(); } } } }