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.
This commit is contained in:
@@ -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}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user