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{ "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 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}");