Files

1132 lines
69 KiB
C#

using System.Buffers.Binary;
using ICRebuild.Core;
int passed=0;
void Test(string name,Action body) { body(); Console.WriteLine($"PASS {name}"); passed++; }
void Check(bool condition,string message="assertion failed") { if(!condition) throw new Exception(message); }
void Throws<T>(Action action) where T:Exception { try { action(); } catch(T) { return; } throw new Exception($"Expected {typeof(T).Name}"); }
Test("network send phases preserve order owned payload identity and raw result",()=>
{
using var pipeline=new NetworkPipeline("v1");
var bytes=new byte[]{1,2,3};
List<string> order=[]; NetworkObservation? before=null,after=null;
using var a=pipeline.Subscribe(NetworkPhase.BeforeSend,p=>{order.Add("before");before=p;});
using var b=pipeline.Subscribe(NetworkPhase.AfterSend,p=>{order.Add("after");after=p;});
int calls=0,reads=0;
var result=pipeline.Send(()=>{calls++;order.Add("native");bytes[0]=99;return 0;},filter=>
{ reads++;order.Add("read");return new(new(42,null,bytes,true)); });
Check(result==0 && calls==1 && reads==1 && order.SequenceEqual(["read","before","native","after"]));
Check(before!=null && after!=null && ReferenceEquals(before.Message,after.Message));
Check(after!.NativeResult==0 && before!.NativeResult==null && after.Message.Payload.SequenceEqual(new byte[]{1,2,3}));
Check(after.Message.GameVersion=="v1" && after.Message.Direction==PacketDirection.Up && pipeline.ObserverErrors==0);
});
Test("network observer and capture failures never skip or retry the native send",()=>
{
using var pipeline=new NetworkPipeline("v1"); int calls=0,notified=0;
using var a=pipeline.Subscribe(NetworkPhase.BeforeSend,_=>throw new InvalidOperationException("observer"));
using var b=pipeline.Subscribe(NetworkPhase.BeforeSend,_=>notified++);
using var c=pipeline.Subscribe(NetworkPhase.AfterSend,_=>throw new InvalidOperationException("after"));
Func<byte> original=()=>{calls++;return 7;};
Check(pipeline.Send(original,_=>new(new(1,null,[],true)))==7);
Check(calls==1 && notified==1 && pipeline.ObserverErrors==2);
Check(pipeline.Send(original,_=>throw new InvalidDataException("reader"))==7);
Check(calls==2 && pipeline.ReadErrors==1 && notified==1);
var after=pipeline.AfterSend;
Throws<InvalidOperationException>(()=>pipeline.Send(()=>{calls++;throw new InvalidOperationException("native");},_=>new(new(1,null,[],true))));
Check(calls==3 && pipeline.AfterSend==after);
});
Test("network receive reads only after native success and preserves metadata failure",()=>
{
using var pipeline=new NetworkPipeline("v1");
int calls=0,reads=0; NetworkObservation? observed=null;
using var s=pipeline.Subscribe(NetworkPhase.Received,p=>observed=p);
Func<Func<int,bool>,CaptureRead> capture=_=>{reads++;return new(new(42,123,[],false,"truncated"));};
Check(pipeline.Receive(()=>{calls++;return 0;},capture)==0 && reads==0 && observed==null);
Check(pipeline.Receive(()=>{calls++;return 2;},capture)==2);
Check(calls==2 && reads==1 && observed is { NativeResult:2 } && !observed.Message.Complete && observed.Message.Issue=="truncated");
Check(pipeline.ReadErrors==1 && pipeline.ObserverErrors==0);
Throws<InvalidOperationException>(()=>pipeline.Receive(()=>{calls++;throw new InvalidOperationException();},capture));
Check(calls==3 && reads==1);
});
Test("log pause direction and opcode filters do not filter independent network observers",()=>
{
using var pipeline=new NetworkPipeline("v1");
var buffer=new PacketBuffer(); var recorder=new PacketRecorder(buffer);
using var up=pipeline.Subscribe(NetworkPhase.BeforeSend,recorder.Record);
using var down=pipeline.Subscribe(NetworkPhase.Received,recorder.Record);
int received=0,sent=0;
using var businessDown=pipeline.Subscribe(NetworkPhase.Received,_=>received++,42);
using var businessUp=pipeline.Subscribe(NetworkPhase.AfterSend,_=>sent++,42);
void SendPair()
{
pipeline.Send(()=>1,_=>new(new(42,null,[1],true)));
pipeline.Receive(()=>1,_=>new(new(42,123,[2],true)));
}
SendPair(); Check(buffer.Snapshot().Length==0); // paused by default
buffer.Paused=false;buffer.OpcodeFilter=43; SendPair(); Check(buffer.Snapshot().Length==0);
buffer.OpcodeFilter=null;buffer.DirectionFilter=PacketDirection.Down;SendPair();
Check(received==3 && sent==3 && buffer.Snapshot().Length==1 && recorder.CompleteDown==1 && recorder.CompleteUp==0);
buffer.DirectionFilter=null; SendPair();
Check(buffer.Snapshot().Length==3 && recorder.CompleteUp==1 && pipeline.ObserverErrors==0);
var record=buffer.Snapshot()[^1];
Check(record.Context is {Phase:NetworkPhase.Received,Stage:PacketStage.RawIpcPayload,Origin:PacketOrigin.NativeZone,NativeResult:1});
});
Test("network cancellation and shutdown remove callbacks without duplicate reattachment",()=>
{
using var pipeline=new NetworkPipeline("v1"); int calls=0,reads=0;
CaptureRead Capture(Func<int,bool> filter) { reads++;return filter(42) ? new(new(42,null,[],true)) : new(null); }
for(int i=0;i<3;i++)
{
var sub=pipeline.Subscribe(NetworkPhase.BeforeSend,_=>calls++,42);
pipeline.Send(()=>1,Capture); sub.Dispose();sub.Dispose();
pipeline.Send(()=>1,Capture); // no observers: do not copy memory
}
Check(calls==3 && reads==3 && pipeline.SubscriberCount==0);
IDisposable? after=null;
using var before=pipeline.Subscribe(NetworkPhase.BeforeSend,_=>after!.Dispose());
after=pipeline.Subscribe(NetworkPhase.AfterSend,_=>calls++);
pipeline.Send(()=>1,Capture); Check(calls==3);
pipeline.Dispose();pipeline.Send(()=>1,Capture);
Check(pipeline.SubscriberCount==0 && reads==4 && pipeline.ObserverErrors==0);
Throws<ObjectDisposedException>(()=>pipeline.Subscribe(NetworkPhase.Received,_=>{}));
});
Test("shared network subscriptions survive recorder shutdown until final owner releases",()=>
{
using var pipeline=new NetworkPipeline("v1");
int started=0,stopped=0,logs=0,business=0;
var shared=new SharedModuleResource(s=>{started++;s.Defer(()=>stopped++);});
var recorder=new ResourceScope();var consumer=new ResourceScope();
shared.Acquire(recorder);recorder.Own(pipeline.Subscribe(NetworkPhase.Received,_=>logs++));
shared.Acquire(consumer);consumer.Own(pipeline.Subscribe(NetworkPhase.Received,_=>business++));
pipeline.Receive(()=>1,_=>new(new(42,null,[],true))); recorder.Dispose();
pipeline.Receive(()=>1,_=>new(new(42,null,[],true)));
Check(started==1 && stopped==0 && logs==1 && business==2);
consumer.Dispose(); Check(stopped==1 && pipeline.SubscriberCount==0);
});
Test("network record context roundtrips without changing timestamps or payload",()=>
{
var path=Path.Combine(Path.GetTempPath(),Guid.NewGuid()+".jsonl");
try
{
var message=new NetworkMessage(42,PacketDirection.Up,"v1",new(7,null,[1,2],true));
var buffer=new PacketBuffer { Paused=false };
new PacketRecorder(buffer).Record(new(message,NetworkPhase.BeforeSend));
PacketFiles.Export(path,buffer.Snapshot());var imported=PacketFiles.Import(path).Single();
Check(imported.Time==message.Time && imported.MonotonicTicks==message.MonotonicTicks && imported.Payload.SequenceEqual(message.Payload.ToArray()));
Check(imported.Context is {ObservationId:42,Phase:NetworkPhase.BeforeSend,NativeResult:null});
}
finally { File.Delete(path); }
});
Test("status blocking requires current local identity and rejects replay or stale frames",()=>
{
var local=(nint)0x10000;
var manager=local+StatusBlockPolicy.ManagerOffset;
var frame=new GameFrame(true,local,false,false,DungeonSnapshot.Empty,
LocalStatusManagerAddress:manager,LocalEntityId:42);
Check(StatusBlockPolicy.IsLocal(frame,manager,local,42));
Check(!StatusBlockPolicy.IsLocal(frame,manager+16,local,42));
Check(!StatusBlockPolicy.IsLocal(frame,manager,local+0x10000,42));
Check(!StatusBlockPolicy.IsLocal(frame,manager,local,43)); // recycled address
Check(!StatusBlockPolicy.IsLocal(frame,manager,0,42));
Check(!StatusBlockPolicy.IsLocal(frame with { IsReplay=true },manager,local,42));
Check(!StatusBlockPolicy.IsLocal(frame with { LocalEntityId=0xE0000000 },manager,local,0xE0000000));
Check(!StatusBlockPolicy.IsLocal(frame with { LocalStatusManagerAddress=manager+16 },manager+16,local,42));
long now=0;
var frames=new FrameStore(()=>now,10);
frames.Publish(frame); now=11;
Check(!StatusBlockPolicy.IsLocal(frames.Read(),manager,local,42));
frames.Publish(frame); frames.Clear();
Check(!StatusBlockPolicy.IsLocal(frames.Read(),manager,local,42));
});
Test("status selection respects both list halves and immutable audited settings",()=>
{
var selected=new HashSet<uint>{1257,142,65535};
var settings=new SettingsSnapshot(new Dictionary<int,float>(),[],true,0,blockedStatuses:selected);
selected.Clear();
Check(settings.BlockedStatuses.SetEquals([1257u]));
foreach(var index in new[]{0,29,30,59}) Check(StatusBlockPolicy.IsSelected(index,1257,settings.BlockedStatuses));
foreach(var index in new[]{-1,60,int.MaxValue}) Check(!StatusBlockPolicy.IsSelected(index,1257,settings.BlockedStatuses));
Check(!StatusBlockPolicy.IsSelected(0,0,settings.BlockedStatuses));
Check(!StatusBlockPolicy.IsSelected(0,1681,settings.BlockedStatuses));
Check(!StatusBlockPolicy.IsSelected(0,142,new HashSet<uint>{142}));
Check(!StatusBlockPolicy.IsSelected(0,1257,new HashSet<uint>()));
});
Test("blocked status forwards an empty update once and preserves native result and refresh flag",()=>
{
foreach(byte refresh in new byte[]{0,1})
{
int calls=0;
StatusSetter original=(manager,index,id,remaining,param,source,flag)=>
{
calls++;
Check(manager==(nint)0x1234 && index==59 && flag==refresh);
Check(id==0 && remaining==0 && param==0 && source==0);
return 1; // batch caller must see a change and refresh flags
};
Check(StatusBlockPolicy.Forward(original,true,0x1234,59,1257,12.5f,7,0x123456789ABCDEF0,refresh)==1);
Check(calls==1);
}
});
Test("unselected status preserves every argument and original failure is never retried",()=>
{
int calls=0;
StatusSetter original=(manager,index,id,remaining,param,source,flag)=>
{
calls++;
Check(manager==(nint)0x1234 && index==30 && id==1681 && remaining==-12.5f);
Check(param==ushort.MaxValue && source==0x123456789ABCDEF0 && flag==1);
return 0;
};
Check(StatusBlockPolicy.Forward(original,false,0x1234,30,1681,-12.5f,ushort.MaxValue,0x123456789ABCDEF0,1)==0);
Check(calls==1);
calls=0;
StatusSetter failing=(_,_,_,_,_,_,_)=>{calls++;throw new InvalidOperationException();};
Throws<InvalidOperationException>(()=>StatusBlockPolicy.Forward(failing,true,0x1234,0,1257,0,0,0,0));
Check(calls==1);
});
Test("approved scope contains exactly 24 unique features",()=>
{
int[] expected=[..Enumerable.Range(1,11),..Enumerable.Range(13,8),27,28,29,36,37];
Check(Features.All.Select(f=>f.Id).SequenceEqual(expected));
});
Test("partial startup rolls back resources in reverse order",()=>
{
using var manager=new ModuleManager(); List<int> released=[];
manager.Register(9,s=> { s.Defer(()=>released.Add(1)); s.Defer(()=>released.Add(2)); throw new InvalidOperationException("scan failed"); });
Check(!manager.Enable(9)); Check(released.SequenceEqual([2,1]));
Check(!manager.IsEnabled(9)); Check(manager.Status[9].State==ModuleState.Failed);
});
Test("cleanup failure does not skip remaining resources",()=>
{
var scope=new ResourceScope(); var released=false;
scope.Defer(()=>released=true); scope.Defer(()=>throw new InvalidOperationException());
Throws<AggregateException>(scope.Dispose); Check(released); scope.Dispose();
});
Test("repeated enable and disable are idempotent",()=>
{
var manager=new ModuleManager(); int started=0,released=0;
manager.Register(9,s=>{started++;s.Defer(()=>released++);});
Check(manager.Enable(9)); Check(manager.Enable(9)); manager.Disable(9); manager.Disable(9); manager.Dispose(); manager.Dispose();
Check(started==1 && released==1); Throws<ObjectDisposedException>(()=>manager.Enable(9));
});
Test("dependent modules stop before their dependency",()=>
{
using var manager=new ModuleManager(); List<int> stopped=[];
manager.Register(9,s=>s.Defer(()=>stopped.Add(9)));
manager.Register(10,s=>s.Defer(()=>stopped.Add(10)),9);
Check(!manager.Enable(10)); Check(manager.Enable(9)); Check(manager.Enable(10));
manager.Disable(9); Check(stopped.SequenceEqual([10,9]));
});
Test("one unavailable module does not block another",()=>
{
using var manager=new ModuleManager(); manager.Register(11,_=>{});
Check(!manager.Enable(8)); Check(manager.Enable(11)); Check(manager.IsEnabled(11));
});
Test("failed cleanup prevents duplicate native attachment",()=>
{
using var manager=new ModuleManager(); var starts=0;
manager.Register(9,s=>{starts++;s.Defer(()=>throw new InvalidOperationException("dispose failed"));});
Check(manager.Enable(9)); manager.Disable(9); Check(!manager.Enable(9)); Check(starts==1);
});
Test("deep dungeon special floors and unknown state",()=>
{
Check(Policies.HeightDelta(true,true,new(true,100,1,9),-7)==-7);
Check(Policies.HeightDelta(true,true,new(true,100,1,10),-7)==0);
Check(Policies.HeightDelta(true,true,new(true,1108,3,99),-7)==0);
Check(Policies.HeightDelta(true,true,new(true,1290,4,99),-7)==0);
Check(Policies.HeightDelta(true,true,new(true,100,1,99),-7)==-7);
Check(Policies.HeightDelta(true,true,DungeonSnapshot.Empty,-7)==0);
Check(Policies.HeightDelta(false,false,DungeonSnapshot.Empty,-7)==0);
Check(Policies.HeightDelta(true,false,DungeonSnapshot.Empty,-7)==-7);
});
Test("nonfinite and out of range parameters",()=>
{
Check(Policies.FiniteClamp(float.NaN,0,5,1)==1);
Check(Policies.FiniteClamp(float.PositiveInfinity,0,5)==0);
Check(Policies.FiniteClamp(999,0,5)==5);
Check(Policies.HeightDelta(true,false,DungeonSnapshot.Empty,float.NaN)==0);
});
Test("timeline list semantics and empty list",()=>
{
HashSet<uint> list=[1,2];
Check(Policies.BlockTimeline(1,list,true)); Check(!Policies.BlockTimeline(3,list,true));
Check(!Policies.BlockTimeline(1,list,false)); Check(Policies.BlockTimeline(3,list,false));
Check(!Policies.BlockTimeline(1,new HashSet<uint>(),false));
});
Test("fall flag transformation preserves unrelated bits",()=>
{
Check(Policies.FallFlags(0x80000000)==0x80000000);
Check((Policies.FallFlags(0x80000700)&0x80000000)!=0);
Check((Policies.FallFlags(0x700)&0x700)==0);
});
const string mapping="""{"version-a":{"UP_PingReq":[1],"DOWN_PingRsp":[1],"UP_EventStart":[3]}}""";
Test("opcode version and direction isolation",()=>
{
var map=new OpcodeMap(); map.Load(mapping,"version-a","fixture");
Check(map.Resolve("version-a",PacketDirection.Up,1)=="UP_PingReq");
Check(map.Resolve("version-a",PacketDirection.Down,1)=="DOWN_PingRsp");
Check(map.Resolve("version-b",PacketDirection.Up,1)==null);
Check(map.Resolve("unknown",PacketDirection.Up,1)==null);
});
Test("invalid mapping updates retain previous mapping",()=>
{
var map=new OpcodeMap(); map.Load(mapping,"version-a","fixture");
Throws<InvalidDataException>(()=>map.Load("""{"version-a":{"UP_A":[1],"UP_B":[1]}}""","version-a","bad"));
Throws<InvalidDataException>(()=>map.Load("""{"version-a":{"UP_A":[65536]}}""","version-a","bad"));
Throws<InvalidDataException>(()=>map.Load(mapping,"missing","bad"));
Check(map.Resolve("version-a",PacketDirection.Up,1)=="UP_PingReq");
});
Test("packet parser rejects malformed and ambiguous payloads",()=>
{
Check(!PacketParser.Parse("UP_PingReq",new byte[2]).Parsed);
Check(PacketParser.Parse("UP_PingReq",new byte[28]).Parsed);
Check(!PacketParser.Parse("UP_EventAction",new byte[1028]).Parsed);
Check(!PacketParser.Parse(null,new byte[8]).Parsed);
var effect=new byte[4+0x58]; effect[0]=17;
Check(!PacketParser.Parse("DOWN_EffectResult",effect).Parsed);
effect[0]=2; Check(!PacketParser.Parse("DOWN_EffectResult",effect).Parsed);
effect[0]=1; effect[4+0x15]=5; Check(!PacketParser.Parse("DOWN_EffectResult",effect).Parsed);
effect[4+0x15]=4; Check(PacketParser.Parse("DOWN_EffectResult",effect).Parsed);
var movement=new byte[40]; BinaryPrimitives.WriteSingleLittleEndian(movement.AsSpan(12),float.NaN);
Check(!PacketParser.Parse("UP_UpdatePositionHandler",movement).Parsed);
});
Test("bounded buffer pause filter and copy ownership",()=>
{
var buffer=new PacketBuffer(2,10); byte[] data=[1,2];
Check(!buffer.Add(PacketDirection.Up,1,"a",null,null,data)); buffer.Paused=false;
Check(buffer.Add(PacketDirection.Up,1,"a",null,null,data)); data[0]=9;
Check(buffer.Snapshot()[0].Payload[0]==1);
buffer.Add(PacketDirection.Up,2,"a",null,null,data); buffer.Add(PacketDirection.Down,3,"a",null,null,data);
Check(buffer.Snapshot().Length==2 && buffer.Dropped==1);
Check(!buffer.Add(PacketDirection.Up,1,"a",null,null,new byte[11])); Check(buffer.Dropped==2);
buffer.DirectionFilter=PacketDirection.Up; Check(!buffer.Add(PacketDirection.Down,1,"a",null,null,data));
});
Test("offline import preserves chronology and invalid replacement is atomic",()=>
{
var buffer=new PacketBuffer(2); var time=DateTimeOffset.Parse("2026-01-01T00:00:00Z");
PacketRecord[] records=[new(42,99,time,PacketDirection.Up,1,"a",3,4,[5])];
buffer.Replace(records); Check(buffer.Snapshot()[0].Time==time && buffer.Snapshot()[0].Sequence==42);
Throws<InvalidDataException>(()=>buffer.Replace([records[0] with {Opcode=-1}]));
Check(buffer.Snapshot()[0].Sequence==42);
});
Test("ping correlation uses echoed value and version, not unrelated packets",()=>
{
var map=new OpcodeMap(); map.Load(mapping,"version-a","fixture");
var now=DateTimeOffset.UtcNow; var up=new byte[28]; var down=new byte[16];
BinaryPrimitives.WriteUInt32LittleEndian(up,123); BinaryPrimitives.WriteUInt32LittleEndian(down,123);
PacketRecord[] packets=[new(1,0,now,PacketDirection.Up,1,"version-a",null,null,up),new(2,0,now.AddMilliseconds(75),PacketDirection.Down,1,"version-a",null,null,down)];
var pairs=PacketCorrelator.Ping(packets,map); Check(pairs.Length==1 && pairs[0].Elapsed.TotalMilliseconds==75);
Check(PacketCorrelator.Ping([packets[0],packets[1] with {GameVersion="version-b"}],map).Length==0);
});
Test("random truncated buffers never throw or read beyond input",()=>
{
var random=new Random(701); string?[] names=[null,"UP_PingReq","DOWN_PingRsp","UP_UpdatePositionHandler","UP_UpdatePositionInstance","UP_EventStart","UP_EventAction","DOWN_EffectResult"];
for(var i=0;i<3000;i++) { var data=new byte[random.Next(0,1800)]; random.NextBytes(data); PacketParser.Parse(names[i%names.Length],data); }
});
Test("metadata events cannot masquerade as complete payloads",()=>
{
var map=new OpcodeMap(); map.Load(mapping,"version-a","fixture");
var buffer=new PacketBuffer(); buffer.Paused=false;
buffer.Add(PacketDirection.Up,1,"version-a",null,null,ReadOnlySpan<byte>.Empty,false);
Check(!buffer.Snapshot()[0].PayloadComplete);
Check(PacketCorrelator.Ping(buffer.Snapshot(),map).Length==0);
});
Test("concurrent capture stays bounded with unique ordered sequences",()=>
{
var buffer=new PacketBuffer(256); buffer.Paused=false;
Parallel.For(0,8000,i=>buffer.Add(PacketDirection.Up,i%65536,"a",null,null,ReadOnlySpan<byte>.Empty,false));
var records=buffer.Snapshot();
Check(records.Length==256 && buffer.Dropped==7744);
Check(records.Select(p=>p.Sequence).Distinct().Count()==256);
Check(records.Zip(records.Skip(1)).All(p=>p.First.Sequence<p.Second.Sequence));
});
Test("packet export roundtrip preserves timestamps and metadata completeness",()=>
{
var path=Path.Combine(AppContext.BaseDirectory,"roundtrip-"+Guid.NewGuid()+".jsonl");
try
{
PacketRecord[] data=[new(42,10,DateTimeOffset.Parse("2026-01-01T00:00:00+08:00"),PacketDirection.Down,2,"a",null,12,[],false)];
PacketFiles.Export(path,data);
var loaded=PacketFiles.Import(path);
Check(loaded.Length==1 && loaded[0].Time==data[0].Time && loaded[0].Sequence==42 && !loaded[0].PayloadComplete);
}
finally { if(File.Exists(path)) File.Delete(path); }
});
Test("native worker reads only the published frame",()=>
{
long now=100;
var frames=new FrameStore(()=>Interlocked.Read(ref now),500);
Check(!frames.Read().Ready);
var expected=new GameFrame(true,(nint)123,true,false,new(true,1108,3,98));
frames.Publish(expected);
Parallel.For(0,10000,_=>Check(frames.Read()==expected));
now=601; Check(!frames.Read().Ready && frames.Read().LocalAddress==0);
now=200; frames.Publish(expected); frames.Clear(); Check(!frames.Read().Ready);
});
Test("published settings are isolated from UI collection mutations",()=>
{
Dictionary<int,float> values=new() { [14]=2 }; HashSet<uint> list=[10];
var settings=new SettingsSnapshot(values,list,true,2);
values[14]=5; list.Clear();
Parallel.For(0,5000,_=>Check(settings.Value(14)==2 && settings.Timelines.Contains(10)));
});
Test("acceleration preserves native output instead of restoring zero",()=>
{
float state=0; int calls=0;
AccelerationPolicy.Run(ref state,true,()=>{ Check(state==100); state=6; calls++; });
Check(state==6 && calls==1);
state=3;
AccelerationPolicy.Run(ref state,false,()=>{ Check(state==3); state=4; calls++; });
Check(state==4 && calls==2);
});
Test("acceleration applies only to the moving local owner",()=>
{
var frame=new GameFrame(true,(nint)123,false,false,DungeonSnapshot.Empty);
Check(AccelerationPolicy.ShouldPrime(frame,(nint)123,true,0));
Check(!AccelerationPolicy.ShouldPrime(frame,(nint)456,true,0));
Check(!AccelerationPolicy.ShouldPrime(frame,(nint)123,false,0));
Check(!AccelerationPolicy.ShouldPrime(frame with {IsSlowed=true},(nint)123,true,0));
Check(!AccelerationPolicy.ShouldPrime(frame,(nint)123,true,float.NaN));
});
Test("atomic snapshot export keeps previous file when serialization fails",()=>
{
var path=Path.Combine(AppContext.BaseDirectory,"atomic-"+Guid.NewGuid()+".jsonl");
IEnumerable<PacketRecord> Broken()
{
yield return new(1,0,DateTimeOffset.UtcNow,PacketDirection.Up,1,"a",null,null,[],false);
throw new InvalidOperationException("capture interrupted");
}
try
{
File.WriteAllText(path,"previous");
Throws<InvalidOperationException>(()=>PacketFiles.ExportAtomic(path,Broken()));
Check(File.ReadAllText(path)=="previous");
Check(Directory.GetFiles(AppContext.BaseDirectory,Path.GetFileName(path)+".*.tmp").Length==0);
}
finally { if(File.Exists(path)) File.Delete(path); }
});
Test("dash metadata decodes ushort ID without including flags",()=>
{
Check(DashPolicy.Timeline([0x34,0x12,0xF1,0xAF])==0x1234);
Check(DashPolicy.Timeline([0x34])==0);
});
Test("dash modes distinguish all-local from empty lists",()=>
{
HashSet<uint> empty=[], selected=[20];
Check(DashPolicy.Block(true,1,1,20,DashFilterMode.AllLocal,empty));
Check(!DashPolicy.Block(true,2,1,20,DashFilterMode.AllLocal,empty));
Check(!DashPolicy.Block(true,1,1,0,DashFilterMode.AllLocal,empty));
Check(!DashPolicy.Block(false,1,1,20,DashFilterMode.AllLocal,empty));
Check(!DashPolicy.Block(true,1,1,20,DashFilterMode.BlockListed,empty));
Check(!DashPolicy.Block(true,1,1,20,DashFilterMode.AllowListed,empty));
Check(DashPolicy.Block(true,1,1,20,DashFilterMode.BlockListed,selected));
Check(!DashPolicy.Block(true,1,1,20,DashFilterMode.AllowListed,selected));
Check(DashPolicy.Block(true,1,1,21,DashFilterMode.AllowListed,selected));
});
Test("target radius is expanded once while own radius remains unchanged",()=>
{
var frame=new GameFrame(true,1,false,false,DungeonSnapshot.Empty,TargetAddress:2,TargetEntityId:20);
Check(TargetRadiusPolicy.Adjust(frame,2,20,1,5)==6);
Check(TargetRadiusPolicy.Adjust(frame,1,10,0.5f,5)==0.5f);
Check(TargetRadiusPolicy.Adjust(frame,3,30,1,5)==1);
// Native landing uses target + own radius: 6 + 0.5, not the erroneous 6 + 5.5.
Check(TargetRadiusPolicy.Adjust(frame,2,20,1,5)+TargetRadiusPolicy.Adjust(frame,1,10,0.5f,5)==6.5f);
});
Test("render range and movement reads agree on the same target radius",()=>
{
var frame=new GameFrame(true,1,false,false,DungeonSnapshot.Empty,TargetAddress:2,TargetEntityId:20);
Check(TargetRadiusPolicy.Adjust(frame,2,20,1,5)==6);
using(NativeTargetRingScope.Enter(true)) Check(TargetRadiusPolicy.Adjust(frame,2,20,1,5)==6);
using(ActionRangeCheckScope.Enter(true)) Check(TargetRadiusPolicy.Adjust(frame,2,20,1,5)==6);
float workerResult=0;
var worker=new Thread(()=>workerResult=TargetRadiusPolicy.Adjust(frame,2,20,1,5)); worker.Start(); worker.Join();
Check(workerResult==6);
});
Test("target changes stale identities and invalid radii are handled",()=>
{
var frame=new GameFrame(true,1,false,false,DungeonSnapshot.Empty,TargetAddress:2,TargetEntityId:20);
Check(TargetRadiusPolicy.Adjust(frame,2,99,1,5)==1);
Check(TargetRadiusPolicy.Adjust(frame with {TargetAddress=3,TargetEntityId=30},2,20,1,5)==1);
Check(TargetRadiusPolicy.Adjust(frame with {Ready=false},2,20,1,5)==1);
Check(TargetRadiusPolicy.Adjust(frame with {TargetAddress=1,TargetEntityId=10},1,10,1,5)==1);
Check(TargetRadiusPolicy.Adjust(frame,2,20,-1,5)==-1);
Check(float.IsNaN(TargetRadiusPolicy.Adjust(frame,2,20,float.NaN,5)));
Check(TargetRadiusPolicy.Adjust(frame,2,20,1,float.NaN)==1);
});
Test("movement catalogue includes player displacement skills and excludes NPCs",()=>
{
var skills=MovementSkillCatalog.Select([
new(7386,"Onslaught",5189,true,true,false),new(29079,"Onslaught",12244,true,true,true),
new(42,"NPC movement",111,false,true,false),new(43,"Ordinary skill",222,true,false,false),
new(44,"Unmapped movement",0,true,true,false),new(45,"Shared",5189,true,true,false)]);
Check(skills.Length==4 && skills.Count(MovementSkillCatalog.Mappable)==3);
Check(skills.Where(MovementSkillCatalog.Mappable).Select(s=>s.Timeline).Distinct().Count()==2);
Check(!skills.Any(s=>s.ActionId is 42 or 43));
});
Test("audited status candidates exclude mismatched legacy entries",()=>
{
Check(StatusCatalog.AuditedBlockCandidates.Count==38);
Check(StatusCatalog.RejectedLegacyBlockIds.All(id=>!StatusCatalog.AuditedBlockCandidates.Contains(id)));
Check(StatusCatalog.AuditedBlockCandidates.Contains(604));
Check(StatusCatalog.AuditedBlockCandidates.Contains(1293));
Check(StatusCatalog.AuditedBlockCandidates.Contains(1422));
Check(!StatusCatalog.AuditedBlockCandidates.Contains(142));
});
Test("slow guard includes newly audited player Heavy rows",()=>
{
Check(StatusCatalog.SlowCandidates.Count==37 && StatusCatalog.AuditedSlow.Count==40);
Check(new uint[]{5044,5175,5356}.All(StatusCatalog.AuditedSlow.Contains));
Check(!StatusCatalog.AuditedSlow.Contains(630));
});
Test("permission, action and cooldown identifiers stay distinct",()=>
{
Check(NativeIds.MovementPermissionChecks.Contains(96));
Check(!NativeIds.MovementPermissionChecks.Contains(14));
Check(NativeIds.MudraActions.Contains(2259));
Check(!Policies.IsMudra(233)); // 233 is Ten's animation timeline, not its action ID.
Check(!Policies.IsMudra(-1));
Check(NativeIds.GlobalCooldownGroup==57);
});
Test("render diagnostics scope is independent of action diagnostics scope",()=>
{
Check(!NativeTargetRingScope.Active);
using(NativeTargetRingScope.Enter(true))
{
Check(NativeTargetRingScope.Active && !ActionRangeCheckScope.Active);
using(NativeTargetRingScope.Enter(false)) Check(!NativeTargetRingScope.Active);
Check(NativeTargetRingScope.Active);
}
Check(!NativeTargetRingScope.Active);
});
Test("render diagnostics scope unwinds after errors",()=>
{
using(NativeTargetRingScope.Enter(true))
{
bool workerState=true;
var worker=new Thread(()=>workerState=NativeTargetRingScope.Active); worker.Start(); worker.Join(); Check(!workerState);
}
try { using var scope=NativeTargetRingScope.Enter(true); throw new InvalidOperationException(); } catch(InvalidOperationException) { }
Check(!NativeTargetRingScope.Active);
});
Test("shared range resource releases its leases independently",()=>
{
int starts=0,stops=0;
var shared=new SharedModuleResource(s=>{starts++;s.Defer(()=>stops++);});
var a=new ResourceScope(); var b=new ResourceScope();
shared.Acquire(a); shared.Acquire(b); Check(starts==1);
a.Dispose(); Check(stops==0);
b.Dispose(); Check(stops==1);
using var c=new ResourceScope(); shared.Acquire(c); Check(starts==2);
});
Test("shared range startup failure cleans its partial resources",()=>
{
var stopped=false;
var shared=new SharedModuleResource(s=>{s.Defer(()=>stopped=true);throw new InvalidOperationException();});
using var owner=new ResourceScope(); Throws<InvalidOperationException>(()=>shared.Acquire(owner)); Check(stopped);
});
Test("failed shared cleanup prevents another module from attaching again",()=>
{
int starts=0;
var shared=new SharedModuleResource(s=>{starts++;s.Defer(()=>throw new InvalidOperationException("native cleanup failed"));});
var first=new ResourceScope(); shared.Acquire(first); Throws<AggregateException>(first.Dispose);
using var second=new ResourceScope(); Throws<InvalidOperationException>(()=>shared.Acquire(second)); Check(starts==1);
});
Test("explicit user validation persists across enabling and disabling",()=>
{
using var manager=new ModuleManager(); manager.Register(1,_=>{});
ValidationCatalog.Apply(manager,ValidationCatalog.ClientVersion);
Check(StatusPresentation.Summary(manager.Status[1])=="已实测有效 · 已关闭");
Check(manager.Enable(1)); Check(StatusPresentation.Summary(manager.Status[1])=="已实测有效 · 已开启");
manager.Disable(1); Check(StatusPresentation.Summary(manager.Status[1])=="已实测有效 · 已关闭");
Check(!StatusPresentation.ShowDetail(manager.Status[1]));
});
Test("untested unavailable and runtime failures have distinct wording",()=>
{
using var manager=new ModuleManager(); manager.Register(14,_=>{}); manager.Register(20,_=>{});
Check(StatusPresentation.Summary(manager.Status[14])=="待复测 · 已关闭");
Check(StatusPresentation.Summary(manager.Status[20])=="待实测 · 已关闭");
Check(StatusPresentation.Summary(manager.Status[8])=="暂不可用 · 已关闭");
manager.Status[14].State=ModuleState.Failed; Check(StatusPresentation.ShowDetail(manager.Status[14]));
Check(StatusPresentation.Summary(manager.Status[14]).Contains("运行异常"));
});
Test("validation list matches user confirmation and is version-specific",()=>
{
Check(ValidationCatalog.UserConfirmed.SetEquals(new[]{1,2,3,9,10,11,13,14,15,16,17,19,27,28,29}));
using var manager=new ModuleManager(); manager.Register(1,_=>{});
ValidationCatalog.Apply(manager,"other-version"); Check(manager.Status[1].Adaptation!=Adaptation.Verified);
manager.Register(14,_=>{}); ValidationCatalog.Apply(manager,ValidationCatalog.ClientVersion);
Check(manager.Status[14].Adaptation==Adaptation.Verified);
manager.Status[14].Adaptation=Adaptation.Testable;
ValidationCatalog.Apply(manager,ValidationCatalog.BaselineClientVersion);
Check(manager.Status[14].Adaptation==Adaptation.Verified);
});
Test("height preset is inactive until applied and second click returns zero",()=>
{
var height=new HeightActivation(); var preset=-7f;
Check(height.Applied==0 && !height.IsApplied);
height.Toggle(true,preset); Check(height.Applied==-7 && height.IsApplied);
preset=-3; Check(height.Applied==-7);
height.Toggle(true,preset); Check(height.Applied==0 && !height.IsApplied);
height.Toggle(true,preset); Check(height.Applied==-3);
});
Test("height activation rejects disabled module and resets between sessions",()=>
{
var height=new HeightActivation(); height.Toggle(false,-7); Check(height.Applied==0);
height.Toggle(true,-7); height.Reset(); Check(height.Applied==0);
height.Toggle(true,float.NaN); Check(height.Applied==0);
height.Toggle(true,999); Check(height.Applied==10);
height.Toggle(false,-4); Check(height.Applied==0);
Check(new HeightActivation().Applied==0);
});
Test("service bar distinguishes prepared applied and dungeon-suppressed height",()=>
{
var frame=new GameFrame(true,123,false,false,new(true,1,1,3));
var prepared=ServiceBarPresentation.Build(frame,ModuleState.Enabled,ModuleState.Disabled,ModuleState.Disabled,-7,0);
Check(prepared.Shown && prepared.Text.Contains("Y:SET") && prepared.Tooltip.Contains("effective 0"));
var active=ServiceBarPresentation.Build(frame,ModuleState.Enabled,ModuleState.Enabled,ModuleState.Enabled,-3,-7);
Check(active.Text=="IC Y:-7 D:1 F:3" && active.Tooltip.Contains("Preset -3, applied -7"));
var special=ServiceBarPresentation.Build(frame with {Dungeon=new(true,1,1,10)},ModuleState.Enabled,ModuleState.Enabled,ModuleState.Enabled,-7,-7);
Check(special.Text.Contains("Y:HOLD") && special.Text.Contains("F:10*") && special.Tooltip.Contains("effective 0"));
Check(new[]{prepared,active,special}.All(s=>s.Text.All(c=>c<128) && s.Tooltip.All(c=>c<128)));
});
Test("service bar clears stale state during logout and reports disabled or failed modules",()=>
{
var frame=new GameFrame(true,123,false,false,DungeonSnapshot.Empty);
var outside=ServiceBarPresentation.Build(frame,ModuleState.Enabled,ModuleState.Enabled,ModuleState.Enabled,-7,-7);
Check(outside.Text.Contains("Y:HOLD") && outside.Text.Contains("F:--"));
var off=ServiceBarPresentation.Build(frame,ModuleState.Disabled,ModuleState.Disabled,ModuleState.Disabled,-7,0);
Check(off.Text=="IC Y:OFF D:0 F:OFF");
var failed=ServiceBarPresentation.Build(frame,ModuleState.Failed,ModuleState.Disabled,ModuleState.Failed,-7,0);
Check(failed.Text.Contains("Y:ERR") && failed.Text.Contains("F:ERR"));
Check(!ServiceBarPresentation.Build(frame with {Ready=false},ModuleState.Enabled,ModuleState.Enabled,ModuleState.Enabled,-7,-7).Shown);
});
Test("native up capture uses declared length and excludes the 32-byte send prefix",()=>
{
var payload=Enumerable.Range(0,28).Select(i=>(byte)i).ToArray();
var memory=PacketMemoryFixture.Up(payload);
var result=NativePacketCapture.Up(memory.Read,0x1000,_=>true);
Check(result.Error==null && result.Packet is {Complete:true,Opcode:838,Target:null});
Check(result.Packet!.Payload.SequenceEqual(payload));
Check(memory.Reads.SequenceEqual(new[]{((nint)0x1000,16),((nint)0x1020,28)}));
result.Packet.Payload[0]=255; Check(memory.Blocks[0x1020][0]==0);
});
Test("native down capture follows owned message and excludes the IPC header",()=>
{
byte[] payload=[1,3,5,7,9]; var memory=PacketMemoryFixture.Down(payload);
var result=NativePacketCapture.Down(memory.Read,0x1000,_=>true);
Check(result.Error==null && result.Packet is {Complete:true,Opcode:354,Target:0x12345678});
Check(result.Packet!.Payload.SequenceEqual(payload));
Check(memory.Reads.SequenceEqual(new[]{((nint)0x1010,8),((nint)0x2020,32),((nint)0x3000,4),((nint)0x3010,5)}));
});
Test("native invalid lengths fall back without reading or allocating payloads",()=>
{
foreach(var length in new ulong[]{0,3,15,65553,0x100000010,ulong.MaxValue})
{
var up=PacketMemoryFixture.Up([],length);
var result=NativePacketCapture.Up(up.Read,0x1000,_=>true);
Check(result.Packet is {Complete:false,Payload.Length:0,Issue:not null}); Check(up.Reads.Count==1);
var down=PacketMemoryFixture.Down([],length);
var received=NativePacketCapture.Down(down.Read,0x1000,_=>true);
Check(length<4 ? received.Error!=null : received.Packet is {Complete:false,Payload.Length:0,Issue:not null});
Check(down.Reads.All(r=>r.Address!=0x3010));
}
});
Test("native truncated and partial memory reads never become complete packets",()=>
{
var up=PacketMemoryFixture.Up([1,2],32);
var down=PacketMemoryFixture.Down([1,2],32);
Check(NativePacketCapture.Up(up.Read,0x1000,_=>true).Packet is {Complete:false,Payload.Length:0});
Check(NativePacketCapture.Down(down.Read,0x1000,_=>true).Packet is {Complete:false,Payload.Length:0});
up.ShortReads=true; Check(NativePacketCapture.Up(up.Read,0x1000,_=>true).Error!=null);
down.Blocks.Remove(0x2020); Check(NativePacketCapture.Down(down.Read,0x1000,_=>true).Error!=null);
});
Test("native filters avoid copying payload and paused buffer rejects new records",()=>
{
var up=PacketMemoryFixture.Up([1,2,3]); var down=PacketMemoryFixture.Down([1,2,3]);
Check(NativePacketCapture.Up(up.Read,0x1000,_=>false).Packet==null && up.Reads.Count==1);
Check(NativePacketCapture.Down(down.Read,0x1000,_=>false).Packet==null && down.Reads.Count==3);
var buffer=new PacketBuffer(); Check(!buffer.Accepts(PacketDirection.Up));
buffer.Paused=false; buffer.DirectionFilter=PacketDirection.Up; buffer.OpcodeFilter=838;
Check(buffer.Accepts(PacketDirection.Up) && buffer.Accepts(PacketDirection.Up,838));
Check(!buffer.Accepts(PacketDirection.Down) && !buffer.Accepts(PacketDirection.Up,839));
buffer.Paused=true; Check(!buffer.Add(PacketDirection.Up,838,"v",null,null,[1]));
});
Test("native payload bounds include true empty payload and exact 64 KiB maximum",()=>
{
Check(NativePacketCapture.TryPayloadLength(16,out var empty) && empty==0);
Check(NativePacketCapture.TryPayloadLength(65552,out var maximum) && maximum==65536);
Check(!NativePacketCapture.TryPayloadLength(65553,out _));
var zero=PacketMemoryFixture.Down([]);
Check(NativePacketCapture.Down(zero.Read,0x1000,_=>true).Packet is {Complete:true,Payload.Length:0});
var full=PacketMemoryFixture.Up(new byte[65536]);
Check(NativePacketCapture.Up(full.Read,0x1000,_=>true).Packet is {Complete:true,Payload.Length:65536});
});
Test("native invalid pointers are rejected before memory access",()=>
{
var memory=new PacketMemoryFixture();
foreach(var pointer in new nint[]{0,-1,nint.MaxValue})
{
Check(NativePacketCapture.Up(memory.Read,pointer,_=>true).Error!=null);
Check(NativePacketCapture.Down(memory.Read,pointer,_=>true).Error!=null);
}
Check(memory.Reads.Count==0);
var down=PacketMemoryFixture.Down([]);
BinaryPrimitives.WriteUInt64LittleEndian(down.Blocks[0x1010],ulong.MaxValue);
Check(NativePacketCapture.Down(down.Read,0x1000,_=>true).Error!=null && down.Reads.Count==1);
});
Test("native downstream target normalization matches reserved IDs",()=>
{
Check(NativePacketCapture.NormalizeTarget(0x12345678)==0x12345678);
Check(NativePacketCapture.NormalizeTarget(0x5FF80000)==0x5FF80002);
Check(NativePacketCapture.NormalizeTarget(0x5FF80001)==0x5FF80001);
Check(NativePacketCapture.NormalizeTarget(0xFFFFFFFF)==0x5FF80001);
});
Test("capture failure reason survives export and legacy metadata remains incomplete",()=>
{
var path=Path.Combine(AppContext.BaseDirectory,"capture-issue-"+Guid.NewGuid()+".jsonl");
try
{
var buffer=new PacketBuffer {Paused=false};
buffer.Add(PacketDirection.Up,838,"v",null,null,[],false,"test failed copy");
PacketFiles.ExportAtomic(path,buffer.Snapshot());
var packet=PacketFiles.Import(path).Single(); Check(!packet.PayloadComplete && packet.CaptureIssue=="test failed copy");
var legacy=System.Text.Json.JsonSerializer.Serialize(packet with {CaptureIssue=null});
legacy=legacy.Replace(",\"CaptureIssue\":null",""); File.WriteAllText(path,legacy);
var old=PacketFiles.Import(path).Single(); Check(!old.PayloadComplete && old.CaptureIssue==null);
}
finally { File.Delete(path); }
});
Test("full-payload buffers bound total bytes across capture import and clear",()=>
{
var buffer=new PacketBuffer(10,16,5) {Paused=false};
Check(buffer.Add(PacketDirection.Up,1,"v",null,null,[1,2,3]));
Check(buffer.Add(PacketDirection.Down,2,"v",null,null,[4,5,6]));
Check(buffer.PayloadBytes==3 && buffer.Dropped==1 && buffer.Snapshot().Single().Opcode==2);
Check(!buffer.Add(PacketDirection.Up,3,"v",null,null,new byte[6])); Check(buffer.PayloadBytes==3);
var records=Enumerable.Range(0,4).Select(i=>new PacketRecord(i,i,DateTimeOffset.UtcNow,PacketDirection.Up,i,"v",null,null,new byte[2])).ToArray();
buffer.Replace(records); Check(buffer.PayloadBytes==4 && buffer.Dropped==2 && buffer.Snapshot().Select(p=>p.Opcode).SequenceEqual([2,3]));
buffer.Clear(); Check(buffer.PayloadBytes==0 && buffer.Dropped==0);
});
Test("current movement and ping payload lengths are version-bound",()=>
{
const string v=PacketParser.AuditedClientVersion;
Check(PacketParser.Parse("UP_UpdatePositionHandler",new byte[24],v).Parsed);
Check(!PacketParser.Parse("UP_UpdatePositionHandler",new byte[40],v).Parsed);
Check(!PacketParser.Parse("UP_UpdatePositionHandler",new byte[23],v).Parsed);
Check(PacketParser.Parse("UP_UpdatePositionHandler",new byte[40]).Parsed);
Check(PacketParser.Parse("UP_PingReq",new byte[32],v).Parsed);
Check(!PacketParser.Parse("UP_PingReq",new byte[28],v).Parsed);
Check(PacketParser.Parse("DOWN_PingRsp",new byte[32],v).Parsed);
Check(!PacketParser.Parse("DOWN_PingRsp",new byte[16],v).Parsed);
var bad=new byte[24]; BinaryPrimitives.WriteSingleLittleEndian(bad.AsSpan(8),float.NaN);
Check(!PacketParser.Parse("UP_UpdatePositionHandler",bad,v).Parsed);
});
Test("verified PingRsp fills only missing mappings for the audited version",()=>
{
const string v=PacketParser.AuditedClientVersion;
var map=new OpcodeMap();
map.Load($$$"""{"{{{v}}}":{"UP_PingReq":[838]}}""",v,"test");
Check(map.Resolve(v,PacketDirection.Down,581)=="DOWN_PingRsp"); Check(map.Snapshot.Source.Contains("本机核实"));
map.Load($$$"""{"{{{v}}}":{"DOWN_Explicit":[581]}}""",v,"test");
Check(map.Resolve(v,PacketDirection.Down,581)=="DOWN_Explicit");
map.Load($$$"""{"{{{v}}}":{"DOWN_PingRsp":[582]}}""",v,"test");
Check(map.Resolve(v,PacketDirection.Down,581)==null);
map.Load("""{"other":{"UP_PingReq":[838]}}""","other","test"); Check(map.Resolve("other",PacketDirection.Down,581)==null);
Throws<InvalidDataException>(()=>map.Load($$$"""{"{{{v}}}":{}}""",v,"test"));
});
Test("current Ping pairs require full exact-length payload and matching echo",()=>
{
const string v=PacketParser.AuditedClientVersion;
var map=new OpcodeMap(); map.Load($$$"""{"{{{v}}}":{"UP_PingReq":[838]}}""",v,"test");
var request=new byte[32]; var response=new byte[32];
BinaryPrimitives.WriteUInt32LittleEndian(request,14606677); BinaryPrimitives.WriteUInt32LittleEndian(response,14606677);
BinaryPrimitives.WriteUInt32LittleEndian(response.AsSpan(4),333); // not RTT; do not use it as elapsed time
var time=DateTimeOffset.UtcNow;
var a=new PacketRecord(1,0,time,PacketDirection.Up,838,v,null,null,request);
var b=new PacketRecord(2,0,time.AddMilliseconds(83),PacketDirection.Down,581,v,null,null,response);
var pairs=PacketCorrelator.Ping([a,b],map); Check(pairs.Length==1 && pairs[0].Elapsed.TotalMilliseconds==83);
Check(PacketCorrelator.Ping([a with {Payload=new byte[28]},b],map).Length==0);
Check(PacketCorrelator.Ping([a,b with {PayloadComplete=false}],map).Length==0);
Check(PacketCorrelator.Ping([a,b with {Payload=new byte[32]}],map).Length==0);
});
Test("audited protocol additions preserve explicit opcode and name assignments",()=>
{
var down=new Dictionary<int,string>{{671,"DOWN_Explicit"},{100,"DOWN_UpdateHater"}};
Check(CurrentProtocolEvidence.AddMissing("other",down)==0);
Check(CurrentProtocolEvidence.AddMissing(PacketParser.BaselineClientVersion,down)==5);
Check(down[671]=="DOWN_Explicit" && !down.ContainsKey(558));
Check(down[697]=="DOWN_EffectResultBasic" && down[338]=="DOWN_PingRsp");
Check(CurrentProtocolEvidence.AddMissing(PacketParser.BaselineClientVersion,down)==0);
var downAudited=new Dictionary<int,string>{{673,"DOWN_Explicit"},{100,"DOWN_UpdateHater"}};
Check(CurrentProtocolEvidence.AddMissing(PacketParser.AuditedClientVersion,downAudited)==5);
Check(downAudited[673]=="DOWN_Explicit" && !downAudited.ContainsKey(171));
Check(downAudited[549]=="DOWN_EffectResultBasic" && downAudited[581]=="DOWN_PingRsp");
Check(CurrentProtocolEvidence.AddMissing(PacketParser.AuditedClientVersion,downAudited)==0);
});
Test("enmity parsers respect capacities and native byte value stride",()=>
{
foreach(var (name,capacity) in new[]{("DOWN_UpdateHate",8),("DOWN_UpdateHater",32)})
{
var payload=new byte[8+capacity*8]; payload[0]=(byte)capacity;
for(var i=0;i<capacity;i++) { BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4+i*8),(uint)(100+i)); payload[8+i*8]=(byte)i; payload[9+i*8]=255; }
var parsed=PacketParser.Parse(name,payload,PacketParser.AuditedClientVersion);
Check(parsed.Parsed && parsed.Summary.Contains("0x00000064:0"));
payload[0]=(byte)(capacity+1); Check(!PacketParser.Parse(name,payload,PacketParser.AuditedClientVersion).Parsed);
payload[0]=0; Check(PacketParser.Parse(name,payload,PacketParser.AuditedClientVersion).Parsed);
Check(!PacketParser.Parse(name,payload[..^1],PacketParser.AuditedClientVersion).Parsed);
Check(!PacketParser.Parse(name,payload,"other").Parsed);
}
});
Test("basic effect result decodes only audited fields and rejects unsupported counts",()=>
{
var payload=new byte[24]; payload[0]=1;
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4),123);
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(12),456); payload[16]=7;
var result=PacketParser.Parse("DOWN_EffectResultBasic",payload,PacketParser.AuditedClientVersion);
Check(result.Parsed && result.Summary.Contains("序列=123") && result.Summary.Contains("HP=456") && result.Summary.Contains("目标序号=7"));
payload[0]=2; Check(!PacketParser.Parse("DOWN_EffectResultBasic",payload,PacketParser.AuditedClientVersion).Parsed);
payload[0]=0; Check(PacketParser.Parse("DOWN_EffectResultBasic",payload,PacketParser.AuditedClientVersion).Parsed);
Check(!PacketParser.Parse("DOWN_EffectResultBasic",payload[..^1],PacketParser.AuditedClientVersion).Parsed);
Check(!PacketParser.Parse("DOWN_EffectResultBasic",payload,"other").Parsed);
});
Test("new protocol parsers bound arbitrary lengths and counts",()=>
{
var random=new Random(671697558);
string[] names=["DOWN_UpdateHate","DOWN_UpdateHater","DOWN_EffectResultBasic"];
for(var i=0;i<1500;i++) { var payload=new byte[random.Next(0,300)]; random.NextBytes(payload); PacketParser.Parse(names[i%3],payload,PacketParser.AuditedClientVersion); }
});
Test("remaining fixed packets decode only proven fields and retain 64-bit tag identity",()=>
{
var p=new byte[16]; p[0]=31;
BinaryPrimitives.WriteUInt16LittleEndian(p.AsSpan(2),100);
BinaryPrimitives.WriteUInt32LittleEndian(p.AsSpan(8),123456);
Check(PacketParser.Parse("DOWN_JobGauge",p,PacketParser.AuditedClientVersion).Summary.Contains("职业 ID=31"));
var info=PacketParser.Parse("DOWN_UpdateClassInfo",p,PacketParser.AuditedClientVersion);
Check(info.Parsed && info.Summary.Contains("当前等级=100") && info.Summary.Contains("经验=123456"));
p[0]=2; BinaryPrimitives.WriteUInt64LittleEndian(p.AsSpan(8),0x123456789ABCDEF0);
var tag=PacketParser.Parse("DOWN_FirstAttack",p,PacketParser.AuditedClientVersion);
Check(tag.Parsed && tag.Summary.Contains("类型=2") && tag.Summary.Contains("123456789ABCDEF0"));
foreach(var name in new[]{"DOWN_JobGauge","DOWN_UpdateClassInfo","DOWN_FirstAttack"})
{
Check(!PacketParser.Parse(name,p[..15],PacketParser.AuditedClientVersion).Parsed);
Check(!PacketParser.Parse(name,new byte[17],PacketParser.AuditedClientVersion).Parsed);
Check(!PacketParser.Parse(name,p,"other").Parsed);
}
});
Test("gauge evidence label preserves explicit ActorGauge mapping",()=>
{
var mapBaseline=new Dictionary<int,string>{{8,"DOWN_ActorGauge"}};
CurrentProtocolEvidence.AddMissing(PacketParser.BaselineClientVersion,mapBaseline);
Check(mapBaseline[8]=="DOWN_ActorGauge" && mapBaseline[614]=="DOWN_JobGauge");
Check(mapBaseline[788]=="DOWN_UpdateClassInfo" && mapBaseline[214]=="DOWN_FirstAttack");
var mapAudited=new Dictionary<int,string>{{8,"DOWN_ActorGauge"}};
CurrentProtocolEvidence.AddMissing(PacketParser.AuditedClientVersion,mapAudited);
Check(mapAudited[8]=="DOWN_ActorGauge" && mapAudited[748]=="DOWN_JobGauge");
Check(mapAudited[592]=="DOWN_UpdateClassInfo" && mapAudited[374]=="DOWN_FirstAttack");
});
Test("stage 2 upstream action, trigger and event tier parsers enforce audited sizes and capacities",()=>
{
const string v=PacketParser.AuditedClientVersion;
var act=new byte[32]; BinaryPrimitives.WriteUInt32LittleEndian(act,7535); act[5]=1; BinaryPrimitives.WriteUInt16LittleEndian(act.AsSpan(6),180);
Check(PacketParser.Parse("UP_ActionSend",act,v).Summary.Contains("请求序号=180"));
Check(!PacketParser.Parse("UP_ActionSend",new byte[24],v).Parsed && !PacketParser.Parse("UP_ActionSend",act,"other").Parsed);
var pos=new byte[24]; BinaryPrimitives.WriteUInt32LittleEndian(pos,3573); pos[5]=1; BinaryPrimitives.WriteUInt16LittleEndian(pos.AsSpan(6),181);
BinaryPrimitives.WriteSingleLittleEndian(pos.AsSpan(12),10f); BinaryPrimitives.WriteSingleLittleEndian(pos.AsSpan(16),20f); BinaryPrimitives.WriteSingleLittleEndian(pos.AsSpan(20),30f);
Check(PacketParser.Parse("UP_ActionSendPos",pos,v).Parsed);
Check(!PacketParser.Parse("UP_ActionSendPos",new byte[32],v).Parsed);
BinaryPrimitives.WriteSingleLittleEndian(pos.AsSpan(16),float.NaN); Check(!PacketParser.Parse("UP_ActionSendPos",pos,v).Parsed);
var trig=new byte[32]; BinaryPrimitives.WriteUInt32LittleEndian(trig,3); BinaryPrimitives.WriteUInt32LittleEndian(trig.AsSpan(4),1);
Check(PacketParser.Parse("UP_ClientTrigger",trig,v).Summary.Contains("指令=3"));
Check(!PacketParser.Parse("UP_ClientTrigger",new byte[24],v).Parsed);
foreach(var (suffix,cap,len) in new[]{("",2,16),("4",4,24),("8",8,40),("16",16,72),("32",32,136),("64",64,264),("128",128,520),("255",255,1032)})
{
foreach(var prefix in new[]{"UP_EventFinish","UP_EventAction"})
{
var p=new byte[len]; p[7]=(byte)cap;
Check(PacketParser.Parse(prefix+suffix,p,v).Parsed);
Check(!PacketParser.Parse(prefix+suffix,new byte[len-1],v).Parsed);
if(cap<255) { p[7]=(byte)(cap+1); Check(!PacketParser.Parse(prefix+suffix,p,v).Parsed); }
}
}
});
Test("stage 2 downstream combat, status and spawn parsers enforce audited sizes, counts and finite floats",()=>
{
const string v=PacketParser.AuditedClientVersion;
Check(PacketParser.Parse("DOWN_ActorControl",new byte[24],v).Parsed && !PacketParser.Parse("DOWN_ActorControl",new byte[32],v).Parsed);
Check(PacketParser.Parse("DOWN_ActorControlSelf",new byte[40],v).Parsed && !PacketParser.Parse("DOWN_ActorControlSelf",new byte[32],v).Parsed);
Check(PacketParser.Parse("DOWN_ActorControlTarget",new byte[32],v).Parsed && !PacketParser.Parse("DOWN_ActorControlTarget",new byte[24],v).Parsed);
Check(PacketParser.Parse("DOWN_ActorDelete",new byte[8],v).Parsed && !PacketParser.Parse("DOWN_ActorDelete",new byte[16],v).Parsed);
var eff=new byte[124]; eff[0x21]=1; BinaryPrimitives.WriteUInt16LittleEndian(eff.AsSpan(0x18),190); BinaryPrimitives.WriteSingleLittleEndian(eff.AsSpan(0x10),0.6f);
Check(PacketParser.Parse("DOWN_Effect",eff,v).Summary.Contains("请求序号=190"));
eff[0x21]=2; Check(!PacketParser.Parse("DOWN_Effect",eff,v).Parsed);
eff[0x21]=1; BinaryPrimitives.WriteSingleLittleEndian(eff.AsSpan(0x10),float.PositiveInfinity); Check(!PacketParser.Parse("DOWN_Effect",eff,v).Parsed);
Check(!PacketParser.Parse("DOWN_Effect",new byte[120],v).Parsed);
var er=new byte[96]; er[0]=1; er[0x19]=2; BinaryPrimitives.WriteUInt16LittleEndian(er.AsSpan(0x1E),1200); BinaryPrimitives.WriteSingleLittleEndian(er.AsSpan(0x24),15f);
Check(PacketParser.Parse("DOWN_EffectResult",er,v).Summary.Contains("状态数=2"));
er[0x19]=5; Check(!PacketParser.Parse("DOWN_EffectResult",er,v).Parsed);
er[0x19]=1; BinaryPrimitives.WriteSingleLittleEndian(er.AsSpan(0x24),float.NaN); Check(!PacketParser.Parse("DOWN_EffectResult",er,v).Parsed);
var legacyEr=new byte[92]; legacyEr[0]=1; Check(PacketParser.Parse("DOWN_EffectResult",legacyEr,"legacy").Parsed);
var sl=new byte[384]; sl[0]=37; sl[1]=100; BinaryPrimitives.WriteUInt16LittleEndian(sl.AsSpan(20),48); BinaryPrimitives.WriteSingleLittleEndian(sl.AsSpan(24),30f);
Check(PacketParser.Parse("DOWN_StatusEffectList",sl,v).Summary.Contains("非空槽位=1/30"));
BinaryPrimitives.WriteSingleLittleEndian(sl.AsSpan(24),float.NaN); Check(!PacketParser.Parse("DOWN_StatusEffectList",sl,v).Parsed);
var obj=new byte[64];
Check(PacketParser.Parse("DOWN_ObjectSpawn",obj,v).Parsed);
BinaryPrimitives.WriteSingleLittleEndian(obj.AsSpan(0x34),float.NaN); Check(!PacketParser.Parse("DOWN_ObjectSpawn",obj,v).Parsed);
var npc=new byte[656]; npc[0x86]=100;
Check(PacketParser.Parse("DOWN_NpcSpawn",npc,v).Summary.Contains("等级=100"));
BinaryPrimitives.WriteSingleLittleEndian(npc.AsSpan(0x200),float.NaN); Check(!PacketParser.Parse("DOWN_NpcSpawn",npc,v).Parsed);
var random=new Random(20261012);
string[] names=["UP_ActionSend","UP_ActionSendPos","UP_ClientTrigger","UP_EventFinish16","UP_EventAction32","DOWN_ActorControl","DOWN_ActorControlSelf","DOWN_ActorControlTarget","DOWN_Effect","DOWN_EffectResult","DOWN_StatusEffectList","DOWN_ActorDelete","DOWN_ObjectSpawn","DOWN_NpcSpawn"];
for(var i=0;i<1500;i++) { var payload=new byte[random.Next(0,700)]; random.NextBytes(payload); PacketParser.Parse(names[i%names.Length],payload,v); }
});
Test("action correlator pairs UP_ActionSend and UP_ActionSendPos with DOWN_Effect by positive RequestId",()=>
{
const string v=PacketParser.AuditedClientVersion;
var map=new OpcodeMap(); map.Load($$$"""{"{{{v}}}":{"UP_ActionSend":[913],"UP_ActionSendPos":[891],"DOWN_Effect":[787]}}""",v,"test");
var t=DateTimeOffset.UtcNow;
var send=new byte[32]; BinaryPrimitives.WriteUInt16LittleEndian(send.AsSpan(6),179);
var sendPos=new byte[24]; BinaryPrimitives.WriteUInt16LittleEndian(sendPos.AsSpan(6),180);
var eff1=new byte[124]; eff1[0x21]=1; BinaryPrimitives.WriteUInt16LittleEndian(eff1.AsSpan(0x18),179);
var eff2=new byte[124]; eff2[0x21]=1; BinaryPrimitives.WriteUInt16LittleEndian(eff2.AsSpan(0x18),180);
var effZero=new byte[124]; effZero[0x21]=1;
var records=new[]{
new PacketRecord(10,1,t,PacketDirection.Up,913,v,null,null,send),
new PacketRecord(11,2,t.AddMilliseconds(50),PacketDirection.Down,787,v,null,null,effZero),
new PacketRecord(12,3,t.AddMilliseconds(95),PacketDirection.Down,787,v,null,null,eff1),
new PacketRecord(13,4,t.AddMilliseconds(200),PacketDirection.Up,891,v,null,null,sendPos),
new PacketRecord(14,5,t.AddMilliseconds(280),PacketDirection.Down,787,v,null,null,eff2)
};
var pairs=PacketCorrelator.Actions(records,map);
Check(pairs.Length==2 && pairs[0].Request==10 && pairs[0].Response==12 && pairs[0].Elapsed.TotalMilliseconds==95);
Check(pairs[1].Request==13 && pairs[1].Response==14 && pairs[1].Elapsed.TotalMilliseconds==80);
});
Test("stage 3 M0 adaptation manifest distinguishes missing implementation, version mismatch, offset mismatch, signature issues and runtime faults",()=>
{
const string v=PacketParser.AuditedClientVersion;
using var manager=new ModuleManager();
foreach(var f in Features.All.Where(f=>f.Id!=20)) manager.Register(f.Id,_=>{});
var okReport=AdaptationManifest.Evaluate(manager,v,_=>1,true);
Check(okReport.ReadyCount==23 && okReport.BlockedCount==1 && okReport.ActiveSendReady);
Check(okReport.Entries.Single(m=>m.FeatureId==20).Issue==AdaptationIssue.MissingImplementation);
var baseReport=AdaptationManifest.Evaluate(manager,PacketParser.BaselineClientVersion,_=>1,true);
Check(baseReport.ReadyCount==23 && baseReport.BlockedCount==1 && baseReport.ActiveSendReady);
AdaptationManifest.ApplyGate(manager,okReport);
Check(manager.Enable(8) && manager.Enable(36) && !manager.Enable(20));
var verReport=AdaptationManifest.Evaluate(manager,"2026.99.99.0000.0000",_=>1,true);
Check(!verReport.ActiveSendReady && verReport.ActiveSendSummary.Contains("仅核对"));
Check(verReport.Entries.Single(m=>m.FeatureId==8).Issue==AdaptationIssue.VersionMismatch);
Check(verReport.Entries.Single(m=>m.FeatureId==36).Issue==AdaptationIssue.VersionMismatch);
Check(verReport.Entries.Single(m=>m.FeatureId==1).Issue==AdaptationIssue.None);
Check(verReport.Entries.Single(m=>m.FeatureId==37).Issue==AdaptationIssue.None);
AdaptationManifest.ApplyGate(manager,verReport);
Check(!manager.IsEnabled(8) && !manager.Enable(8) && !manager.Enable(36));
Check(manager.Enable(1) && manager.Enable(37));
AdaptationManifest.ApplyGate(manager,okReport);
Check(manager.Enable(8));
var offsetReport=AdaptationManifest.Evaluate(manager,v,_=>1,false);
Check(offsetReport.Entries.Single(m=>m.FeatureId==8).Issue==AdaptationIssue.OffsetMismatch);
Check(offsetReport.Entries.Single(m=>m.FeatureId==14).Issue==AdaptationIssue.None);
var multiSend=AdaptationManifest.Evaluate(manager,v,sig=>sig==AdaptationManifest.ActiveSendReceiverSignature ? 2 : 1,true);
Check(!multiSend.ActiveSendReady && multiSend.ActiveSendSummary.Contains("多重匹配"));
var missingSig=AdaptationManifest.Evaluate(manager,v,sig=>sig.StartsWith("40 53 56 41 56",StringComparison.Ordinal) ? 0 : 1,true);
Check(missingSig.Entries.Single(m=>m.FeatureId==8).Issue==AdaptationIssue.SignatureMissing);
manager.Status[14].State=ModuleState.Failed;
manager.Status[14].Detail="hook target fault";
var faultReport=AdaptationManifest.Evaluate(manager,v,_=>1,true);
var m14=faultReport.Entries.Single(m=>m.FeatureId==14);
Check(m14.Issue==AdaptationIssue.RuntimeFault && m14.Summary.Contains("hook target fault"));
});
Test("stage 3 M4 native send buffer layout and audited encoders roundtrip through NativePacketCapture and PacketParser",()=>
{
const string v=PacketParser.AuditedClientVersion;
var map=new OpcodeMap();
map.Load($$$"""{"{{{v}}}":{"UP_ActionSend":[913],"UP_ActionSendPos":[891],"UP_EventStart":[720],"UP_ClientTrigger":[963],"UP_EventFinish":[501],"UP_EventFinish4":[502],"UP_EventFinish8":[503],"UP_EventFinish16":[504],"UP_EventFinish32":[505],"UP_EventFinish64":[506],"UP_EventFinish128":[507],"UP_EventFinish255":[508],"UP_EventAction":[601],"UP_EventAction4":[602],"UP_EventAction8":[603],"UP_EventAction16":[604],"UP_EventAction32":[605],"UP_EventAction64":[606],"UP_EventAction128":[607],"UP_EventAction255":[608],"DOWN_Effect":[787],"DOWN_EventStart":[737],"DOWN_EventFinish":[217]}}""",v,"test");
var act=AuditedPacketEncoder.EncodeActionSend(map,v,7535,1,321,0x10001234);
Check(act.Name=="UP_ActionSend" && act.Opcode==913 && act.Payload.Length==32 && act.ExpectedResponseOpcode==787);
Check(PacketParser.Parse(act.Name,act.Payload,v).Summary.Contains("请求序号=321"));
var raw=act.NativeBuffer;
Check(raw.Length==NativeSendPacket.NativePrefixSize+act.Payload.Length);
var mem=new PacketMemoryFixture();
mem.Blocks[0x4000]=raw[..16];
mem.Blocks[0x4020]=raw[0x20..(0x20+act.Payload.Length)];
var captured=NativePacketCapture.Up(mem.Read,0x4000,_=>true);
Check(captured.Error==null && captured.Packet is {Complete:true,Opcode:913});
Check(captured.Packet!.Payload.SequenceEqual(act.Payload));
var pos=AuditedPacketEncoder.EncodeActionSendPos(map,v,188,1,322,new System.Numerics.Vector3(100.5f,20.0f,-50.25f),512,256);
Check(pos.Name=="UP_ActionSendPos" && pos.Opcode==891 && pos.Payload.Length==24 && pos.ExpectedResponseOpcode==787);
Check(PacketParser.Parse(pos.Name,pos.Payload,v).Summary.Contains("请求序号=322") && PacketParser.Parse(pos.Name,pos.Payload,v).Summary.Contains("X=100.500"));
var rawPos=pos.NativeBuffer;
Check(rawPos.Length==NativeSendPacket.NativePrefixSize+pos.Payload.Length);
mem.Blocks[0x5000]=rawPos[..16];
mem.Blocks[0x5020]=rawPos[0x20..(0x20+pos.Payload.Length)];
var capturedPos=NativePacketCapture.Up(mem.Read,0x5000,_=>true);
Check(capturedPos.Error==null && capturedPos.Packet is {Complete:true,Opcode:891});
Check(capturedPos.Packet!.Payload.SequenceEqual(pos.Payload));
Throws<ArgumentException>(()=>AuditedPacketEncoder.EncodeActionSendPos(map,v,188,1,322,new System.Numerics.Vector3(float.NaN,0,0)));
var evStart=AuditedPacketEncoder.EncodeEventStart(map,v,0x1000200030004000,0x00050001,7);
Check(evStart.Payload.Length==16 && evStart.ExpectedResponseOpcode==737 && PacketParser.Parse(evStart.Name,evStart.Payload,v).Parsed);
var trig=AuditedPacketEncoder.EncodeClientTrigger(map,v,3,1,2,3,4,0xABCDEF01);
Check(trig.Payload.Length==32 && PacketParser.Parse(trig.Name,trig.Payload,v).Summary.Contains("指令=3"));
foreach(var (count,expectedName,expectedLen) in new[]{(0,"UP_EventFinish",16),(2,"UP_EventFinish",16),(3,"UP_EventFinish4",24),(5,"UP_EventFinish8",40),(9,"UP_EventFinish16",72),(17,"UP_EventFinish32",136),(33,"UP_EventFinish64",264),(65,"UP_EventFinish128",520),(129,"UP_EventFinish255",1032)})
{
var args=Enumerable.Range(1,count).Select(i=>(uint)i).ToArray();
var fin=AuditedPacketEncoder.EncodeEventFinish(map,v,0x000A0001,0,0,args);
Check(fin.Name==expectedName && fin.Payload.Length==expectedLen && PacketParser.Parse(fin.Name,fin.Payload,v).Parsed);
var ea=AuditedPacketEncoder.EncodeEventAction(map,v,0x000A0001,0,0,args);
Check(ea.Name==expectedName.Replace("Finish","Action") && ea.Payload.Length==expectedLen && PacketParser.Parse(ea.Name,ea.Payload,v).Parsed);
}
Throws<ArgumentException>(()=>AuditedPacketEncoder.EncodeActionSend(map,v,1,1,0,0x10001234));
Throws<ArgumentOutOfRangeException>(()=>AuditedPacketEncoder.EncodeEventFinish(map,v,1,0,0,new uint[256]));
});
Test("stage 3 M4 ActiveSendScope tags plugin-initiated packets without affecting normal native Zone traffic",()=>
{
using var pipeline=new NetworkPipeline(PacketParser.AuditedClientVersion);
List<PacketContext> observed=[];
using var sub=pipeline.Subscribe(NetworkPhase.BeforeSend,obs=>observed.Add(obs.Context));
Check(pipeline.Send(()=>1,_=>new(new(913,null,new byte[32],true),null))==1);
using(ActiveSendScope.Enter())
{
Check(ActiveSendScope.Active);
Check(pipeline.Send(()=>1,_=>new(new(913,null,new byte[32],true),null))==1);
}
Check(!ActiveSendScope.Active);
Check(observed.Count==2);
Check(observed[0].Origin==PacketOrigin.NativeZone && observed[0].Phase==NetworkPhase.BeforeSend);
Check(observed[1].Origin==PacketOrigin.PluginActiveSend && observed[1].Phase==NetworkPhase.BeforeSend);
});
Test("stage 3 M4 ActivePacketDispatcher enforces rate limit, queue bounds, session/territory gate, cancel semantics, response correlation and recursive pump guard",()=>
{
const string v=PacketParser.AuditedClientVersion;
var map=new OpcodeMap();
map.Load($$$"""{"{{{v}}}":{"UP_ActionSend":[913],"UP_ClientTrigger":[963],"UP_EventStart":[720],"DOWN_Effect":[787],"DOWN_EventStart":[737]}}""",v,"test");
using var pipeline=new NetworkPipeline(v);
long now=1000;
var session=new SendSession(true,1108,(nint)0x2000,0x10000001);
byte nextNativeResult=1;
var dispatcher=new ActivePacketDispatcher(v,()=>now,maxQueue:4,minIntervalMs:100,queueTimeoutMs:1000,responseTimeoutMs:2000);
using var sub=dispatcher.Attach(pipeline);
byte NativeSender(ReadOnlySpan<byte> buf)
{
var opcode=BinaryPrimitives.ReadInt32LittleEndian(buf[..4]);
var payload=buf[NativeSendPacket.NativePrefixSize..].ToArray();
return pipeline.Send(()=>nextNativeResult,_=>new(new(opcode,null,payload,true),null));
}
var tx1=dispatcher.Enqueue(AuditedPacketEncoder.EncodeActionSend(map,v,7,1,501,0x10000001),session).Id;
var tx2=dispatcher.Enqueue(AuditedPacketEncoder.EncodeActionSend(map,v,7,1,502,0x10000001),session).Id;
var tx3=dispatcher.Enqueue(AuditedPacketEncoder.EncodeClientTrigger(map,v,3,1),session).Id;
Check(dispatcher.PendingQueueCount==3);
Check(dispatcher.Pump(session,NativeSender)?.Id==tx1);
Check(!dispatcher.Cancel(tx1)); // already SentNative
Check(dispatcher.Cancel(tx3,"test cancel"));
var snap1=dispatcher.Snapshot();
Check(snap1.Single(s=>s.Id==tx1) is {State:SendTransactionState.SentNative,NativeResult:1});
Check(snap1.Single(s=>s.Id==tx2).State==SendTransactionState.Queued);
Check(snap1.Single(s=>s.Id==tx3).State==SendTransactionState.Canceled);
// Rate limit prevents sending tx2 before +100ms
now=1050;
Check(dispatcher.Pump(session,NativeSender)==null);
// Confirm tx1 via DOWN_Effect with matching RequestId=501 and verify recursive Pump is forbidden inside Received callback
var eff=new byte[124]; eff[0x21]=1; BinaryPrimitives.WriteUInt16LittleEndian(eff.AsSpan(0x18),501);
bool recursiveRejected=false;
using(var guardSub=pipeline.Subscribe(NetworkPhase.Received,_=>
{
try { dispatcher.Pump(session,NativeSender); }
catch(InvalidOperationException) { recursiveRejected=true; }
}))
{
pipeline.Receive(()=>1,_=>new(new(787,0x10000001,eff,true),null));
}
Check(recursiveRejected);
var confirmed=dispatcher.Snapshot().Single(s=>s.Id==tx1);
Check(confirmed.State==SendTransactionState.Confirmed && confirmed.RoundTripMs==50 && confirmed.ResponseObservationId>0);
// Advance past rate limit and send tx2, then let it time out waiting for response
now=1100;
Check(dispatcher.Pump(session,NativeSender)?.Id==tx2);
Check(dispatcher.Snapshot().Single(s=>s.Id==tx2).State==SendTransactionState.SentNative);
now=3200;
dispatcher.Pump(session,NativeSender);
Check(dispatcher.Snapshot().Single(s=>s.Id==tx2).State==SendTransactionState.TimedOut);
// Territory change cancels queued items before native send
var tx4=dispatcher.Enqueue(AuditedPacketEncoder.EncodeActionSend(map,v,7,1,503,0x10000001),session).Id;
session=new SendSession(true,1109,(nint)0x2000,0x10000001);
now=3400;
Check(dispatcher.Pump(session,NativeSender)==null);
Check(dispatcher.Snapshot().Single(s=>s.Id==tx4).State==SendTransactionState.Canceled);
// Native return 0 marks transaction RejectedNative
nextNativeResult=0;
var tx5=dispatcher.Enqueue(AuditedPacketEncoder.EncodeActionSend(map,v,7,1,504,0x10000001),session).Id;
now=3600;
Check(dispatcher.Pump(session,NativeSender)?.Id==tx5);
Check(dispatcher.Snapshot().Single(s=>s.Id==tx5) is {State:SendTransactionState.RejectedNative,NativeResult:0});
});
Test("opcodes.json loads cleanly without conflicts and resolves dual-version audited opcodes",()=>
{
var path=Path.Combine(AppContext.BaseDirectory,"opcodes.json");
if(!File.Exists(path)) path=Path.Combine(AppContext.BaseDirectory,"..","..","..","opcodes.json");
if(!File.Exists(path)) path="opcodes.json";
Check(File.Exists(path));
var json=File.ReadAllText(path);
// 2026.10.06.0000.0000 audit verification
var mapAudited=new OpcodeMap();
mapAudited.Load(json,PacketParser.AuditedClientVersion,"local");
Check(mapAudited.ResolveOpcode(PacketParser.AuditedClientVersion,PacketDirection.Down,"DOWN_ActorControlTarget")==200);
Check(mapAudited.ResolveOpcode(PacketParser.AuditedClientVersion,PacketDirection.Down,"DOWN_ActorControlSelf")==486);
Check(mapAudited.ResolveOpcode(PacketParser.AuditedClientVersion,PacketDirection.Down,"DOWN_ActorControl")==408);
Check(mapAudited.ResolveOpcode(PacketParser.AuditedClientVersion,PacketDirection.Down,"DOWN_Effect")==189);
Check(mapAudited.ResolveOpcode(PacketParser.AuditedClientVersion,PacketDirection.Down,"DOWN_StatusEffectList")==718);
Check(mapAudited.ResolveOpcode(PacketParser.AuditedClientVersion,PacketDirection.Down,"DOWN_EffectResult")==242);
Check(mapAudited.ResolveOpcode(PacketParser.AuditedClientVersion,PacketDirection.Down,"DOWN_InventoryTransaction")==136);
Check(mapAudited.ResolveOpcode(PacketParser.AuditedClientVersion,PacketDirection.Up,"UP_ActionSend")==176);
Check(mapAudited.ResolveOpcode(PacketParser.AuditedClientVersion,PacketDirection.Up,"UP_ClientTrigger")==872);
Check(mapAudited.ResolveOpcode(PacketParser.AuditedClientVersion,PacketDirection.Down,"DOWN_PingRsp")==581);
Check(mapAudited.ResolveOpcode(PacketParser.AuditedClientVersion,PacketDirection.Up,"UP_PingReq")==137);
// 2026.09.15.0000.0000 baseline verification
var mapBase=new OpcodeMap();
mapBase.Load(json,PacketParser.BaselineClientVersion,"local");
Check(mapBase.ResolveOpcode(PacketParser.BaselineClientVersion,PacketDirection.Down,"DOWN_ActorControlTarget")==856);
Check(mapBase.ResolveOpcode(PacketParser.BaselineClientVersion,PacketDirection.Down,"DOWN_ActorControlSelf")==516);
Check(mapBase.ResolveOpcode(PacketParser.BaselineClientVersion,PacketDirection.Down,"DOWN_ActorControl")==607);
Check(mapBase.ResolveOpcode(PacketParser.BaselineClientVersion,PacketDirection.Down,"DOWN_Effect")==787);
Check(mapBase.ResolveOpcode(PacketParser.BaselineClientVersion,PacketDirection.Up,"UP_ActionSend")==913);
});
Console.WriteLine($"{passed} tests passed.");