Update .gitignore to exclude TypeScript build info and add dist directory. Expand README with project overview, technology stack, prerequisites, and instructions for running and testing the application.
ci / server (push) Failing after 4m10s
ci / client (push) Successful in 17s

This commit is contained in:
Leonid Pershin
2026-08-18 11:11:48 +03:00
parent 84aafb0b69
commit e6739e7912
84 changed files with 5698 additions and 2 deletions
+171
View File
@@ -0,0 +1,171 @@
namespace HSchool.Protocol;
/// <summary>
/// The single place where the wire format is defined on the .NET side.
/// Every change here must be mirrored in <c>src/HSchool.Client/src/net/protocol.ts</c>
/// and documented in <c>docs/protocol.md</c>.
/// </summary>
public static class ProtocolCodec
{
public static int WriteHello(Span<byte> destination, in ClientHelloMessage message)
{
var writer = new PacketWriter(destination);
writer.WriteMessageType(MessageType.ClientHello);
writer.WriteByte(message.ProtocolVersion);
writer.WriteShortString(message.PlayerName);
return writer.Position;
}
public static int WriteInput(Span<byte> destination, in ClientInputMessage message)
{
var writer = new PacketWriter(destination);
writer.WriteMessageType(MessageType.ClientInput);
writer.WriteUInt32(message.Sequence);
writer.WriteByte((byte)message.Buttons);
return writer.Position;
}
public static int WritePing(Span<byte> destination, in ClientPingMessage message)
{
var writer = new PacketWriter(destination);
writer.WriteMessageType(MessageType.ClientPing);
writer.WriteInt64(message.ClientTimeMs);
return writer.Position;
}
public static int WriteWelcome(Span<byte> destination, in ServerWelcomeMessage message)
{
var writer = new PacketWriter(destination);
writer.WriteMessageType(MessageType.ServerWelcome);
writer.WriteByte(message.ProtocolVersion);
writer.WriteUInt32(message.PlayerEntityId);
writer.WriteByte(message.TickRate);
writer.WriteSingle(message.WorldWidth);
writer.WriteSingle(message.WorldHeight);
return writer.Position;
}
public static int WritePong(Span<byte> destination, in ServerPongMessage message)
{
var writer = new PacketWriter(destination);
writer.WriteMessageType(MessageType.ServerPong);
writer.WriteInt64(message.ClientTimeMs);
writer.WriteUInt32(message.ServerTick);
return writer.Position;
}
/// <summary>Writes a full-state snapshot; entities missing from it are despawned by the client.</summary>
public static int WriteSnapshot(Span<byte> destination, uint tick, ReadOnlySpan<EntitySnapshot> entities)
{
if (entities.Length > ushort.MaxValue)
{
throw new ProtocolException($"Snapshot holds {entities.Length} entities, limit is {ushort.MaxValue}.");
}
var writer = new PacketWriter(destination);
writer.WriteMessageType(MessageType.ServerSnapshot);
writer.WriteUInt32(tick);
writer.WriteUInt16((ushort)entities.Length);
foreach (var entity in entities)
{
writer.WriteUInt32(entity.Id);
writer.WriteByte((byte)entity.Kind);
writer.WriteSingle(entity.X);
writer.WriteSingle(entity.Y);
writer.WriteSingle(entity.Radius);
writer.WriteUInt32(entity.Color);
}
return writer.Position;
}
/// <summary>Exact byte size of a snapshot frame for <paramref name="entityCount"/> entities.</summary>
public static int SnapshotSize(int entityCount) =>
ProtocolConstants.SnapshotHeaderSize + (entityCount * ProtocolConstants.EntitySnapshotSize);
public static MessageType PeekMessageType(ReadOnlySpan<byte> source) =>
source.IsEmpty ? MessageType.None : (MessageType)source[0];
public static ClientHelloMessage ReadHello(ReadOnlySpan<byte> source)
{
var reader = new PacketReader(source);
Expect(ref reader, MessageType.ClientHello);
var version = reader.ReadByte();
var name = reader.ReadShortString();
return new ClientHelloMessage(version, name);
}
public static ClientInputMessage ReadInput(ReadOnlySpan<byte> source)
{
var reader = new PacketReader(source);
Expect(ref reader, MessageType.ClientInput);
var sequence = reader.ReadUInt32();
var buttons = (InputButtons)reader.ReadByte();
return new ClientInputMessage(sequence, buttons);
}
public static ClientPingMessage ReadPing(ReadOnlySpan<byte> source)
{
var reader = new PacketReader(source);
Expect(ref reader, MessageType.ClientPing);
return new ClientPingMessage(reader.ReadInt64());
}
public static ServerWelcomeMessage ReadWelcome(ReadOnlySpan<byte> source)
{
var reader = new PacketReader(source);
Expect(ref reader, MessageType.ServerWelcome);
var version = reader.ReadByte();
var playerEntityId = reader.ReadUInt32();
var tickRate = reader.ReadByte();
var width = reader.ReadSingle();
var height = reader.ReadSingle();
return new ServerWelcomeMessage(version, playerEntityId, tickRate, width, height);
}
public static ServerPongMessage ReadPong(ReadOnlySpan<byte> source)
{
var reader = new PacketReader(source);
Expect(ref reader, MessageType.ServerPong);
var clientTime = reader.ReadInt64();
var serverTick = reader.ReadUInt32();
return new ServerPongMessage(clientTime, serverTick);
}
/// <summary>Reads a snapshot into <paramref name="destination"/> and returns the entity count.</summary>
public static int ReadSnapshot(ReadOnlySpan<byte> source, Span<EntitySnapshot> destination, out uint tick)
{
var reader = new PacketReader(source);
Expect(ref reader, MessageType.ServerSnapshot);
tick = reader.ReadUInt32();
var count = reader.ReadUInt16();
if (count > destination.Length)
{
throw new ProtocolException($"Snapshot holds {count} entities, destination fits {destination.Length}.");
}
for (var i = 0; i < count; i++)
{
destination[i] = new EntitySnapshot(
reader.ReadUInt32(),
(EntityKind)reader.ReadByte(),
reader.ReadSingle(),
reader.ReadSingle(),
reader.ReadSingle(),
reader.ReadUInt32());
}
return count;
}
private static void Expect(ref PacketReader reader, MessageType expected)
{
var actual = reader.ReadMessageType();
if (actual != expected)
{
throw new ProtocolException($"Expected {expected} (0x{(byte)expected:X2}) but got 0x{(byte)actual:X2}.");
}
}
}