using System.Buffers.Binary;
using System.Collections;
using System.Text;
namespace CampusNet.Protocol;
///
/// 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.
///
public sealed record CustomValue(byte Code, byte[] Data);
/// An array the client sent with an explicit element type ('y'): re-emitted the same way.
public sealed record TypedArray(byte ElemType, object?[] Items, byte CustomCode = 0);
/// A Dictionary sent with typed keys/values ('D'); key/value type 0 = per-entry typed.
public sealed class TypedDictionary
{
public byte KeyType, ValueType;
public readonly List> Entries = new();
}
///
/// 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).
///
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 _b;
public int Pos;
public Reader(ReadOnlySpan 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 Params()
{
int n = I16(); var d = new Dictionary(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 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 Params = new(); }
public sealed class EventData { public byte Code; public Dictionary Params = new(); }
public static OperationRequest ReadOperationRequest(ReadOnlySpan body)
{
var r = new Reader(body);
var op = new OperationRequest { Code = r.U8() };
op.Params = r.Params();
return op;
}
public static OperationResponse ReadOperationResponse(ReadOnlySpan 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 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 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 Span => _b.AsSpan(0, Length);
/// Write a typed value (type byte + data).
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 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 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