Initial import of IC.Rebuild

This commit is contained in:
2026-10-13 22:55:05 +08:00
commit 16d61f7a5e
87 changed files with 17054 additions and 0 deletions
+401
View File
@@ -0,0 +1,401 @@
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<int,string> Up, IReadOnlyDictionary<int,string> Down);
public sealed class OpcodeMap
{
private OpcodeSnapshot snapshot = new("unknown","none",new ReadOnlyDictionary<int,string>(new Dictionary<int,string>()),new ReadOnlyDictionary<int,string>(new Dictionary<int,string>()));
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<int,string> up = [], down = [];
foreach (var property in entries.EnumerateObject())
{
Dictionary<int,string> 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<int,string>(up),new ReadOnlyDictionary<int,string>(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<byte> 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<statusCount;i++)
if(!float.IsFinite(F32(payload,0x1C+i*16+8))) return new(false,"效果状态持续时间包含非有限数");
return new(true,$"效果结果条目=1 · 序列={U32(payload,4)} HP={U32(payload,0x0C)}/{U32(payload,0x10)} MP={U16(payload,0x14)} 目标序号={payload[0x16]} 职业={payload[0x17]} 护盾={payload[0x18]}% 状态数={statusCount}");
}
if (payload.Length < 4) return Invalid(name,payload.Length,4);
int count = payload[0];
if (count > 16 || payload.Length < 4 + count * 0x58) return new(false,"效果结果计数超限或载荷截断");
for (var i=0;i<count;i++)
if (payload[4+i*0x58+0x15] > 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<char> 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<byte> 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<string>(previewCount);
for(var i=0;i<previewCount;i++) args.Add(U32(payload,8+i*4).ToString());
var suffix=argCount>previewCount ? ",..." : "";
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<byte> p,int at) => BinaryPrimitives.ReadUInt16LittleEndian(p[at..]);
private static uint U32(ReadOnlySpan<byte> p,int at) => BinaryPrimitives.ReadUInt32LittleEndian(p[at..]);
private static ulong U64(ReadOnlySpan<byte> p,int at) => BinaryPrimitives.ReadUInt64LittleEndian(p[at..]);
private static float F32(ReadOnlySpan<byte> p,int at) => BinaryPrimitives.ReadSingleLittleEndian(p[at..]);
private static ParseResult Unverified(string name) => new(false,$"{name}:此版本的字段布局未核实");
private static ParseResult Enmity(string name,ReadOnlySpan<byte> 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<string>(count);
for(var i=0;i<count;i++) entries.Add($"0x{U32(payload,4+i*8):X8}:{payload[8+i*8]}");
var label=name=="DOWN_UpdateHate" ? "目标仇恨列表" : "自身敌对列表";
return new(true,$"{label} {count} 项(对象:仇恨值){string.Join(", ",entries)}");
}
private static ParseResult Position(ReadOnlySpan<byte> 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<PacketRecord> 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<byte> 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<PacketRecord> 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<PacketRecord> packets,OpcodeMap map)
{
Dictionary<(string,uint),Queue<PacketRecord>> requests=[];
List<PacketCorrelation> 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<PacketRecord> packets,OpcodeMap map)
{
Dictionary<(string,ushort),Queue<PacketRecord>> requests=[];
List<PacketCorrelation> 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<PacketRecord> 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<PacketRecord> 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<PacketRecord>();
foreach(var line in File.ReadLines(path))
{
if (line.Length>100000) throw new InvalidDataException("记录行过长");
if (string.IsNullOrWhiteSpace(line)) continue;
var packet=JsonSerializer.Deserialize<PacketRecord>(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();
}
}