351 lines
10 KiB
C#
351 lines
10 KiB
C#
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<SweBoundaryStateMessage> 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();
|
|
}
|
|
}
|
|
}
|
|
}
|