using System.Buffers.Binary; using System.Text.Json; using System.Collections.ObjectModel; namespace ICRebuild.Core; public enum PacketDirection { Up, Down } public sealed record PacketRecord(long Sequence, long MonotonicTicks, DateTimeOffset Time, PacketDirection Direction, int Opcode, string GameVersion, uint? Source, uint? Target, byte[] Payload, bool PayloadComplete = true,string? CaptureIssue = null, PacketContext? Context = null); public sealed record OpcodeSnapshot(string GameVersion, string Source, IReadOnlyDictionary Up, IReadOnlyDictionary Down); public sealed class OpcodeMap { private OpcodeSnapshot snapshot = new("unknown","none",new ReadOnlyDictionary(new Dictionary()),new ReadOnlyDictionary(new Dictionary())); public OpcodeSnapshot Snapshot => Volatile.Read(ref snapshot); public string? Resolve(string version, PacketDirection direction, int opcode) { var state = Snapshot; return version != "unknown" && state.GameVersion == version ? (direction == PacketDirection.Up ? state.Up : state.Down).GetValueOrDefault(opcode) : null; } public int? ResolveOpcode(string version, PacketDirection direction, string name) { var state = Snapshot; if (version == "unknown" || state.GameVersion != version || string.IsNullOrWhiteSpace(name)) return null; var dict = direction == PacketDirection.Up ? state.Up : state.Down; int? found = null; foreach (var kv in dict) { if (kv.Value != name) continue; if (found.HasValue) return null; // Refuse ambiguous multi-opcode mappings. found = kv.Key; } return found; } public void Load(string json, string version, string source) { if (string.IsNullOrWhiteSpace(version) || version == "unknown") throw new InvalidDataException("客户端版本未知,不能应用映射"); using var document = JsonDocument.Parse(json,new JsonDocumentOptions { MaxDepth = 12 }); if (!document.RootElement.TryGetProperty(version,out var entries) || entries.ValueKind != JsonValueKind.Object) throw new InvalidDataException($"映射不包含版本 {version}"); Dictionary up = [], down = []; foreach (var property in entries.EnumerateObject()) { Dictionary target; if (property.Name.StartsWith("UP_",StringComparison.Ordinal)) target = up; else if (property.Name.StartsWith("DOWN_",StringComparison.Ordinal)) target = down; else continue; if (property.Value.ValueKind != JsonValueKind.Array) throw new InvalidDataException("Opcode 值应为数组"); foreach (var item in property.Value.EnumerateArray()) { if (!item.TryGetInt32(out var code) || code is < 0 or > 65535) throw new InvalidDataException("Opcode 超出 ushort 范围"); if (target.TryGetValue(code,out var old) && old != property.Name) throw new InvalidDataException($"Opcode {code} 存在冲突"); target[code] = property.Name; } } if (up.Count + down.Count == 0) throw new InvalidDataException("映射为空"); var additions=CurrentProtocolEvidence.AddMissing(version,down); if(additions>0) source+=$" + 本机核实 {additions} 项"; Volatile.Write(ref snapshot,new(version,source,new ReadOnlyDictionary(up),new ReadOnlyDictionary(down))); } } public sealed record ParseResult(bool Parsed, string Summary); public static class PacketParser { public const string AuditedClientVersion="2026.10.06.0000.0000"; public const string BaselineClientVersion="2026.09.15.0000.0000"; public static bool IsAuditedVersion(string? version) => version is AuditedClientVersion or BaselineClientVersion; public static ParseResult Parse(string? name, ReadOnlySpan payload,string? gameVersion=null) { if (name == null) return new(false,"未知 Opcode;保留原始数据"); var current=IsAuditedVersion(gameVersion); if (TryEventTier(name,out var eventLabel,out var eventCapacity,out var eventLength)) return EventPacket(name,eventLabel,payload,current,eventCapacity,eventLength); switch (name) { case "UP_PingReq": var pingLength=current ? 0x20 : 0x1C; if (payload.Length != pingLength) return Invalid(name,payload.Length,pingLength); return new(true,$"Ping 请求 time={U32(payload,0)} ms reported={U32(payload,4)} ms"); case "DOWN_PingRsp": if (current ? payload.Length!=0x20 : payload.Length<0x10) return Invalid(name,payload.Length,current ? 0x20 : 0x10); return new(true,$"Ping 响应 echo={U32(payload,0)} ms field04={U32(payload,4)}(未解释);其余字节不解释"); case "UP_UpdatePositionHandler": var positionLength=current ? 0x18 : 0x28; if (payload.Length != positionLength) return Invalid(name,payload.Length,positionLength); return Position(payload,8); case "UP_UpdatePositionInstance": if (payload.Length != 0x30) return Invalid(name,payload.Length,0x30); var first = Position(payload,12); var second = Position(payload,24); return new(first.Parsed && second.Parsed,$"{first.Summary}; new {second.Summary}"); case "UP_EventStart": if (payload.Length != 0x10) return Invalid(name,payload.Length,0x10); return new(true,$"对象={U64(payload,0)} event={U32(payload,8)}"); case "UP_ActionSend": if(!current) return Unverified(name); if(payload.Length!=0x20) return Invalid(name,payload.Length,0x20); return new(true,$"动作请求 · 技能={U32(payload,0)} 类型={payload[5]} 请求序号={U16(payload,6)} 目标=0x{U64(payload,0x10):X16} 附加={U32(payload,0x18)}"); case "UP_ActionSendPos": if(!current) return Unverified(name); if(payload.Length!=0x18) return Invalid(name,payload.Length,0x18); var actionPos=Position(payload,0x0C); return actionPos.Parsed ? new(true,$"地面动作请求 · 技能={U32(payload,0)} 类型={payload[5]} 请求序号={U16(payload,6)} {actionPos.Summary}") : actionPos; case "UP_ClientTrigger": if(!current) return Unverified(name); if(payload.Length!=0x20) return Invalid(name,payload.Length,0x20); return new(true,$"客户端触发器 · 指令={U32(payload,0)} 参数=({U32(payload,4)},{U32(payload,8)},{U32(payload,12)},{U32(payload,16)}) 目标=0x{U64(payload,0x18):X16}"); case "DOWN_UpdateHate": case "DOWN_UpdateHater": if(!current) return new(false,$"{name}:此版本的仇恨列表布局未核实"); return Enmity(name,payload,name=="DOWN_UpdateHate" ? 8 : 32); case "DOWN_EffectResultBasic": if(!current) return new(false,"此版本的精简效果结果布局未核实"); if(payload.Length!=24) return Invalid(name,payload.Length,24); if(payload[0]>1) return new(false,"精简效果结果计数超出此包容量"); if(payload[0]==0) return new(true,"精简效果结果条目=0"); return new(true,$"精简效果结果条目=1 · 序列={U32(payload,4)} · 当前 HP={U32(payload,12)} · 目标序号={payload[16]};其他字段未解释"); case "DOWN_JobGauge": if(!current) return new(false,"此版本的职业量表布局未核实"); if(payload.Length!=16) return Invalid(name,payload.Length,16); return new(true,$"职业量表更新 · 职业 ID={payload[0]};各职业量表字段保留原始数据"); case "DOWN_UpdateClassInfo": if(!current) return new(false,"此版本的职业信息布局未核实"); if(payload.Length!=16) return Invalid(name,payload.Length,16); return new(true,$"职业信息 · 职业 ID={payload[0]} · 当前等级={U16(payload,2)} · 经验={U32(payload,8)};其他字段未解释"); case "DOWN_FirstAttack": if(!current) return new(false,"此版本的战斗归属布局未核实"); if(payload.Length!=16) return Invalid(name,payload.Length,16); return new(true,$"战斗归属 · 类型={payload[0]} · 标记者=0x{U64(payload,8):X16};不是技能伤害包"); case "DOWN_ActorControl": if(!current) return Unverified(name); if(payload.Length!=0x18) return Invalid(name,payload.Length,0x18); var category=U16(payload,0); var note=category==0x22 ? "(Arg0 为分派前原始值)" : ""; return new(true,$"角色控制 · 类型=0x{category:X4} 参数=({U32(payload,4)},{U32(payload,8)},{U32(payload,12)},{U32(payload,16)}){note}"); case "DOWN_ActorControlSelf": if(!current) return Unverified(name); if(payload.Length!=0x28) return Invalid(name,payload.Length,0x28); return new(true,$"自身控制 · 类型=0x{U16(payload,0):X4} 参数=({U32(payload,4)},{U32(payload,8)},{U32(payload,12)},{U32(payload,16)},{U32(payload,20)},{U32(payload,24)},{U32(payload,28)},{U32(payload,32)})"); case "DOWN_ActorControlTarget": if(!current) return Unverified(name); if(payload.Length!=0x20) return Invalid(name,payload.Length,0x20); return new(true,$"目标控制 · 类型=0x{U16(payload,0):X4} 参数=({U32(payload,4)},{U32(payload,8)},{U32(payload,12)},{U32(payload,16)}) 目标=0x{U64(payload,0x18):X16}"); case "DOWN_Effect": if(!current) return Unverified(name); if(payload.Length!=0x7C) return Invalid(name,payload.Length,0x7C); var lockTime=F32(payload,0x10); if(!float.IsFinite(lockTime) || lockTime<0) return new(false,"技能效果动画锁时间无效"); var targetCount=payload[0x21]; if(targetCount>1) return new(false,$"单目标技能效果目标计数 {targetCount} 超出容量 1"); return new(true,$"技能效果 · 序列={U32(payload,0x0C)} 请求序号={U16(payload,0x18)} 动画锁={lockTime:F2}s 类型={payload[0x1F]} 目标数={targetCount} 主目标=0x{U64(payload,0x70):X16}(动作与效果字为分派前原始值)"); case "DOWN_StatusEffectList": if(!current) return Unverified(name); if(payload.Length!=0x180) return Invalid(name,payload.Length,0x180); var activeSlots=0; for(var i=0;i<30;i++) { var slotOffset=0x14+i*12; if(!float.IsFinite(F32(payload,slotOffset+4))) return new(false,"状态列表包含非有限持续时间"); if(U16(payload,slotOffset)!=0) activeSlots++; } return new(true,$"状态列表 · 职业={payload[0]} 等级={payload[1]} HP={U32(payload,4)}/{U32(payload,8)} 资源={U16(payload,12)}/{U16(payload,14)} 护盾={payload[0x10]}% 非空槽位={activeSlots}/30(状态 ID 为分派转换前原始值)"); case "DOWN_EffectResult": if(current) { if(payload.Length!=0x60) return Invalid(name,payload.Length,0x60); int entryCount=payload[0]; if(entryCount>1) return new(false,"效果结果计数超出单条目包容量"); if(entryCount==0) return new(true,"效果结果条目=0"); int statusCount=payload[0x19]; if(statusCount>4) return new(false,"效果状态计数超限"); for(var i=0;i 16 || payload.Length < 4 + count * 0x58) return new(false,"效果结果计数超限或载荷截断"); for (var i=0;i 4) return new(false,"效果状态计数超限"); return new(true,$"效果结果条目={count};历史布局解析,须样本核验"); case "DOWN_ActorDelete": if(!current) return Unverified(name); if(payload.Length!=8) return Invalid(name,payload.Length,8); return new(true,$"对象移除 · 槽位索引={payload[0]};其余字节未由消费函数读取"); case "DOWN_ObjectSpawn": if(!current) return Unverified(name); if(payload.Length!=0x40) return Invalid(name,payload.Length,0x40); var objPos=Position(payload,0x34); return objPos.Parsed ? new(true,$"对象生成 · 槽位={payload[0]} 类型={payload[1]} BaseId={U32(payload,4)} EntityId=0x{U32(payload,8):X8} {objPos.Summary}") : objPos; case "DOWN_NpcSpawn": if(!current) return Unverified(name); if(payload.Length!=0x290) return Invalid(name,payload.Length,0x290); var npcPos=Position(payload,0x200); return npcPos.Parsed ? new(true,$"NPC 生成 · 槽位={payload[0x7E]} BaseId={U32(payload,0x40)} Owner=0x{U32(payload,0x54):X8} 等级={payload[0x86]} 职业={payload[0x87]} HP={U32(payload,0x60)}/{U32(payload,0x5C)} {npcPos.Summary}") : npcPos; default: return new(false,$"{name}:尚无已接入解析器,保留原始数据"); } } private static bool TryEventTier(string name,out string label,out int capacity,out int expectedLength) { label=""; capacity=0; expectedLength=0; ReadOnlySpan suffix; if(name.StartsWith("UP_EventFinish",StringComparison.Ordinal)) { label="事件完成"; suffix=name.AsSpan("UP_EventFinish".Length); } else if(name.StartsWith("UP_EventAction",StringComparison.Ordinal)) { label="事件动作"; suffix=name.AsSpan("UP_EventAction".Length); } else return false; if(suffix.IsEmpty) { capacity=2; expectedLength=0x10; return true; } if(!int.TryParse(suffix,out capacity)) return false; expectedLength=capacity switch { 4=>0x18, 8=>0x28, 16=>0x48, 32=>0x88, 64=>0x108, 128=>0x208, 255=>0x408, _=>0 }; return expectedLength!=0; } private static ParseResult EventPacket(string name,string label,ReadOnlySpan payload,bool current,int capacity,int expectedLength) { if(!current) return new(false,$"{name}:变长事件分档仅核实 {AuditedClientVersion},保留 {payload.Length} 字节原始数据"); if(payload.Length!=expectedLength) return Invalid(name,payload.Length,expectedLength); int argCount=payload[7]; if(argCount>capacity) return new(false,$"{name} 参数计数 {argCount} 超出分档容量 {capacity}"); var previewCount=Math.Min(argCount,4); var args=new List(previewCount); for(var i=0;ipreviewCount ? ",..." : ""; return new(true,$"{label} · event={U32(payload,0)} scene={U16(payload,4)} 状态={payload[6]} 参数数={argCount}/{capacity} [{string.Join(",",args)}{suffix}]"); } private static ushort U16(ReadOnlySpan p,int at) => BinaryPrimitives.ReadUInt16LittleEndian(p[at..]); private static uint U32(ReadOnlySpan p,int at) => BinaryPrimitives.ReadUInt32LittleEndian(p[at..]); private static ulong U64(ReadOnlySpan p,int at) => BinaryPrimitives.ReadUInt64LittleEndian(p[at..]); private static float F32(ReadOnlySpan p,int at) => BinaryPrimitives.ReadSingleLittleEndian(p[at..]); private static ParseResult Unverified(string name) => new(false,$"{name}:此版本的字段布局未核实"); private static ParseResult Enmity(string name,ReadOnlySpan payload,int capacity) { var expected=8+capacity*8; // 4-byte prefix, fixed entries, 4 trailing bytes kept uninterpreted. if(payload.Length!=expected) return Invalid(name,payload.Length,expected); var count=payload[0]; if(count>capacity) return new(false,$"仇恨列表计数 {count} 超过容量 {capacity}"); var entries=new List(count); for(var i=0;i p,int at) { float x=BinaryPrimitives.ReadSingleLittleEndian(p[at..]), y=BinaryPrimitives.ReadSingleLittleEndian(p[(at+4)..]), z=BinaryPrimitives.ReadSingleLittleEndian(p[(at+8)..]); return float.IsFinite(x)&&float.IsFinite(y)&&float.IsFinite(z) ? new(true,$"X={x:F3} Y={y:F3} Z={z:F3}") : new(false,"坐标包含非有限数"); } private static ParseResult Invalid(string name,int actual,int expected) => new(false,$"{name} 长度不匹配:{actual},预期 {expected}"); } public sealed class PacketBuffer(int capacity = 2048, int maxPayload = 65536,int maxBytes = 8*1024*1024) { private readonly object sync = new(); private readonly Queue records = new(); private long sequence; private bool paused = true; private PacketDirection? directionFilter; private int? opcodeFilter; private long dropped; private long payloadBytes; public long PayloadBytes { get { lock(sync) return payloadBytes; } } public int PayloadByteLimit => maxBytes; public bool Paused { get { lock(sync) return paused; } set { lock(sync) paused=value; } } public PacketDirection? DirectionFilter { get { lock(sync) return directionFilter; } set { lock(sync) directionFilter=value; } } public int? OpcodeFilter { get { lock(sync) return opcodeFilter; } set { lock(sync) opcodeFilter=value; } } public long Dropped { get { lock(sync) return dropped; } } public bool Accepts(PacketDirection direction,int? opcode=null) { lock(sync) return !paused && (!directionFilter.HasValue || directionFilter==direction) && (!opcode.HasValue || !opcodeFilter.HasValue || opcodeFilter==opcode); } public bool Add(PacketDirection direction,int opcode,string version,uint? source,uint? target,ReadOnlySpan payload,bool payloadComplete = true,string? captureIssue = null, PacketContext? context=null,long? monotonicTicks=null,DateTimeOffset? time=null) { lock(sync) { if (paused || (directionFilter.HasValue && directionFilter!=direction) || (opcodeFilter.HasValue && opcodeFilter!=opcode)) return false; if (payload.Length>maxPayload || payload.Length>maxBytes || opcode is <0 or >65535 || capacity <= 0 || !Enum.IsDefined(direction)) { dropped++; return false; } while (records.Count>0 && (records.Count>=capacity || payloadBytes+payload.Length>maxBytes)) { payloadBytes-=records.Dequeue().Payload.Length; dropped++; } records.Enqueue(new(++sequence,monotonicTicks ?? System.Diagnostics.Stopwatch.GetTimestamp(),time ?? DateTimeOffset.UtcNow,direction,opcode,version,source,target,payload.ToArray(),payloadComplete,captureIssue,context)); payloadBytes+=payload.Length; return true; } } public PacketRecord[] Snapshot() { lock(sync) return records.ToArray(); } public void Clear() { lock(sync) { records.Clear(); dropped=0; payloadBytes=0; } } public void Replace(IEnumerable imported) { var input=imported.ToArray(); if(capacity<=0 || maxBytes<0) throw new InvalidDataException("无效缓冲区容量"); foreach(var p in input) if(p.Payload == null || p.Payload.Length>maxPayload || p.Opcode is <0 or >65535 || !Enum.IsDefined(p.Direction) || string.IsNullOrWhiteSpace(p.GameVersion)) throw new InvalidDataException("无效记录"); lock(sync) { records.Clear(); payloadBytes=0; dropped=Math.Max(0,input.Length-capacity); foreach(var p in input.TakeLast(capacity)) { if(p.Payload.Length>maxBytes) { dropped++; continue; } while(records.Count>0 && payloadBytes+p.Payload.Length>maxBytes) { payloadBytes-=records.Dequeue().Payload.Length; dropped++; } records.Enqueue(p with { Payload=p.Payload.ToArray() }); payloadBytes+=p.Payload.Length; } sequence=input.Length==0 ? 0 : input.Max(p=>p.Sequence); } } } public sealed record PacketCorrelation(long Request,long Response,TimeSpan Elapsed); public static class PacketCorrelator { public static PacketCorrelation[] Ping(IEnumerable packets,OpcodeMap map) { Dictionary<(string,uint),Queue> requests=[]; List results=[]; foreach(var packet in packets.OrderBy(p=>p.Time).ThenBy(p=>p.Sequence)) { if(!packet.PayloadComplete) continue; var name=map.Resolve(packet.GameVersion,packet.Direction,packet.Opcode); var current=packet.GameVersion==PacketParser.AuditedClientVersion; if(name=="UP_PingReq" && packet.Payload.Length==(current ? 0x20 : 0x1C)) { var key=(packet.GameVersion,BinaryPrimitives.ReadUInt32LittleEndian(packet.Payload)); if(!requests.TryGetValue(key,out var queue)) requests[key]=queue=new(); queue.Enqueue(packet); } else if(name=="DOWN_PingRsp" && (current ? packet.Payload.Length==0x20 : packet.Payload.Length>=0x10)) { var key=(packet.GameVersion,BinaryPrimitives.ReadUInt32LittleEndian(packet.Payload)); if(requests.TryGetValue(key,out var queue) && queue.TryDequeue(out var request)) { var elapsed=packet.Time-request.Time; if(elapsed>=TimeSpan.Zero && elapsed<=TimeSpan.FromMinutes(1)) results.Add(new(request.Sequence,packet.Sequence,elapsed)); } } } return results.ToArray(); } public static PacketCorrelation[] Actions(IEnumerable packets,OpcodeMap map) { Dictionary<(string,ushort),Queue> requests=[]; List results=[]; foreach(var packet in packets.OrderBy(p=>p.Time).ThenBy(p=>p.Sequence)) { if(!packet.PayloadComplete || packet.GameVersion!=PacketParser.AuditedClientVersion) continue; var name=map.Resolve(packet.GameVersion,packet.Direction,packet.Opcode); if((name=="UP_ActionSend" && packet.Payload.Length==0x20) || (name=="UP_ActionSendPos" && packet.Payload.Length==0x18)) { var reqId=BinaryPrimitives.ReadUInt16LittleEndian(packet.Payload.AsSpan(6)); if(reqId==0) continue; if(!requests.TryGetValue((packet.GameVersion,reqId),out var queue)) requests[(packet.GameVersion,reqId)]=queue=new(); queue.Enqueue(packet); } else if(name=="DOWN_Effect" && packet.Payload.Length==0x7C && packet.Payload[0x21]<=1) { var reqId=BinaryPrimitives.ReadUInt16LittleEndian(packet.Payload.AsSpan(0x18)); if(reqId>0 && requests.TryGetValue((packet.GameVersion,reqId),out var queue) && queue.TryDequeue(out var request)) { var elapsed=packet.Time-request.Time; if(elapsed>=TimeSpan.Zero && elapsed<=TimeSpan.FromMinutes(1)) results.Add(new(request.Sequence,packet.Sequence,elapsed)); } } } return results.ToArray(); } } public static class PacketFiles { public static void ExportAtomic(string path,IEnumerable packets) { var temporary=path+"."+Guid.NewGuid().ToString("N")+".tmp"; try { Export(temporary,packets); File.Move(temporary,path,true); } finally { if(File.Exists(temporary)) File.Delete(temporary); } } public static void Export(string path,IEnumerable packets) { using var writer = new StreamWriter(path,false,new System.Text.UTF8Encoding(false)); foreach (var packet in packets) writer.WriteLine(JsonSerializer.Serialize(packet)); } public static PacketRecord[] Import(string path) { if (new FileInfo(path).Length > 32*1024*1024) throw new InvalidDataException("记录文件超过 32 MiB"); var records = new List(); foreach(var line in File.ReadLines(path)) { if (line.Length>100000) throw new InvalidDataException("记录行过长"); if (string.IsNullOrWhiteSpace(line)) continue; var packet=JsonSerializer.Deserialize(line) ?? throw new InvalidDataException("空记录"); if (packet.Payload == null || packet.Payload.Length>65536 || packet.Opcode is <0 or >65535 || !Enum.IsDefined(packet.Direction) || string.IsNullOrWhiteSpace(packet.GameVersion)) throw new InvalidDataException("无效记录"); records.Add(packet); if (records.Count>10000) throw new InvalidDataException("记录超过 10000 条"); } return records.ToArray(); } }