Enhance school creation and map management features by updating the API to support mod packs and map layouts. Introduce a new map snapshot protocol for efficient data handling during school sessions. Revise documentation to reflect these changes, including updates to the protocol and architecture documents. Improve UI components for mod selection and map editing, ensuring a better user experience. Update tests to validate new functionalities and ensure robustness.
This commit is contained in:
@@ -4,9 +4,9 @@ using System.Threading.Channels;
|
||||
namespace HSchool.Server.Net;
|
||||
|
||||
/// <summary>
|
||||
/// One connected browser. Frames are queued instead of written inline so a slow client can never
|
||||
/// stall a school worker; when the outbox overflows the oldest frame is dropped, which is right
|
||||
/// for a clock that is resent 20 times a second.
|
||||
/// One connected browser. Clock frames go through a 32-slot outbox that drops the oldest under
|
||||
/// pressure — a stale clock is worthless. One-shot frames (the map snapshot) use a separate
|
||||
/// reliable channel so they cannot be crowded out by ticks.
|
||||
/// </summary>
|
||||
internal sealed class GameClient(uint playerId, WebSocket socket)
|
||||
{
|
||||
@@ -20,8 +20,16 @@ internal sealed class GameClient(uint playerId, WebSocket socket)
|
||||
SingleWriter = false,
|
||||
});
|
||||
|
||||
private readonly Channel<ReadOnlyMemory<byte>> _reliable =
|
||||
Channel.CreateUnbounded<ReadOnlyMemory<byte>>(new UnboundedChannelOptions
|
||||
{
|
||||
SingleReader = true,
|
||||
SingleWriter = false,
|
||||
});
|
||||
|
||||
private bool _ready;
|
||||
private int _openSchoolId;
|
||||
private int _locale;
|
||||
|
||||
public uint PlayerId { get; } = playerId;
|
||||
|
||||
@@ -33,6 +41,16 @@ internal sealed class GameClient(uint playerId, WebSocket socket)
|
||||
/// </summary>
|
||||
public bool IsReady => Volatile.Read(ref _ready);
|
||||
|
||||
/// <summary>
|
||||
/// Hello locale byte. Workers read this when labelling a map snapshot; unknown values are
|
||||
/// treated as Russian by <see cref="HSchool.Protocol.ProtocolConstants.CatalogLocale"/>.
|
||||
/// </summary>
|
||||
public byte Locale
|
||||
{
|
||||
get => (byte)Volatile.Read(ref _locale);
|
||||
set => Volatile.Write(ref _locale, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// School this connection is watching, or <c>null</c> in the menu. Written by the supervisor
|
||||
/// on open/close, read by the connection thread on disconnect.
|
||||
@@ -50,23 +68,97 @@ internal sealed class GameClient(uint playerId, WebSocket socket)
|
||||
|
||||
public void MarkReady() => Volatile.Write(ref _ready, true);
|
||||
|
||||
/// <summary>Queues a frame. Returns false once the connection is shutting down.</summary>
|
||||
/// <summary>Queues a clock frame. Returns false once the connection is shutting down.</summary>
|
||||
public bool TrySend(ReadOnlyMemory<byte> frame) => _outbox.Writer.TryWrite(frame);
|
||||
|
||||
/// <summary>Pumps queued frames to the socket until cancelled or the outbox completes.</summary>
|
||||
/// <summary>Queues a frame that must arrive; never dropped for a newer clock.</summary>
|
||||
public bool TrySendReliable(ReadOnlyMemory<byte> frame) => _reliable.Writer.TryWrite(frame);
|
||||
|
||||
/// <summary>Pumps queued frames to the socket until cancelled or both channels complete.</summary>
|
||||
public async Task RunSendLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await foreach (var frame in _outbox.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
|
||||
var reliable = _reliable.Reader;
|
||||
var outbox = _outbox.Reader;
|
||||
|
||||
while (Socket.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
if (Socket.State != WebSocketState.Open)
|
||||
if (reliable.TryRead(out var reliableFrame))
|
||||
{
|
||||
break;
|
||||
if (!await SendAsync(reliableFrame, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
await Socket.SendAsync(frame, WebSocketMessageType.Binary, endOfMessage: true, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (outbox.TryRead(out var clockFrame))
|
||||
{
|
||||
if (!await SendAsync(clockFrame, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var waitReliable = reliable.WaitToReadAsync(cancellationToken).AsTask();
|
||||
var waitOutbox = outbox.WaitToReadAsync(cancellationToken).AsTask();
|
||||
Task<bool> finished;
|
||||
try
|
||||
{
|
||||
finished = await Task.WhenAny(waitReliable, waitOutbox).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasData;
|
||||
try
|
||||
{
|
||||
hasData = await finished.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasData)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var other = ReferenceEquals(finished, waitReliable) ? waitOutbox : waitReliable;
|
||||
try
|
||||
{
|
||||
if (!await other.ConfigureAwait(false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CompleteOutbox() => _outbox.Writer.TryComplete();
|
||||
public void CompleteOutbox()
|
||||
{
|
||||
_reliable.Writer.TryComplete();
|
||||
_outbox.Writer.TryComplete();
|
||||
}
|
||||
|
||||
private async Task<bool> SendAsync(ReadOnlyMemory<byte> frame, CancellationToken cancellationToken)
|
||||
{
|
||||
if (Socket.State != WebSocketState.Open)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
await Socket.SendAsync(frame, WebSocketMessageType.Binary, endOfMessage: true, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user