Initial commit: Client Doc docs Server Tools
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7211accc68f561241b9ec9736c8b4398
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace XWorld.Framework
|
||||
{
|
||||
// 服务端权威随机 / 客户端用服务端下发的种子复现
|
||||
public interface IRandom
|
||||
{
|
||||
int Next(int maxExclusive); // 返回 [0, maxExclusive)
|
||||
}
|
||||
|
||||
public interface ITimer
|
||||
{
|
||||
float Now { get; } // 单调递增的秒
|
||||
}
|
||||
|
||||
public interface ILogger
|
||||
{
|
||||
void Info(string message);
|
||||
void Warn(string message);
|
||||
void Error(string message);
|
||||
}
|
||||
|
||||
// 仅服务端注入;客户端 ctx 不提供
|
||||
public interface IStorage
|
||||
{
|
||||
byte[] Load(string key); // 不存在返回 null
|
||||
void Save(string key, byte[] data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ffc5726043101c4a9f62b706d5f8c22
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
|
||||
namespace XWorld.Framework
|
||||
{
|
||||
// SplitMix64:跨平台确定性、无浮点、状态即种子(便于快照/复盘)
|
||||
public sealed class DeterministicRandom : IRandom
|
||||
{
|
||||
private ulong _state;
|
||||
|
||||
public DeterministicRandom(ulong seed)
|
||||
{
|
||||
_state = seed;
|
||||
}
|
||||
|
||||
// 当前内部状态;用它构造新实例可从"下一步"继续同一序列
|
||||
public ulong State => _state;
|
||||
|
||||
private ulong NextU64()
|
||||
{
|
||||
_state += 0x9E3779B97F4A7C15UL;
|
||||
ulong z = _state;
|
||||
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9UL;
|
||||
z = (z ^ (z >> 27)) * 0x94D049BB133111EBUL;
|
||||
return z ^ (z >> 31);
|
||||
}
|
||||
|
||||
public int Next(int maxExclusive)
|
||||
{
|
||||
if (maxExclusive <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(maxExclusive), "maxExclusive 必须为正");
|
||||
|
||||
ulong bound = (ulong)maxExclusive;
|
||||
// 拒绝采样,消除取模偏差
|
||||
ulong threshold = (ulong.MaxValue - bound + 1) % bound;
|
||||
while (true)
|
||||
{
|
||||
ulong r = NextU64();
|
||||
if (r >= threshold) return (int)(r % bound);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6db124033cabe1e438951dabd03ffcc4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "XWorld.Framework.Shared",
|
||||
"rootNamespace": "XWorld.Framework",
|
||||
"references": [],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": true
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 516480831de1cb14cbc3988963329ffa
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace XWorld.Framework
|
||||
{
|
||||
// 框架版本(常驻框架的版本号)。小游戏 game.json 的 minFrameworkVersion 大于此值则拒绝加载(设计 §6)。
|
||||
public static class FrameworkInfo
|
||||
{
|
||||
public const int Version = 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 01f797754c3653f4b92536caa0421eb9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace XWorld.Framework
|
||||
{
|
||||
public sealed class PlayerInfo
|
||||
{
|
||||
public int PlayerId;
|
||||
public string Name;
|
||||
public bool IsAI;
|
||||
}
|
||||
|
||||
public sealed class RoomConfig
|
||||
{
|
||||
public string GameId;
|
||||
public int Version;
|
||||
public ulong Seed; // 权威随机种子,两端共用以复现
|
||||
public int PlayerCount;
|
||||
public IReadOnlyDictionary<string, string> Params; // 小游戏自定义参数,可为 null
|
||||
}
|
||||
|
||||
// 房间结算结果:由小游戏逻辑在 EndRoom 时给出,经网关放入 RoomEndMsg 下发客户端。
|
||||
public sealed class RoomEndResult
|
||||
{
|
||||
public int WinnerPlayerId; // 0 = 平局/无胜者(与 RoomEndMsg 语义一致);真人 pid 必须 >0,AI 为负数
|
||||
public byte[] ResultBlob; // 小游戏自定义结算数据;放 UTF-8 文本时框架结算弹框直接显示。
|
||||
// 约定:WinnerPlayerId=0 且携带文本时,客户端标题按"平局"呈现
|
||||
}
|
||||
|
||||
// 一条小游戏业务消息(承载在帧的 Game 通道里)
|
||||
public readonly struct NetMessage
|
||||
{
|
||||
public readonly ushort Opcode;
|
||||
public readonly byte[] Payload;
|
||||
|
||||
public NetMessage(ushort opcode, byte[] payload)
|
||||
{
|
||||
Opcode = opcode;
|
||||
Payload = payload ?? Array.Empty<byte>();
|
||||
}
|
||||
}
|
||||
|
||||
// Core.Step 的纯函数式返回:新状态 + 本步事件
|
||||
public readonly struct StepResult<TState, TEvent>
|
||||
{
|
||||
public readonly TState State;
|
||||
public readonly IReadOnlyList<TEvent> Events;
|
||||
|
||||
public StepResult(TState state, IReadOnlyList<TEvent> events)
|
||||
{
|
||||
State = state;
|
||||
Events = events ?? Array.Empty<TEvent>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e8d9e263f862e64e894fd64031d374f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace XWorld.Framework
|
||||
{
|
||||
// 资源加载抽象,避免共享层引用 UnityEngine;客户端实现里再转成具体 Unity 对象。
|
||||
// 统一异步:客户端薄层按需从 CDN 下载并在 onLoaded 回调里返回对象(失败回调 null)。
|
||||
public interface IAssetLoader
|
||||
{
|
||||
void Load(string path, System.Action<object> onLoaded); // 结果由客户端薄层按需强转
|
||||
}
|
||||
|
||||
public interface IGameClientCtx
|
||||
{
|
||||
IAssetLoader Assets { get; }
|
||||
ILogger Logger { get; }
|
||||
void Send(NetMessage message); // 发往服务端(Game 通道)
|
||||
void Exit(); // 请求退出当前小游戏
|
||||
}
|
||||
|
||||
public interface IGameClient
|
||||
{
|
||||
void OnEnter(IGameClientCtx ctx);
|
||||
void OnNetMessage(NetMessage message);
|
||||
void OnUpdate(float deltaTime);
|
||||
void OnExit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ad30c72bfe7f3e4383458a7a2721442
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace XWorld.Framework
|
||||
{
|
||||
// 平台无关、纯函数式的权威逻辑;两端共用做权威计算与客户端预演
|
||||
public interface IGameLogic<TState, TInput, TEvent>
|
||||
{
|
||||
TState CreateInitial(RoomConfig config, IRandom random);
|
||||
StepResult<TState, TEvent> Step(TState state, TInput input, IRandom random);
|
||||
byte[] Encode(TState state); // 快照编解码,两端共用
|
||||
TState Decode(byte[] data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f654d94c28d42e04eade1ad4ba566d52
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace XWorld.Framework
|
||||
{
|
||||
public interface IRoomCtx
|
||||
{
|
||||
IRandom Random { get; } // 权威随机
|
||||
ITimer Timer { get; }
|
||||
ILogger Logger { get; }
|
||||
IStorage Storage { get; }
|
||||
void Broadcast(NetMessage message);
|
||||
void SendTo(int playerId, NetMessage message);
|
||||
void EndRoom(); // 由小游戏逻辑请求结束本房间(无结算结果)
|
||||
void EndRoom(RoomEndResult result); // 带结算结果结束;result 可为 null,等价无参
|
||||
}
|
||||
|
||||
public interface IGameServerRoom
|
||||
{
|
||||
void OnRoomStart(IReadOnlyList<PlayerInfo> players, IRoomCtx ctx);
|
||||
void OnMessage(int playerId, NetMessage message);
|
||||
void OnTick(float deltaTime); // 10-20Hz,内部调用 Core.Step + 广播快照
|
||||
void OnRoomEnd();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b337060f9f8da3c498d3508ce3d80664
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2cd090a33ac0fcb44b6839366ccd2375
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
Reference in New Issue
Block a user