Initial commit: Client Doc docs Server Tools

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ud18010
2026-07-10 10:24:29 +08:00
co-authored by Cursor
commit 7e35d8da31
3374 changed files with 680813 additions and 0 deletions
@@ -0,0 +1,113 @@
using System;
namespace XWorld.Framework.Protocol
{
public static class DevDiscovery
{
public const int Port = 48923;
public const uint ProtoVersion = 1;
public static readonly byte[] Magic = { 0x58, 0x57, 0x44, 0x4C };
public const byte TypeQuery = 1;
public const byte TypeReply = 2;
}
public struct DevDiscoveryQuery
{
public uint ProtoVersion;
public uint Nonce;
public string Token;
}
public struct DevDiscoveryReply
{
public uint ProtoVersion;
public uint Nonce;
public string ServerName;
public string GatewayUrl;
public string ResourceBaseUrl;
public string Token;
}
public static class DevDiscoveryCodec
{
public static byte[] EncodeQuery(DevDiscoveryQuery q)
{
var w = new PacketWriter(32);
WriteHeader(w, DevDiscovery.TypeQuery);
w.WriteVarUInt(q.ProtoVersion);
w.WriteVarUInt(q.Nonce);
w.WriteString(q.Token);
return w.ToArray();
}
public static byte[] EncodeReply(DevDiscoveryReply r)
{
var w = new PacketWriter(96);
WriteHeader(w, DevDiscovery.TypeReply);
w.WriteVarUInt(r.ProtoVersion);
w.WriteVarUInt(r.Nonce);
w.WriteString(r.ServerName);
w.WriteString(r.GatewayUrl);
w.WriteString(r.ResourceBaseUrl);
w.WriteString(r.Token);
return w.ToArray();
}
public static bool TryDecodeQuery(byte[] data, out DevDiscoveryQuery q)
{
q = default;
try
{
var r = new PacketReader(data);
if (!ReadAndCheckHeader(r, DevDiscovery.TypeQuery)) return false;
q.ProtoVersion = r.ReadVarUInt();
q.Nonce = r.ReadVarUInt();
q.Token = r.ReadString();
return true;
}
catch (FormatException)
{
return false;
}
}
public static bool TryDecodeReply(byte[] data, out DevDiscoveryReply reply)
{
reply = default;
try
{
var r = new PacketReader(data);
if (!ReadAndCheckHeader(r, DevDiscovery.TypeReply)) return false;
reply.ProtoVersion = r.ReadVarUInt();
reply.Nonce = r.ReadVarUInt();
reply.ServerName = r.ReadString();
reply.GatewayUrl = r.ReadString();
reply.ResourceBaseUrl = r.ReadString();
reply.Token = r.ReadString();
return true;
}
catch (FormatException)
{
return false;
}
}
private static void WriteHeader(PacketWriter w, byte type)
{
for (int i = 0; i < DevDiscovery.Magic.Length; i++)
{
w.WriteByte(DevDiscovery.Magic[i]);
}
w.WriteByte(type);
}
private static bool ReadAndCheckHeader(PacketReader r, byte expectedType)
{
for (int i = 0; i < DevDiscovery.Magic.Length; i++)
{
if (r.ReadByte() != DevDiscovery.Magic[i]) return false;
}
return r.ReadByte() == expectedType;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7488713e311bfdc4983b3d78c810d8f2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,47 @@
using System;
namespace XWorld.Framework.Protocol
{
public enum Channel : byte
{
Framework = 0, // 匹配/心跳/大厅等框架消息
Game = 1, // 转发给具体小游戏房间的业务消息
}
// 一帧线网消息
public readonly struct Frame
{
public readonly Channel Channel;
public readonly ushort Opcode;
public readonly byte[] Payload;
public Frame(Channel channel, ushort opcode, byte[] payload)
{
Channel = channel;
Opcode = opcode;
Payload = payload ?? Array.Empty<byte>();
}
}
public static class FrameCodec
{
// 线网: [channel:u8][opcode:u16 LE][payloadLen:varuint][payload]
public static byte[] Encode(Frame frame)
{
var w = new PacketWriter(frame.Payload.Length + 8);
w.WriteByte((byte)frame.Channel);
w.WriteUInt16(frame.Opcode);
w.WriteBytes(frame.Payload); // 自带 varuint 长度前缀
return w.ToArray();
}
public static Frame Decode(byte[] data)
{
var r = new PacketReader(data);
var channel = (Channel)r.ReadByte();
ushort opcode = r.ReadUInt16();
byte[] payload = r.ReadBytes();
return new Frame(channel, opcode, payload);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5a1a38b3c8fc8f640958b68fc7d1ca14
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,384 @@
using System.Collections.Generic;
using XWorld.Framework;
namespace XWorld.Framework.Protocol
{
// 框架通道(Channel.Framework)的 opcode;数值是线网契约,禁止随意改动
public enum FrameworkOpcode : ushort
{
Heartbeat = 1,
MatchRequest = 2,
MatchFound = 3,
GameInput = 4,
Snapshot = 5,
RoomEnd = 6,
Error = 7,
GameListRequest = 8,
GameListResponse = 9,
RandomMatchRequest = 10,
MatchAssigned = 11,
LobbyJoin = 12,
LobbyState = 13,
LobbyMove = 14,
LobbyLeave = 15,
}
public sealed class HeartbeatMsg
{
public long ClientTimeMs;
public byte[] Encode()
{
var w = new PacketWriter();
w.WriteVarLong(ClientTimeMs);
return w.ToArray();
}
public static HeartbeatMsg Decode(byte[] data)
{
var r = new PacketReader(data);
return new HeartbeatMsg { ClientTimeMs = r.ReadVarLong() };
}
}
public sealed class MatchRequestMsg
{
public string GameId; // null 编码为空字符串
public int Version;
public byte[] Encode()
{
var w = new PacketWriter();
w.WriteString(GameId);
w.WriteVarInt(Version);
return w.ToArray();
}
public static MatchRequestMsg Decode(byte[] data)
{
var r = new PacketReader(data);
return new MatchRequestMsg { GameId = r.ReadString(), Version = r.ReadVarInt() };
}
}
public sealed class MatchFoundMsg
{
public string RoomId; // null 编码为空字符串
public int Version;
public int SelfPlayerId;
public List<PlayerInfo> Players = new List<PlayerInfo>();
public byte[] Encode()
{
var w = new PacketWriter();
w.WriteString(RoomId);
w.WriteVarInt(Version);
w.WriteVarInt(SelfPlayerId);
w.WriteVarUInt((uint)Players.Count);
foreach (var p in Players)
{
w.WriteVarInt(p.PlayerId);
w.WriteString(p.Name);
w.WriteBool(p.IsAI);
}
return w.ToArray();
}
public static MatchFoundMsg Decode(byte[] data)
{
var r = new PacketReader(data);
var m = new MatchFoundMsg
{
RoomId = r.ReadString(),
Version = r.ReadVarInt(),
SelfPlayerId = r.ReadVarInt(),
};
uint n = r.ReadVarUInt();
for (uint i = 0; i < n; i++)
{
m.Players.Add(new PlayerInfo
{
PlayerId = r.ReadVarInt(),
Name = r.ReadString(),
IsAI = r.ReadBool(),
});
}
return m;
}
}
public sealed class GameInfoMsg
{
public string GameId;
public int Version;
public string DisplayName;
public int MinPlayers;
public int MaxPlayers;
}
public sealed class GameListResponseMsg
{
public List<GameInfoMsg> Games = new List<GameInfoMsg>();
public byte[] Encode()
{
var w = new PacketWriter();
w.WriteVarUInt((uint)Games.Count);
foreach (var g in Games)
{
w.WriteString(g.GameId);
w.WriteVarInt(g.Version);
w.WriteString(g.DisplayName);
w.WriteVarInt(g.MinPlayers);
w.WriteVarInt(g.MaxPlayers);
}
return w.ToArray();
}
public static GameListResponseMsg Decode(byte[] data)
{
var r = new PacketReader(data);
var msg = new GameListResponseMsg();
uint n = r.ReadVarUInt();
for (uint i = 0; i < n; i++)
{
msg.Games.Add(new GameInfoMsg
{
GameId = r.ReadString(),
Version = r.ReadVarInt(),
DisplayName = r.ReadString(),
MinPlayers = r.ReadVarInt(),
MaxPlayers = r.ReadVarInt(),
});
}
return msg;
}
}
public sealed class MatchAssignedMsg
{
public string GameId;
public int Version;
public byte[] Encode()
{
var w = new PacketWriter();
w.WriteString(GameId);
w.WriteVarInt(Version);
return w.ToArray();
}
public static MatchAssignedMsg Decode(byte[] data)
{
var r = new PacketReader(data);
return new MatchAssignedMsg { GameId = r.ReadString(), Version = r.ReadVarInt() };
}
}
// 把一条小游戏业务输入封进框架通道(也可直接走 Channel.Game,二者择一,详见服务端子项目)
public sealed class GameInputMsg
{
public ushort GameOpcode;
public byte[] Payload; // null 与空数组线网等价,Decode 恒返回非 null
public byte[] Encode()
{
var w = new PacketWriter();
w.WriteUInt16(GameOpcode);
w.WriteBytes(Payload);
return w.ToArray();
}
public static GameInputMsg Decode(byte[] data)
{
var r = new PacketReader(data);
return new GameInputMsg { GameOpcode = r.ReadUInt16(), Payload = r.ReadBytes() };
}
}
public sealed class SnapshotMsg
{
public int Tick;
public byte[] StateBlob; // 由小游戏 Core.Encode(state) 产出,框架不解释内容;同上:null 与空等价
public byte[] Encode()
{
var w = new PacketWriter();
w.WriteVarInt(Tick);
w.WriteBytes(StateBlob);
return w.ToArray();
}
public static SnapshotMsg Decode(byte[] data)
{
var r = new PacketReader(data);
return new SnapshotMsg { Tick = r.ReadVarInt(), StateBlob = r.ReadBytes() };
}
}
public sealed class RoomEndMsg
{
public int WinnerPlayerId; // 0 表示平局/无胜者
public byte[] ResultBlob; // 小游戏自定义结算数据;同上:null 与空等价
public byte[] Encode()
{
var w = new PacketWriter();
w.WriteVarInt(WinnerPlayerId);
w.WriteBytes(ResultBlob);
return w.ToArray();
}
public static RoomEndMsg Decode(byte[] data)
{
var r = new PacketReader(data);
return new RoomEndMsg { WinnerPlayerId = r.ReadVarInt(), ResultBlob = r.ReadBytes() };
}
}
public sealed class ErrorMsg
{
public int Code;
public string Message; // null 编码为空字符串
public byte[] Encode()
{
var w = new PacketWriter();
w.WriteVarInt(Code);
w.WriteString(Message);
return w.ToArray();
}
public static ErrorMsg Decode(byte[] data)
{
var r = new PacketReader(data);
return new ErrorMsg { Code = r.ReadVarInt(), Message = r.ReadString() };
}
}
public sealed class LobbyJoinMsg
{
public string Name;
public int Figure;
public byte[] Encode()
{
var w = new PacketWriter();
w.WriteString(Name);
w.WriteVarInt(Figure);
return w.ToArray();
}
public static LobbyJoinMsg Decode(byte[] data)
{
var r = new PacketReader(data);
return new LobbyJoinMsg { Name = r.ReadString(), Figure = r.ReadVarInt() };
}
}
public sealed class LobbyPlayerMsg
{
public int PlayerId;
public string Name;
public int Figure;
public float X;
public float Y;
public float Z;
public float DirX;
public float DirZ;
public bool Moving;
public void Encode(PacketWriter w)
{
w.WriteVarInt(PlayerId);
w.WriteString(Name);
w.WriteVarInt(Figure);
w.WriteSingle(X);
w.WriteSingle(Y);
w.WriteSingle(Z);
w.WriteSingle(DirX);
w.WriteSingle(DirZ);
w.WriteBool(Moving);
}
public static LobbyPlayerMsg Decode(PacketReader r)
{
return new LobbyPlayerMsg
{
PlayerId = r.ReadVarInt(),
Name = r.ReadString(),
Figure = r.ReadVarInt(),
X = r.ReadSingle(),
Y = r.ReadSingle(),
Z = r.ReadSingle(),
DirX = r.ReadSingle(),
DirZ = r.ReadSingle(),
Moving = r.ReadBool(),
};
}
}
public sealed class LobbyStateMsg
{
public List<LobbyPlayerMsg> Players = new List<LobbyPlayerMsg>();
public byte[] Encode()
{
var w = new PacketWriter();
w.WriteVarUInt((uint)Players.Count);
foreach (var p in Players) p.Encode(w);
return w.ToArray();
}
public static LobbyStateMsg Decode(byte[] data)
{
var r = new PacketReader(data);
var m = new LobbyStateMsg();
uint n = r.ReadVarUInt();
for (uint i = 0; i < n; i++)
{
m.Players.Add(LobbyPlayerMsg.Decode(r));
}
return m;
}
}
public sealed class LobbyMoveMsg
{
public int PlayerId;
public float X;
public float Y;
public float Z;
public float DirX;
public float DirZ;
public bool Moving;
public byte[] Encode()
{
var w = new PacketWriter();
w.WriteVarInt(PlayerId);
w.WriteSingle(X);
w.WriteSingle(Y);
w.WriteSingle(Z);
w.WriteSingle(DirX);
w.WriteSingle(DirZ);
w.WriteBool(Moving);
return w.ToArray();
}
public static LobbyMoveMsg Decode(byte[] data)
{
var r = new PacketReader(data);
return new LobbyMoveMsg
{
PlayerId = r.ReadVarInt(),
X = r.ReadSingle(),
Y = r.ReadSingle(),
Z = r.ReadSingle(),
DirX = r.ReadSingle(),
DirZ = r.ReadSingle(),
Moving = r.ReadBool(),
};
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c41cd94f44b3ac94e9358a667b0855e0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,107 @@
using System;
using System.Text;
namespace XWorld.Framework.Protocol
{
// 与 PacketWriter 对偶;任何越界/畸形长度抛 FormatException(容错入口统一)
public sealed class PacketReader
{
private readonly byte[] _buf;
private int _pos;
public PacketReader(byte[] data)
{
_buf = data ?? Array.Empty<byte>();
_pos = 0;
}
public int Position => _pos;
public bool HasMore => _pos < _buf.Length;
public byte ReadByte()
{
if (_pos >= _buf.Length) throw new FormatException("PacketReader: read past end");
return _buf[_pos++];
}
public bool ReadBool() => ReadByte() != 0;
public ushort ReadUInt16()
{
byte lo = ReadByte();
byte hi = ReadByte();
return (ushort)(lo | (hi << 8));
}
public uint ReadVarUInt()
{
uint result = 0;
int shift = 0;
while (true)
{
if (shift > 28) throw new FormatException("PacketReader: varuint too long");
byte b = ReadByte();
// 第 5 字节(shift==28)只有低 4 位有效,超出即为越界 varuint
if (shift == 28 && (b & 0x70) != 0)
throw new FormatException("PacketReader: varuint overflow");
result |= (uint)(b & 0x7F) << shift;
if ((b & 0x80) == 0) break;
shift += 7;
}
return result;
}
public int ReadVarInt()
{
uint zig = ReadVarUInt();
return (int)(zig >> 1) ^ -(int)(zig & 1); // zigzag decode
}
public long ReadVarLong()
{
ulong result = 0;
int shift = 0;
while (true)
{
if (shift > 63) throw new FormatException("PacketReader: varlong too long");
byte b = ReadByte();
// 第 10 字节(shift==63)只有最低 1 位有效,超出即为越界 varlong
if (shift == 63 && (b & 0x7E) != 0)
throw new FormatException("PacketReader: varlong overflow");
result |= (ulong)(b & 0x7F) << shift;
if ((b & 0x80) == 0) break;
shift += 7;
}
return (long)(result >> 1) ^ -(long)(result & 1);
}
public float ReadSingle()
{
byte[] b = new byte[4];
b[0] = ReadByte(); b[1] = ReadByte(); b[2] = ReadByte(); b[3] = ReadByte();
if (!BitConverter.IsLittleEndian) Array.Reverse(b);
return BitConverter.ToSingle(b, 0);
}
public byte[] ReadBytes()
{
uint len = ReadVarUInt();
if (len == 0) return Array.Empty<byte>();
if (_pos + len > _buf.Length) throw new FormatException("PacketReader: bytes length exceeds buffer");
byte[] outv = new byte[len];
Array.Copy(_buf, _pos, outv, 0, (int)len);
_pos += (int)len;
return outv;
}
public string ReadString()
{
uint len = ReadVarUInt();
if (len == 0) return string.Empty;
if (_pos + len > _buf.Length) throw new FormatException("PacketReader: string length exceeds buffer");
string s = Encoding.UTF8.GetString(_buf, _pos, (int)len);
_pos += (int)len;
return s;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4f2e9f22b4b01f947897c0ab68e8e0c2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace XWorld.Framework.Protocol
{
// 手写小端二进制写入器:varint(LEB128) + zigzag,无反射,AOT/WASM 友好
public sealed class PacketWriter
{
private readonly List<byte> _buf;
public PacketWriter(int capacity = 64)
{
_buf = new List<byte>(capacity);
}
public void WriteByte(byte v) => _buf.Add(v);
public void WriteBool(bool v) => _buf.Add(v ? (byte)1 : (byte)0);
public void WriteUInt16(ushort v)
{
_buf.Add((byte)(v & 0xFF));
_buf.Add((byte)((v >> 8) & 0xFF));
}
public void WriteVarUInt(uint v)
{
while (v >= 0x80)
{
_buf.Add((byte)(v | 0x80));
v >>= 7;
}
_buf.Add((byte)v);
}
public void WriteVarInt(int v)
{
uint zig = (uint)((v << 1) ^ (v >> 31)); // zigzag
WriteVarUInt(zig);
}
public void WriteVarLong(long v)
{
ulong zig = (ulong)((v << 1) ^ (v >> 63));
while (zig >= 0x80)
{
_buf.Add((byte)(zig | 0x80));
zig >>= 7;
}
_buf.Add((byte)zig);
}
public void WriteSingle(float v)
{
byte[] b = BitConverter.GetBytes(v);
if (!BitConverter.IsLittleEndian) Array.Reverse(b);
_buf.Add(b[0]); _buf.Add(b[1]); _buf.Add(b[2]); _buf.Add(b[3]);
}
public void WriteBytes(byte[] data)
{
if (data == null || data.Length == 0)
{
WriteVarUInt(0);
return;
}
WriteVarUInt((uint)data.Length);
_buf.AddRange(data);
}
public void WriteString(string s)
{
if (string.IsNullOrEmpty(s))
{
WriteVarUInt(0);
return;
}
byte[] b = Encoding.UTF8.GetBytes(s);
WriteVarUInt((uint)b.Length);
_buf.AddRange(b);
}
public byte[] ToArray() => _buf.ToArray();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9721803ff97146841adf382189af8c70
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: