safety snapshot: recover geo importers and current quest/server state
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace FloodSWE.Networking
|
||||
{
|
||||
/// <summary>
|
||||
/// Drives a Connect/Disconnect button from SweQuestControlClient state.
|
||||
/// </summary>
|
||||
public sealed class SweConnectionButtonController : MonoBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
[SerializeField] private SweQuestControlClient controlClient;
|
||||
[SerializeField] private Button button;
|
||||
[SerializeField] private TMP_Text label;
|
||||
|
||||
[Header("Labels")]
|
||||
[SerializeField] private string connectLabel = "Connect";
|
||||
[SerializeField] private string loadingLabel = "Loading...";
|
||||
[SerializeField] private string disconnectLabel = "Disconnect";
|
||||
|
||||
[Header("Behavior")]
|
||||
[SerializeField] private bool disableButtonWhileLoading = true;
|
||||
[SerializeField] private bool allowCancelWhileLoading = false;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (button == null)
|
||||
{
|
||||
button = GetComponent<Button>();
|
||||
}
|
||||
|
||||
if (label == null && button != null)
|
||||
{
|
||||
label = button.GetComponentInChildren<TMP_Text>();
|
||||
}
|
||||
|
||||
if (controlClient == null)
|
||||
{
|
||||
controlClient = FindFirstObjectByType<SweQuestControlClient>();
|
||||
}
|
||||
|
||||
if (button != null)
|
||||
{
|
||||
button.onClick.AddListener(OnButtonPressed);
|
||||
}
|
||||
|
||||
RefreshVisual();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (button != null)
|
||||
{
|
||||
button.onClick.RemoveListener(OnButtonPressed);
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
RefreshVisual();
|
||||
}
|
||||
|
||||
public void OnButtonPressed()
|
||||
{
|
||||
if (controlClient == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (controlClient.IsConnectionAlive)
|
||||
{
|
||||
controlClient.Disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
if (controlClient.IsWaitingForAck)
|
||||
{
|
||||
if (allowCancelWhileLoading)
|
||||
{
|
||||
controlClient.Disconnect();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
controlClient.Connect();
|
||||
}
|
||||
|
||||
private void RefreshVisual()
|
||||
{
|
||||
if (controlClient == null)
|
||||
{
|
||||
SetLabel(connectLabel);
|
||||
SetButtonInteractable(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (controlClient.IsConnectionAlive)
|
||||
{
|
||||
SetLabel(disconnectLabel);
|
||||
SetButtonInteractable(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (controlClient.IsWaitingForAck)
|
||||
{
|
||||
SetLabel(loadingLabel);
|
||||
bool interactable = !disableButtonWhileLoading || allowCancelWhileLoading;
|
||||
SetButtonInteractable(interactable);
|
||||
return;
|
||||
}
|
||||
|
||||
SetLabel(connectLabel);
|
||||
SetButtonInteractable(true);
|
||||
}
|
||||
|
||||
private void SetLabel(string textValue)
|
||||
{
|
||||
if (label == null || label.text == textValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
label.text = textValue;
|
||||
}
|
||||
|
||||
private void SetButtonInteractable(bool interactable)
|
||||
{
|
||||
if (button == null || button.interactable == interactable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
button.interactable = interactable;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46eb88ffb765e8ab790ae8afc4e27087
|
||||
102
Assets/Scripts/Networking/Client/SweConnectionStatusHud.cs
Normal file
102
Assets/Scripts/Networking/Client/SweConnectionStatusHud.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace FloodSWE.Networking
|
||||
{
|
||||
/// <summary>
|
||||
/// Displays Quest->Server connection status based on SweQuestControlClient ACKs.
|
||||
/// </summary>
|
||||
public sealed class SweConnectionStatusHud : MonoBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
[SerializeField] private SweQuestControlClient controlClient;
|
||||
[SerializeField] private TMP_Text statusText;
|
||||
[SerializeField] private TMP_Text ackText;
|
||||
|
||||
[Header("Display")]
|
||||
[SerializeField] private string connectedLabel = "Connected";
|
||||
[SerializeField] private string waitingLabel = "Waiting for ACK";
|
||||
[SerializeField] private string disconnectedLabel = "Disconnected";
|
||||
[SerializeField] private string ackPrefix = "Last ACK: ";
|
||||
[SerializeField] private Color connectedColor = new Color(0.4f, 1.0f, 0.4f);
|
||||
[SerializeField] private Color waitingColor = new Color(1.0f, 0.9f, 0.3f);
|
||||
[SerializeField] private Color disconnectedColor = new Color(1.0f, 0.5f, 0.5f);
|
||||
[SerializeField] private string ageFormat = "0.00";
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (controlClient == null)
|
||||
{
|
||||
controlClient = FindFirstObjectByType<SweQuestControlClient>();
|
||||
}
|
||||
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void Refresh()
|
||||
{
|
||||
if (controlClient == null)
|
||||
{
|
||||
SetStatus(disconnectedLabel, disconnectedColor);
|
||||
SetAck("Control client missing");
|
||||
return;
|
||||
}
|
||||
|
||||
if (controlClient.IsConnectionAlive)
|
||||
{
|
||||
SetStatus(connectedLabel, connectedColor);
|
||||
string ackMsg = string.IsNullOrWhiteSpace(controlClient.LastAckMessage)
|
||||
? "(empty)"
|
||||
: controlClient.LastAckMessage;
|
||||
SetAck($"{ackPrefix}{ackMsg} ({controlClient.LastAckAgeSeconds.ToString(ageFormat)}s)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (controlClient.HasReceivedAck)
|
||||
{
|
||||
SetStatus(disconnectedLabel, disconnectedColor);
|
||||
string ackMsg = string.IsNullOrWhiteSpace(controlClient.LastAckMessage)
|
||||
? "(empty)"
|
||||
: controlClient.LastAckMessage;
|
||||
SetAck($"{ackPrefix}{ackMsg} ({controlClient.LastAckAgeSeconds.ToString(ageFormat)}s)");
|
||||
return;
|
||||
}
|
||||
|
||||
SetStatus(waitingLabel, waitingColor);
|
||||
SetAck("No ACK yet");
|
||||
}
|
||||
|
||||
private void SetStatus(string text, Color color)
|
||||
{
|
||||
if (statusText == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (statusText.text != text)
|
||||
{
|
||||
statusText.text = text;
|
||||
}
|
||||
|
||||
if (statusText.color != color)
|
||||
{
|
||||
statusText.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetAck(string text)
|
||||
{
|
||||
if (ackText == null || ackText.text == text)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ackText.text = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4665719901a7c1f31bb6eb5678abe4b1
|
||||
266
Assets/Scripts/Networking/Client/SweQuestControlClient.cs
Normal file
266
Assets/Scripts/Networking/Client/SweQuestControlClient.cs
Normal file
@@ -0,0 +1,266 @@
|
||||
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 string lastAckMessage = "";
|
||||
private bool connectRequested;
|
||||
|
||||
public bool HasReceivedAck => lastAckTimeUnscaled >= 0.0f;
|
||||
public float LastAckAgeSeconds => HasReceivedAck ? Time.unscaledTime - lastAckTimeUnscaled : float.PositiveInfinity;
|
||||
public string LastAckMessage => lastAckMessage;
|
||||
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()
|
||||
{
|
||||
PollAcks();
|
||||
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;
|
||||
lastAckMessage = "";
|
||||
connectRequested = false;
|
||||
}
|
||||
|
||||
public void SendHello()
|
||||
{
|
||||
Send(new SweControlCommand
|
||||
{
|
||||
command = "hello",
|
||||
});
|
||||
lastHelloSentTimeUnscaled = Time.unscaledTime;
|
||||
}
|
||||
|
||||
public void SetSourceLevel(int sourceId, float level)
|
||||
{
|
||||
Send(new SweControlCommand
|
||||
{
|
||||
command = "set_source_level",
|
||||
sourceId = sourceId,
|
||||
sourceLevel = level,
|
||||
});
|
||||
}
|
||||
|
||||
public void SetSinkLevel(int sinkId, float level)
|
||||
{
|
||||
Send(new SweControlCommand
|
||||
{
|
||||
command = "set_sink_level",
|
||||
sinkId = sinkId,
|
||||
sinkLevel = level,
|
||||
});
|
||||
}
|
||||
|
||||
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 PollAcks()
|
||||
{
|
||||
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))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
lastAckTimeUnscaled = Time.unscaledTime;
|
||||
lastAckMessage = message ?? "";
|
||||
|
||||
if (verboseLogging)
|
||||
{
|
||||
Debug.Log($"SweQuestControlClient: ack from {sender}: {lastAckMessage}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 38923d8392c381c9190bc65a51d7ab7b
|
||||
153
Assets/Scripts/Networking/Client/SweQuestStreamClient.cs
Normal file
153
Assets/Scripts/Networking/Client/SweQuestStreamClient.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using FloodSWE.Streaming;
|
||||
using FloodSWE.TileGraph;
|
||||
using UnityEngine;
|
||||
|
||||
namespace FloodSWE.Networking
|
||||
{
|
||||
public sealed class SweQuestStreamClient : MonoBehaviour
|
||||
{
|
||||
[Header("Network")]
|
||||
public int listenPort = 29011;
|
||||
|
||||
[Header("Interpolation")]
|
||||
public float frameDurationSeconds = 0.1f;
|
||||
|
||||
[Header("Filter")]
|
||||
public bool filterByTile = false;
|
||||
public int expectedLod = 1;
|
||||
public int expectedTileX = 1;
|
||||
public int expectedTileY = 1;
|
||||
|
||||
[Header("Output")]
|
||||
public Material targetMaterial;
|
||||
public string textureProperty = "_HeightTex";
|
||||
|
||||
public Texture2D outputTexture;
|
||||
public HeightmapPacket lastPacket;
|
||||
|
||||
private readonly HeightmapInterpolator interpolator = new HeightmapInterpolator();
|
||||
private UdpClient socket;
|
||||
private IPEndPoint endpoint;
|
||||
private int outputResolution;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
interpolator.Reset();
|
||||
interpolator.FrameDurationSeconds = Mathf.Max(0.01f, frameDurationSeconds);
|
||||
|
||||
try
|
||||
{
|
||||
socket = new UdpClient(listenPort);
|
||||
socket.Client.Blocking = false;
|
||||
endpoint = new IPEndPoint(IPAddress.Any, 0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"SweQuestStreamClient: failed to open UDP port {listenPort}. {ex.Message}");
|
||||
enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (socket != null)
|
||||
{
|
||||
socket.Close();
|
||||
socket = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
PollFrames();
|
||||
interpolator.Step(Time.deltaTime);
|
||||
TryUpdateOutputTexture();
|
||||
}
|
||||
|
||||
private void PollFrames()
|
||||
{
|
||||
if (socket == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
while (socket.Available > 0)
|
||||
{
|
||||
byte[] payload;
|
||||
try
|
||||
{
|
||||
payload = socket.Receive(ref endpoint);
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!SweUdpProtocol.TryDecodeFrame(payload, out HeightmapPacket packet))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (filterByTile)
|
||||
{
|
||||
TileId expected = new TileId(expectedLod, expectedTileX, expectedTileY);
|
||||
if (packet.Tile != expected)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
interpolator.PushPacket(packet);
|
||||
lastPacket = packet;
|
||||
}
|
||||
}
|
||||
|
||||
private void TryUpdateOutputTexture()
|
||||
{
|
||||
if (!interpolator.TryGetHeights(out float[] heights) || heights == null || heights.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int resolution = Mathf.RoundToInt(Mathf.Sqrt(heights.Length));
|
||||
if (resolution <= 0 || resolution * resolution != heights.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureOutputTexture(resolution);
|
||||
outputTexture.SetPixelData(heights, 0);
|
||||
outputTexture.Apply(false, false);
|
||||
|
||||
if (targetMaterial != null)
|
||||
{
|
||||
targetMaterial.SetTexture(textureProperty, outputTexture);
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureOutputTexture(int resolution)
|
||||
{
|
||||
if (outputTexture != null && outputResolution == resolution)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (outputTexture != null)
|
||||
{
|
||||
Destroy(outputTexture);
|
||||
}
|
||||
|
||||
outputTexture = new Texture2D(resolution, resolution, TextureFormat.RFloat, false, true)
|
||||
{
|
||||
wrapMode = TextureWrapMode.Clamp,
|
||||
filterMode = FilterMode.Bilinear,
|
||||
name = "SWE_StreamedHeight"
|
||||
};
|
||||
|
||||
outputResolution = resolution;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 055c4861dda402b40959e0b8173083fe
|
||||
Reference in New Issue
Block a user