63 lines
5.6 KiB
C#
63 lines
5.6 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text.Json;
|
|
using ICRebuild.Core;
|
|
|
|
if(args.Length!=3) throw new ArgumentException("CaptureAudit capture.jsonl opcodes.json report.json");
|
|
var packets=PacketFiles.Import(args[0]);
|
|
var versions=packets.Select(p=>p.GameVersion).Distinct().ToArray();
|
|
if(versions.Length!=1 || packets.Length==0) throw new InvalidDataException("Expected one nonempty capture version");
|
|
var map=new OpcodeMap(); map.Load(File.ReadAllText(args[1]),versions[0],"local cache");
|
|
string? Name(PacketRecord p)=>map.Resolve(p.GameVersion,p.Direction,p.Opcode);
|
|
var supported=new HashSet<string>{
|
|
"UP_PingReq","DOWN_PingRsp","UP_UpdatePositionHandler","UP_UpdatePositionInstance","UP_EventStart",
|
|
"UP_ActionSend","UP_ActionSendPos","UP_ClientTrigger",
|
|
"UP_EventFinish","UP_EventFinish4","UP_EventFinish8","UP_EventFinish16","UP_EventFinish32","UP_EventFinish64","UP_EventFinish128","UP_EventFinish255",
|
|
"UP_EventAction","UP_EventAction4","UP_EventAction8","UP_EventAction16","UP_EventAction32","UP_EventAction64","UP_EventAction128","UP_EventAction255",
|
|
"DOWN_EffectResult","DOWN_UpdateHate","DOWN_UpdateHater","DOWN_EffectResultBasic","DOWN_JobGauge","DOWN_UpdateClassInfo","DOWN_FirstAttack",
|
|
"DOWN_ActorControl","DOWN_ActorControlSelf","DOWN_ActorControlTarget","DOWN_Effect","DOWN_StatusEffectList","DOWN_ActorDelete","DOWN_ObjectSpawn","DOWN_NpcSpawn"
|
|
};
|
|
var parsed=packets.Select(p=>(Packet:p,Name:Name(p),Result:p.PayloadComplete ? PacketParser.Parse(Name(p),p.Payload,p.GameVersion) : new ParseResult(false,"incomplete"))).ToArray();
|
|
var correlations=PacketCorrelator.Ping(packets,map);
|
|
var actionCorrelations=PacketCorrelator.Actions(packets,map);
|
|
// Replay owned sample data through the manager and recorder; no native calls or sockets.
|
|
using var manager=new NetworkPipeline(versions[0]);
|
|
var replayBuffer=new PacketBuffer { Paused=false };
|
|
var recorder=new PacketRecorder(replayBuffer);
|
|
using var up=manager.Subscribe(NetworkPhase.BeforeSend,recorder.Record);
|
|
using var down=manager.Subscribe(NetworkPhase.Received,recorder.Record);
|
|
long returned=0;
|
|
using var post=manager.Subscribe(NetworkPhase.AfterSend,_=>returned++);
|
|
foreach(var p in packets)
|
|
{
|
|
CaptureRead Read(Func<int,bool> accepts)=>accepts(p.Opcode) ? new(new(p.Opcode,p.Target,p.Payload,p.PayloadComplete,p.CaptureIssue)) : new(null);
|
|
if(p.Direction==PacketDirection.Up) manager.Send(()=>1,Read); else manager.Receive(()=>1,Read);
|
|
}
|
|
var replayed=replayBuffer.Snapshot();
|
|
var replayMatches=replayed.Length==packets.Length && replayed.Zip(packets).All(pair=>
|
|
pair.First.Direction==pair.Second.Direction && pair.First.Opcode==pair.Second.Opcode &&
|
|
pair.First.GameVersion==pair.Second.GameVersion && pair.First.Target==pair.Second.Target &&
|
|
pair.First.PayloadComplete==pair.Second.PayloadComplete && pair.First.CaptureIssue==pair.Second.CaptureIssue &&
|
|
pair.First.Payload.SequenceEqual(pair.Second.Payload));
|
|
if(!replayMatches || manager.ObserverErrors!=0 || returned!=packets.Count(p=>p.Direction==PacketDirection.Up))
|
|
throw new InvalidDataException("Manager replay changed sample data or missed send completion observations");
|
|
var report=new {
|
|
Source=Path.GetFileName(args[0]),SHA256=Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(args[0]))),
|
|
MapSHA256=Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(args[1]))),MapSource=map.Snapshot.Source,Version=versions[0],
|
|
Records=packets.Length,Up=packets.Count(p=>p.Direction==PacketDirection.Up),Down=packets.Count(p=>p.Direction==PacketDirection.Down),
|
|
ManagerReplay=new {Matches=replayMatches,Records=replayed.Length,SendReturns=returned,manager.ObserverErrors,manager.ReadErrors},
|
|
FirstTime=packets.Min(p=>p.Time),LastTime=packets.Max(p=>p.Time),FirstSequence=packets.First().Sequence,LastSequence=packets.Last().Sequence,
|
|
SequenceContinuous=packets.Zip(packets.Skip(1)).All(p=>p.Second.Sequence==p.First.Sequence+1),
|
|
MonotonicOrder=packets.Zip(packets.Skip(1)).All(p=>p.Second.MonotonicTicks>=p.First.MonotonicTicks),
|
|
Complete=packets.Count(p=>p.PayloadComplete),Incomplete=packets.Count(p=>!p.PayloadComplete),CaptureIssues=packets.Count(p=>p.CaptureIssue!=null),
|
|
PayloadBytes=packets.Sum(p=>p.Payload.Length),MinPayload=packets.Min(p=>p.Payload.Length),MaxPayload=packets.Max(p=>p.Payload.Length),
|
|
EmptyPayloads=packets.Count(p=>p.Payload.Length==0),Known=packets.Count(p=>Name(p)!=null),Unknown=packets.Count(p=>Name(p)==null),
|
|
Parsed=parsed.Count(p=>p.Result.Parsed),SupportedButRejected=parsed.Count(p=>p.Name!=null && supported.Contains(p.Name) && !p.Result.Parsed),
|
|
Groups=parsed.GroupBy(p=>new{p.Packet.Direction,p.Packet.Opcode,p.Name,Length=p.Packet.Payload.Length,p.Packet.PayloadComplete,p.Result.Parsed})
|
|
.OrderByDescending(g=>g.Count()).Select(g=>new{g.Key,Count=g.Count(),Rejection=g.Key.Name!=null && supported.Contains(g.Key.Name) && !g.Key.Parsed ? g.First().Result.Summary : null}),
|
|
PingPairs=correlations.Select(p=>new{p.Request,p.Response,Milliseconds=p.Elapsed.TotalMilliseconds}),
|
|
ActionPairs=actionCorrelations.Select(p=>new{p.Request,p.Response,Milliseconds=p.Elapsed.TotalMilliseconds}),
|
|
Limitations="No raw payload, coordinates or actor IDs included. Successful parsing does not verify all fields. Header-read failures absent from export require UI counters."
|
|
};
|
|
File.WriteAllText(args[2],JsonSerializer.Serialize(report,new JsonSerializerOptions{WriteIndented=true}));
|
|
Console.WriteLine($"Records={report.Records}; up={report.Up}; down={report.Down}; complete={report.Complete}; known={report.Known}; parsed={report.Parsed}; rejected={report.SupportedButRejected}; pingPairs={correlations.Length}; actionPairs={actionCorrelations.Length}");
|