Add C# RUDP channel ergonomics
This commit is contained in:
parent
6373f8126b
commit
a40e0f248b
2 changed files with 215 additions and 0 deletions
|
|
@ -1272,6 +1272,59 @@ namespace GameCult.Networking
|
|||
SendPacket(_session.CreateConnect(NowMs(), payload ?? Array.Empty<byte>()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the client connect packet with a UTF-8 payload.
|
||||
/// </summary>
|
||||
public void Connect(string payload)
|
||||
{
|
||||
Connect(Encoding.UTF8.GetBytes(payload ?? string.Empty));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the client connect packet and polls until the handshake completes or times out.
|
||||
/// </summary>
|
||||
public bool ConnectAndWait(
|
||||
byte[]? payload = null,
|
||||
TimeSpan? timeout = null,
|
||||
TimeSpan? pollInterval = null)
|
||||
{
|
||||
Connect(payload);
|
||||
return AwaitConnected(timeout, pollInterval);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the client connect packet with a UTF-8 payload and polls until the handshake completes or times out.
|
||||
/// </summary>
|
||||
public bool ConnectAndWait(
|
||||
string payload,
|
||||
TimeSpan? timeout = null,
|
||||
TimeSpan? pollInterval = null)
|
||||
{
|
||||
return ConnectAndWait(Encoding.UTF8.GetBytes(payload ?? string.Empty), timeout, pollInterval);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Polls the transport until the RUDP handshake completes or times out.
|
||||
/// </summary>
|
||||
public bool AwaitConnected(TimeSpan? timeout = null, TimeSpan? pollInterval = null)
|
||||
{
|
||||
var deadline = DateTimeOffset.UtcNow.Add(timeout ?? TimeSpan.FromSeconds(1));
|
||||
var interval = pollInterval ?? TimeSpan.FromMilliseconds(5);
|
||||
while (DateTimeOffset.UtcNow < deadline)
|
||||
{
|
||||
_ = ReceiveOnce();
|
||||
PollResends();
|
||||
if (Connected)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Thread.Sleep(interval);
|
||||
}
|
||||
|
||||
return Connected;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a logical transport frame through the RUDP session.
|
||||
/// </summary>
|
||||
|
|
@ -1284,6 +1337,22 @@ namespace GameCult.Networking
|
|||
_stats.FramesSent++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a reliable ordered schema-channel payload.
|
||||
/// </summary>
|
||||
public void SendSchema(byte[] payload)
|
||||
{
|
||||
Send("schema", payload);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a reliable ordered schema-channel UTF-8 payload.
|
||||
/// </summary>
|
||||
public void SendSchema(string payload)
|
||||
{
|
||||
SendSchema(Encoding.UTF8.GetBytes(payload ?? string.Empty));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a CultNet schema-v0 message on the reliable ordered schema channel.
|
||||
/// </summary>
|
||||
|
|
@ -1294,6 +1363,38 @@ namespace GameCult.Networking
|
|||
Send("schema", CultNetSchemaMessageSerialization.Serialize(message));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends an unreliable sequenced latest-state payload.
|
||||
/// </summary>
|
||||
public void SendLatest(byte[] payload)
|
||||
{
|
||||
Send("latest", payload);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends an unreliable sequenced latest-state UTF-8 payload.
|
||||
/// </summary>
|
||||
public void SendLatest(string payload)
|
||||
{
|
||||
SendLatest(Encoding.UTF8.GetBytes(payload ?? string.Empty));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends an unreliable unordered realtime payload.
|
||||
/// </summary>
|
||||
public void SendRealtime(byte[] payload)
|
||||
{
|
||||
Send("realtime", payload);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends an unreliable unordered realtime UTF-8 payload.
|
||||
/// </summary>
|
||||
public void SendRealtime(string payload)
|
||||
{
|
||||
SendRealtime(Encoding.UTF8.GetBytes(payload ?? string.Empty));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to receive the next CultNet schema-v0 message.
|
||||
/// </summary>
|
||||
|
|
@ -1325,6 +1426,61 @@ namespace GameCult.Networking
|
|||
return ReceiveSchemaMessageOnce() as TMessage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Polls until a delivered transport frame matches the predicate or the timeout expires.
|
||||
/// </summary>
|
||||
public CultNetTransportFrame? ReceiveUntil(
|
||||
TimeSpan timeout,
|
||||
Func<CultNetTransportFrame, bool>? predicate = null,
|
||||
TimeSpan? pollInterval = null)
|
||||
{
|
||||
var deadline = DateTimeOffset.UtcNow.Add(timeout);
|
||||
var interval = pollInterval ?? TimeSpan.FromMilliseconds(5);
|
||||
predicate ??= _ => true;
|
||||
while (DateTimeOffset.UtcNow < deadline)
|
||||
{
|
||||
var frame = ReceiveOnce();
|
||||
if (frame != null && predicate(frame))
|
||||
{
|
||||
return frame;
|
||||
}
|
||||
|
||||
PollResends();
|
||||
Thread.Sleep(interval);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Polls until a schema-channel payload arrives or the timeout expires.
|
||||
/// </summary>
|
||||
public byte[]? ReceiveSchema(TimeSpan timeout, TimeSpan? pollInterval = null)
|
||||
{
|
||||
return ReceiveUntil(
|
||||
timeout,
|
||||
frame => string.Equals(frame.ChannelId, "schema", StringComparison.Ordinal),
|
||||
pollInterval)?.Payload;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Polls until a CultNet schema-v0 message arrives or the timeout expires.
|
||||
/// </summary>
|
||||
public ICultNetSchemaMessage? ReceiveSchemaMessage(TimeSpan timeout, TimeSpan? pollInterval = null)
|
||||
{
|
||||
var payload = ReceiveSchema(timeout, pollInterval);
|
||||
return payload == null ? null : CultNetSchemaMessageSerialization.Deserialize(payload);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Polls until a CultNet schema-v0 message of the requested type arrives or the timeout expires.
|
||||
/// </summary>
|
||||
public TMessage? ReceiveSchemaMessage<TMessage>(TimeSpan timeout, TimeSpan? pollInterval = null)
|
||||
where TMessage : class, ICultNetSchemaMessage
|
||||
{
|
||||
return ReceiveSchemaMessage(timeout, pollInterval) as TMessage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a transport-level disconnect packet.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -721,6 +721,65 @@ namespace GameCult.Networking.Tests
|
|||
Assert.That(profile.Transports[0].Protocol, Is.EqualTo("rudp"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RudpSocketTransport_ErgonomicHelpersCarryNamedChannels()
|
||||
{
|
||||
using var serverSocket = BindUdpSocket();
|
||||
using var clientSocket = BindUdpSocket();
|
||||
var serverEndPoint = serverSocket.LocalEndPoint!;
|
||||
const uint connectionId = 0x10203044;
|
||||
using var server = new CultNetRudpSocketTransportConnection(new CultNetRudpSocketTransportOptions
|
||||
{
|
||||
RuntimeId = "csharp-rudp-helper-server",
|
||||
Socket = serverSocket,
|
||||
Mode = CultNetRudpSocketMode.Server,
|
||||
ConnectionId = connectionId,
|
||||
InitialSequence = 100,
|
||||
ResendDelayMs = 25
|
||||
});
|
||||
using var client = new CultNetRudpSocketTransportConnection(new CultNetRudpSocketTransportOptions
|
||||
{
|
||||
RuntimeId = "csharp-rudp-helper-client",
|
||||
Socket = clientSocket,
|
||||
Mode = CultNetRudpSocketMode.Client,
|
||||
RemoteEndPoint = serverEndPoint,
|
||||
ConnectionId = connectionId,
|
||||
InitialSequence = 1,
|
||||
ResendDelayMs = 25
|
||||
});
|
||||
|
||||
client.Connect("join");
|
||||
PumpRudpHandshake(client, server);
|
||||
|
||||
client.SendSchema("client-state");
|
||||
var schemaPayload = server.ReceiveSchema(TimeSpan.FromSeconds(1));
|
||||
Assert.That(schemaPayload, Is.Not.Null);
|
||||
Assert.That(Encoding.UTF8.GetString(schemaPayload!), Is.EqualTo("client-state"));
|
||||
|
||||
server.SendSchemaMessage(new CultNetHelloMessage
|
||||
{
|
||||
RuntimeId = "csharp-rudp-helper-server",
|
||||
RuntimeKind = "csharp"
|
||||
});
|
||||
var hello = client.ReceiveSchemaMessage<CultNetHelloMessage>(TimeSpan.FromSeconds(1));
|
||||
Assert.That(hello, Is.Not.Null);
|
||||
Assert.That(hello!.RuntimeId, Is.EqualTo("csharp-rudp-helper-server"));
|
||||
|
||||
client.SendLatest("latest-state");
|
||||
var latest = server.ReceiveUntil(
|
||||
TimeSpan.FromSeconds(1),
|
||||
frame => string.Equals(frame.ChannelId, "latest", StringComparison.Ordinal));
|
||||
Assert.That(latest, Is.Not.Null);
|
||||
Assert.That(Encoding.UTF8.GetString(latest!.Payload), Is.EqualTo("latest-state"));
|
||||
|
||||
client.SendRealtime("tick");
|
||||
var realtime = server.ReceiveUntil(
|
||||
TimeSpan.FromSeconds(1),
|
||||
frame => string.Equals(frame.ChannelId, "realtime", StringComparison.Ordinal));
|
||||
Assert.That(realtime, Is.Not.Null);
|
||||
Assert.That(Encoding.UTF8.GetString(realtime!.Payload), Is.EqualTo("tick"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RudpSocketTransport_CarriesFragmentedReliableOrderedSchemaFrames()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue