402 lines
26 KiB
C#
402 lines
26 KiB
C#
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();
|
||
}
|
||
}
|