Files
2026-10-14 06:59:51 +02:00

290 lines
14 KiB
C#

using System.Buffers.Binary;
using System.Collections;
using System.Text;
namespace CampusNet.Protocol;
/// <summary>
/// A value the client sent as a custom type ('c'): PUN registers Vector3/Quaternion/Vector2/Player
/// etc. under one-letter codes. The server never interprets them - it stores and re-emits the bytes.
/// </summary>
public sealed record CustomValue(byte Code, byte[] Data);
/// <summary>An array the client sent with an explicit element type ('y'): re-emitted the same way.</summary>
public sealed record TypedArray(byte ElemType, object?[] Items, byte CustomCode = 0);
/// <summary>A Dictionary sent with typed keys/values ('D'); key/value type 0 = per-entry typed.</summary>
public sealed class TypedDictionary
{
public byte KeyType, ValueType;
public readonly List<KeyValuePair<object?, object?>> Entries = new();
}
/// <summary>
/// Photon GpBinary 1.6 - the wire format PUN uses on every Realtime connection (verified from a live
/// capture, see docs/photon-wire-findings.md). Big-endian throughout. Hashtables are the workhorse:
/// keys are typed values (bytes for system props, strings for custom props).
/// </summary>
public static class GpBinary16
{
public const byte Magic = 0xF3;
// Message types (the byte after 0xF3; bit 0x80 = encrypted body).
public const byte MsgInit = 0, MsgInitResponse = 1, MsgOperationRequest = 2, MsgOperationResponse = 3,
MsgEvent = 4, MsgInternalOperationRequest = 6, MsgInternalOperationResponse = 7,
MsgMessage = 8, MsgRawMessage = 9;
public const byte EncryptedFlag = 0x80;
// ---------------------------------------------------------------- reading
public ref struct Reader
{
private readonly ReadOnlySpan<byte> _b;
public int Pos;
public Reader(ReadOnlySpan<byte> b, int pos = 0) { _b = b; Pos = pos; }
public int Remaining => _b.Length - Pos;
public byte U8() => _b[Pos++];
public short I16() { var v = BinaryPrimitives.ReadInt16BigEndian(_b.Slice(Pos)); Pos += 2; return v; }
public int I32() { var v = BinaryPrimitives.ReadInt32BigEndian(_b.Slice(Pos)); Pos += 4; return v; }
public long I64() { var v = BinaryPrimitives.ReadInt64BigEndian(_b.Slice(Pos)); Pos += 8; return v; }
public float F32() { var v = BinaryPrimitives.ReadSingleBigEndian(_b.Slice(Pos)); Pos += 4; return v; }
public double F64() { var v = BinaryPrimitives.ReadDoubleBigEndian(_b.Slice(Pos)); Pos += 8; return v; }
public byte[] Bytes(int n) { var v = _b.Slice(Pos, n).ToArray(); Pos += n; return v; }
public string Str() { int n = I16(); var s = Encoding.UTF8.GetString(_b.Slice(Pos, n)); Pos += n; return s; }
public object? Value() => Value(U8());
public object? Value(byte t)
{
switch ((char)t)
{
case '*': return null;
case 'b': return U8();
case 'o': return U8() != 0;
case 'k': return I16();
case 'i': return I32();
case 'l': return I64();
case 'f': return F32();
case 'd': return F64();
case 's': return Str();
case 'x': { int n = I32(); return Bytes(n); }
case 'n': { int n = I32(); var a = new int[n]; for (int i = 0; i < n; i++) a[i] = I32(); return a; }
case 'a': { int n = I16(); var a = new string[n]; for (int i = 0; i < n; i++) a[i] = Str(); return a; }
case 'h':
{
int n = I16(); var h = new Hashtable(n);
for (int i = 0; i < n; i++) { var k = Value(); var v = Value(); if (k != null) h[k] = v; }
return h;
}
case 'D':
{
var d = new TypedDictionary { KeyType = U8(), ValueType = U8() };
int n = I16();
for (int i = 0; i < n; i++)
{
var k = d.KeyType == 0 ? Value() : Value(d.KeyType);
var v = d.ValueType == 0 ? Value() : Value(d.ValueType);
d.Entries.Add(new(k, v));
}
return d;
}
case 'z': { int n = I16(); var a = new object?[n]; for (int i = 0; i < n; i++) a[i] = Value(); return a; }
case 'y':
{
int n = I16(); byte et = U8();
if ((char)et == 'c')
{
byte code = U8(); var items = new object?[n];
for (int i = 0; i < n; i++) { int len = I16(); items[i] = new CustomValue(code, Bytes(len)); }
return new TypedArray(et, items, code);
}
var arr = new object?[n];
for (int i = 0; i < n; i++) arr[i] = Value(et);
return new TypedArray(et, arr);
}
case 'c': { byte code = U8(); int len = I16(); return new CustomValue(code, Bytes(len)); }
default: throw new InvalidDataException($"GpBinary16: unknown type 0x{t:x2} at {Pos - 1}");
}
}
public Dictionary<byte, object?> Params()
{
int n = I16(); var d = new Dictionary<byte, object?>(n);
for (int i = 0; i < n; i++) { byte k = U8(); d[k] = Value(); }
return d;
}
}
public sealed class OperationRequest { public byte Code; public Dictionary<byte, object?> Params = new(); public int PayloadBytes; /* wire size of the whole message, for budgets */ }
public sealed class OperationResponse { public byte Code; public short ReturnCode; public string? Debug; public Dictionary<byte, object?> Params = new(); }
public sealed class EventData { public byte Code; public Dictionary<byte, object?> Params = new(); }
public static OperationRequest ReadOperationRequest(ReadOnlySpan<byte> body)
{
var r = new Reader(body);
var op = new OperationRequest { Code = r.U8() };
op.Params = r.Params();
return op;
}
public static OperationResponse ReadOperationResponse(ReadOnlySpan<byte> body)
{
var r = new Reader(body);
var op = new OperationResponse { Code = r.U8(), ReturnCode = r.I16() };
op.Debug = r.Value() as string;
op.Params = r.Params();
return op;
}
public static EventData ReadEvent(ReadOnlySpan<byte> body)
{
var r = new Reader(body);
var ev = new EventData { Code = r.U8() };
ev.Params = r.Params();
return ev;
}
// ---------------------------------------------------------------- writing
public sealed class Writer
{
private byte[] _b = new byte[512];
public int Length;
public Writer(int cap = 512) { _b = new byte[cap]; }
private void Ensure(int n) { if (Length + n > _b.Length) Array.Resize(ref _b, Math.Max(_b.Length * 2, Length + n)); }
public void U8(byte v) { Ensure(1); _b[Length++] = v; }
public void I16(short v) { Ensure(2); BinaryPrimitives.WriteInt16BigEndian(_b.AsSpan(Length), v); Length += 2; }
public void I32(int v) { Ensure(4); BinaryPrimitives.WriteInt32BigEndian(_b.AsSpan(Length), v); Length += 4; }
public void I64(long v) { Ensure(8); BinaryPrimitives.WriteInt64BigEndian(_b.AsSpan(Length), v); Length += 8; }
public void F32(float v) { Ensure(4); BinaryPrimitives.WriteSingleBigEndian(_b.AsSpan(Length), v); Length += 4; }
public void F64(double v) { Ensure(8); BinaryPrimitives.WriteDoubleBigEndian(_b.AsSpan(Length), v); Length += 8; }
public void Raw(ReadOnlySpan<byte> s) { Ensure(s.Length); s.CopyTo(_b.AsSpan(Length)); Length += s.Length; }
public void Str(string s) { var n = Encoding.UTF8.GetByteCount(s); I16((short)n); Ensure(n); Encoding.UTF8.GetBytes(s, _b.AsSpan(Length)); Length += n; }
public byte[] ToArray() => _b.AsSpan(0, Length).ToArray();
public ReadOnlySpan<byte> Span => _b.AsSpan(0, Length);
/// <summary>Write a typed value (type byte + data).</summary>
public void Value(object? v)
{
switch (v)
{
case null: U8((byte)'*'); break;
case byte b: U8((byte)'b'); U8(b); break;
case bool o: U8((byte)'o'); U8(o ? (byte)1 : (byte)0); break;
case short k: U8((byte)'k'); I16(k); break;
case int i: U8((byte)'i'); I32(i); break;
case long l: U8((byte)'l'); I64(l); break;
case float f: U8((byte)'f'); F32(f); break;
case double d: U8((byte)'d'); F64(d); break;
case string s: U8((byte)'s'); Str(s); break;
case byte[] x: U8((byte)'x'); I32(x.Length); Raw(x); break;
case int[] n: U8((byte)'n'); I32(n.Length); foreach (var e in n) I32(e); break;
case string[] a: U8((byte)'a'); I16((short)a.Length); foreach (var e in a) Str(e); break;
case Hashtable h:
U8((byte)'h'); I16((short)h.Count);
foreach (DictionaryEntry e in h) { Value(e.Key); Value(e.Value); }
break;
case TypedDictionary td:
U8((byte)'D'); U8(td.KeyType); U8(td.ValueType); I16((short)td.Entries.Count);
foreach (var e in td.Entries)
{
if (td.KeyType == 0) Value(e.Key); else ValueBody(e.Key, td.KeyType);
if (td.ValueType == 0) Value(e.Value); else ValueBody(e.Value, td.ValueType);
}
break;
case TypedArray ta:
U8((byte)'y'); I16((short)ta.Items.Length); U8(ta.ElemType);
if ((char)ta.ElemType == 'c')
{
U8(ta.CustomCode);
foreach (var e in ta.Items) { var c = (CustomValue)e!; I16((short)c.Data.Length); Raw(c.Data); }
}
else foreach (var e in ta.Items) ValueBody(e, ta.ElemType);
break;
case object?[] z: U8((byte)'z'); I16((short)z.Length); foreach (var e in z) Value(e); break;
case CustomValue c: U8((byte)'c'); U8(c.Code); I16((short)c.Data.Length); Raw(c.Data); break;
case Dictionary<byte, object?> dbo: // convenience: byte-keyed dictionary as Hashtable
U8((byte)'h'); I16((short)dbo.Count);
foreach (var e in dbo) { Value(e.Key); Value(e.Value); }
break;
case Dictionary<int, object?> dio: // actorNr -> props as Hashtable with int keys
U8((byte)'h'); I16((short)dio.Count);
foreach (var e in dio) { Value(e.Key); Value(e.Value); }
break;
case Dictionary<object, object?> doo:
U8((byte)'h'); I16((short)doo.Count);
foreach (var e in doo) { Value(e.Key); Value(e.Value); }
break;
default: throw new NotSupportedException($"GpBinary16: cannot serialize {v.GetType()}");
}
}
/// <summary>Write only the data of a value whose type byte was already written (typed arrays/dictionaries).</summary>
private void ValueBody(object? v, byte t)
{
// Re-use Value() and drop the leading type byte it wrote.
int start = Length;
Value(v);
if (_b[start] != t)
{
// Element type mismatch (e.g. int in a 'k' array) - fix up the type byte position.
throw new InvalidDataException($"GpBinary16: element type {(char)_b[start]} does not match array type {(char)t}");
}
Buffer.BlockCopy(_b, start + 1, _b, start, Length - start - 1);
Length--;
}
public void Params(IReadOnlyDictionary<byte, object?> p)
{
I16((short)p.Count);
foreach (var e in p) { U8(e.Key); Value(e.Value); }
}
}
public static byte[] OperationResponseMessage(byte opCode, short returnCode, string? debug, IReadOnlyDictionary<byte, object?> p, bool internalOp = false)
{
var w = new Writer();
w.U8(Magic); w.U8(internalOp ? MsgInternalOperationResponse : MsgOperationResponse);
w.U8(opCode); w.I16(returnCode); w.Value(debug); w.Params(p);
return w.ToArray();
}
public static byte[] EventMessage(byte eventCode, IReadOnlyDictionary<byte, object?> p)
{
var w = new Writer();
w.U8(Magic); w.U8(MsgEvent);
w.U8(eventCode); w.Params(p);
return w.ToArray();
}
/// <summary>Body (after the 0xF3 + type header) of an operation response - what gets encrypted when a reply must be.</summary>
public static byte[] OperationResponseBody(byte opCode, short returnCode, string? debug, IReadOnlyDictionary<byte, object?> p)
{
var w = new Writer();
w.U8(opCode); w.I16(returnCode); w.Value(debug); w.Params(p);
return w.ToArray();
}
public static byte[] InitResponse() => new byte[] { Magic, MsgInitResponse, 0 };
public static string Describe(object? v, int depth = 0)
{
switch (v)
{
case null: return "null";
case string s: return $"\"{s}\"";
case byte[] x: return $"byte[{x.Length}]";
case int[] n: return "[" + string.Join(",", n) + "]";
case string[] a: return "[" + string.Join(",", a.Select(s => "\"" + s + "\"")) + "]";
case Hashtable h:
if (depth > 2) return $"{{…{h.Count}}}";
return "{" + string.Join(", ", h.Cast<DictionaryEntry>().Select(e => Describe(e.Key, depth + 1) + ":" + Describe(e.Value, depth + 1))) + "}";
case TypedDictionary d: return $"D[{d.Entries.Count}]";
case TypedArray ta: return $"{(char)ta.ElemType}[{ta.Items.Length}]";
case object?[] z: return "(" + string.Join(", ", z.Select(e => Describe(e, depth + 1))) + ")";
case CustomValue c: return $"custom'{(char)c.Code}'[{c.Data.Length}]";
default: return v.ToString() ?? "?";
}
}
}