Files
DTrierFlood_New/Assets/Scripts/Networking/Client/SweQuestControlClient.cs

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();
}
}
}
}