using System.Diagnostics;
using System.Globalization;
using XFEEDToolBox.Core.Navigation;
namespace XFEEDToolBox.Core.Native;
/// <summary>Read-only provenance check for the original FSD and docking action registries.</summary>
public static class NativeDockingInputDiagnostics
{
public static Task<object> ReadAsync(int processId,CancellationToken token=default)=>ReadCoreAsync(processId,null,token);
public static Task<object> ReadForStationAsync(int processId,long marketId,CancellationToken token=default)
=>marketId is <=0 or >uint.MaxValue?throw new ArgumentOutOfRangeException(nameof(marketId)):ReadCoreAsync(processId,marketId,token);
private static async Task<object> ReadCoreAsync(int processId,long? stationMarketId,CancellationToken token)
{
using var process=Process.GetProcessById(processId);
var started=process.StartTime.ToUniversalTime().Ticks;
var pipe=new NavigationPipeClient(processId);
var frame=await pipe.CallAsync(new{op="health.read"},token);
static void FreshHealth(System.Text.Json.JsonElement value)
{
var age=DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()-value.GetProperty("observedAtUnixMilliseconds").GetInt64();
if(!value.GetProperty("localPlayerShipVerified").GetBoolean()||!value.GetProperty("shipHealth").GetProperty("verified").GetBoolean()||age is <0 or >500)
throw new InvalidDataException("Fresh verified main ship health is required for read-only input diagnostics.");
}
FreshHealth(frame);
var entity=ulong.Parse(frame.GetProperty("entityId").GetString()!,CultureInfo.InvariantCulture);
using var memory=new ReadOnlyPlayerMemory(processId,started,NativeSystemLocationReader.GameSha256);
var (fsd,life,root)=memory.Player(entity);
var computer=memory.Component(root,memory.U32(memory.ImageBase+0x5F5317C));
if(memory.U64(computer)!=memory.ImageBase+0x5165248)throw new InvalidDataException("Original docking computer differs.");
var fsdController=memory.U64(fsd+0x700);var dockingController=memory.U64(computer+0x240);
if(fsdController==0||fsdController!=dockingController||memory.U64(memory.U64(fsdController)+0x158)!=memory.ImageBase+0x25E5D10)
throw new InvalidDataException("Original throttle controller ownership differs.");
var fsdActions=memory.U64(fsd+0x6F8);var dockingActions=memory.U64(computer+0x230);
object Registry(ulong value,out Dictionary<string,ulong> bindings)
{
if(memory.U64(value)!=memory.ImageBase+0x512FD90||memory.U64(memory.U64(value)+0x40)!=memory.ImageBase+0x8FEDC0)
throw new InvalidDataException("Original action registry differs.");
var buckets=memory.U64(value+0xC0);var count=memory.U64(value+0xC8);
if(buckets==0||count is 0 or >4096)throw new InvalidDataException("Original action registry exceeds bound.");
bindings=new(StringComparer.Ordinal);
var seen=new HashSet<ulong>();object? zero=null,absoluteSet=null,thrust=null;
for(ulong i=0;i<count;i++)
{
var node=memory.U64(buckets+i*8);
while(node<buckets||node>=buckets+count*8)
{
if(node==0||!seen.Add(node)||seen.Count>4096)throw new InvalidDataException("Original action registry chain differs.");
var name=memory.U64(node+8);var length=memory.U64(name);
if(length is 0 or >128)throw new InvalidDataException("Original action name exceeds bound.");
var originalName=System.Text.Encoding.ASCII.GetString(memory.Read(name+0x14,(int)length));
if(originalName is "SetSpeedZero" or "ForwardReverseThrust_Set" or "ForwardReverseThrust")
{
var binding=memory.U64(node+0x10);var bytes=memory.Read(binding,0x38);bindings.Add(originalName,binding);
var bindingName=BitConverter.ToUInt64(bytes,8);var bindingLength=memory.U64(bindingName);
var bindingNameMatches=bindingLength==(ulong)originalName.Length&&memory.Read(bindingName+0x14,originalName.Length).AsSpan().SequenceEqual(System.Text.Encoding.ASCII.GetBytes(originalName));
var input=new{originalName,binding=binding.ToString("x"),mapName=name.ToString("x"),bindingName=bindingName.ToString("x"),bindingLength,bindingNameEqualsMapKey=bindingName==name,bindingNameMatches,
registryOwnerMatches=BitConverter.ToUInt64(bytes,0x10)==value+0xB8,
value=BitConverter.ToSingle(bytes,0x18),pending=BitConverter.ToInt32(bytes,0x1C),forced=bytes[0x24],consumed=BitConverter.ToInt32(bytes,0x2C),consumedForced=bytes[0x34]};
if(originalName=="SetSpeedZero")zero=input;else if(originalName=="ForwardReverseThrust_Set")absoluteSet=input;else thrust=input;
}
node=memory.U64(node);
}
}
if(memory.U64(value+0xC0)!=buckets||memory.U64(value+0xC8)!=count)throw new InvalidDataException("Original action registry changed.");
return new{address=value.ToString("x"),zero,absoluteSet,thrust};
}
var originalFsd=Registry(fsdActions,out _);var originalDocking=Registry(dockingActions,out var dockingBindings);
var physics=memory.U64(fsd+0x2A8)-0x58;var inputRoot=memory.U64(physics+0x3B0)-0x60;
if(memory.U64(inputRoot+0x2F8)!=fsdController)throw new InvalidDataException("Original ship input controller differs.");
var dockingThrottle=memory.U64(computer+0x260);
float Float(ulong address)=>BitConverter.ToSingle(memory.Read(address,4));
object domain;ulong domainTable=0,domainGetter=0,domainRoot=0;uint domainEventType=0;var eventSources=new HashSet<ulong>();
try
{
var context=memory.U64(fsdActions-0x50);var getter=memory.U64(context);var objectRoot=memory.LeafReference(fsdActions-0x50,getter);
domainTable=context;domainGetter=getter;domainRoot=objectRoot;var eventType=memory.U32(memory.ImageBase+0x5F2DFF4);domainEventType=eventType;
// Same original service-domain chain used by 869FC0. The pinned
// leaf getters and registry key are also checked by GalaxyCatalog.
if(memory.U64(memory.U64(objectRoot)+0x20)!=memory.ImageBase+0x8C51E0)throw new InvalidDataException("Original input service-domain getter differs.");
var scope=memory.LeafReference(objectRoot,memory.ImageBase+0x8C51E0);
if(scope==0||memory.U64(memory.U64(scope))!=memory.ImageBase+0x5299D0||memory.U32(memory.ImageBase+0x5F05AB8)!=1)throw new InvalidDataException("Original input scope differs.");
var scopeSlots=memory.U64(scope+0x20);var scopeCount=memory.U64(scope+0x28);if(scopeSlots==0||scopeCount is 0 or >1024)throw new InvalidDataException("Original input scope map exceeds bound.");
ulong serviceContext=0;var scopeSeen=new HashSet<ulong>();var scopeContexts=new List<(uint Kind,ulong Context)>();
for(ulong i=0;i<scopeCount;i++)
{
var head=scopeSlots+i*8;var item=memory.U64(head);
while(item!=head)
{
if(item<0x10000||!scopeSeen.Add(item)||scopeSeen.Count>1024)throw new InvalidDataException("Original input scope chain differs.");
var kind=memory.U32(item+8);var childContext=memory.U64(item+0x10);scopeContexts.Add((kind,childContext));
if(kind==1){if(serviceContext!=0)throw new InvalidDataException("Original service context is ambiguous.");serviceContext=childContext;}
item=memory.U64(item);
}
}
if(serviceContext==0||memory.U64(memory.U64(serviceContext)+0x18)!=memory.ImageBase+0x1F3150)throw new InvalidDataException("Original service context differs.");
var serviceLife=memory.U64(serviceContext+0x568);if(serviceLife==0||memory.U32(serviceLife+0xC0)!=8)throw new InvalidDataException("Original service life differs.");
var services=memory.U64(serviceLife+0xC8)-0x30;var events=memory.Component(services,eventType);
var table=events!=0?memory.U64(events):0;
if(table!=memory.ImageBase+0x512EC98||memory.U64(table+0x18)!=memory.ImageBase+0x90C170)throw new InvalidDataException("Original input event registry differs.");
eventSources.Add(events);var contextEvents=new List<object>();
foreach(var (kind,child) in scopeContexts)
{
try
{
var lifeGetter=memory.U64(memory.U64(child)+0x18);
if(lifeGetter!=memory.ImageBase+0x1F3150){contextEvents.Add(new{kind,lifeGetterRva=memory.InImage(lifeGetter)?(lifeGetter-memory.ImageBase).ToString("x"):"outside-image",checkedContext=false});continue;}
var childLife=memory.U64(child+0x568);if(childLife==0||memory.U32(childLife+0xC0)!=8)continue;
var childRegistry=memory.U64(childLife+0xC8)-0x30;var inputEvents=memory.Component(childRegistry,eventType);
if(memory.U64(inputEvents)!=memory.ImageBase+0x512EC98||memory.U64(memory.U64(inputEvents)+0x18)!=memory.ImageBase+0x90C170)throw new InvalidDataException("Different original event registry.");
eventSources.Add(inputEvents);contextEvents.Add(new{kind,events=inputEvents.ToString("x"),checkedContext=true});
}
catch(Exception error){contextEvents.Add(new{kind,issue=error.GetType().Name});}
}
domain=new{contextTable=context.ToString("x"),getterRva=memory.InImage(getter)?(getter-memory.ImageBase).ToString("x"):"outside-image",objectRoot=objectRoot.ToString("x"),serviceContext=serviceContext.ToString("x"),services=services.ToString("x"),eventType,events=events.ToString("x"),eventTableRva=memory.InImage(table)?(table-memory.ImageBase).ToString("x"):"outside-image",
subscribeRva=table!=0?(memory.U64(table+0x18)-memory.ImageBase).ToString("x"):"0",contextEvents};
}
catch(Exception error){domain=new{issue=error.GetType().Name,detail=error.Message,contextTable=domainTable.ToString("x"),getterRva=memory.InImage(domainGetter)?(domainGetter-memory.ImageBase).ToString("x"):"outside-image",objectRoot=domainRoot.ToString("x"),eventType=domainEventType};}
object dockingConsumer;
try
{
var updateObject=computer-0x58;var configuration=memory.U64(updateObject+0x50);var controls=memory.U64(updateObject+0x180);var policy=memory.U64(updateObject+0x198);
var stateInterface=memory.U64(updateObject+0x298);var stateTable=memory.U64(stateInterface);var flags=memory.Read(updateObject+0x3CA,6);
var profileMode=memory.I32(stateInterface+0x5F8);var profileIndex=memory.I32(stateInterface+0x560);var profileCount=memory.U32(stateInterface+0x530);var profile=stateInterface+0x2E0;
if(profileMode==2&&profileIndex>=0&&(uint)profileIndex<profileCount&&profileCount<=128)profile=memory.U64(stateInterface+0x538)+(ulong)profileIndex*0x248+0x18;
var fallback=memory.U64(stateInterface+0x80);var fallbackTable=memory.U64(fallback);var fallback78=memory.U64(fallbackTable+0x78);
string Getter(ulong offset){var getter=memory.U64(stateTable+offset);return memory.InImage(getter)?(getter-memory.ImageBase).ToString("x"):"unverified";}
object Interface(ulong pointer,params ulong[] offsets)
{
var table=memory.U64(pointer);if(!memory.InImage(table))throw new InvalidDataException("Original consumer interface is outside the pinned image.");
return new{address=pointer.ToString("x"),vtableRva=(table-memory.ImageBase).ToString("x"),slots=offsets.ToDictionary(offset=>offset.ToString("x"),offset=>{var method=memory.U64(table+offset);return memory.InImage(method)?(method-memory.ImageBase).ToString("x"):"unverified";})};
}
object Activity(ulong address)
{
if(address==0)return new{available=false};var table=memory.U64(address);var getter=memory.U64(table+0x20);
if(!memory.InImage(table)||!memory.InImage(getter))throw new InvalidDataException("Original activity interface is outside the pinned image.");
var code=memory.Read(getter,16);int? state=null;
if(code[0]==0xB8&&code[5]==0xC3)state=BitConverter.ToInt32(code,1);
else if(code[0]==0x31&&code[1]==0xC0&&code[2]==0xC3)state=0;
else if(code[0]==0x8B&&code[1]==0x81&&code[6]==0xC3)state=memory.I32(checked((ulong)((long)address+BitConverter.ToInt32(code,2))));
else if(code[0]==0x8B&&code[1]==0x41&&code[3]==0xC3)state=memory.I32(checked((ulong)((long)address+(sbyte)code[2])));
return new{available=true,address=address.ToString("x"),vtableRva=(table-memory.ImageBase).ToString("x"),stateGetterRva=(getter-memory.ImageBase).ToString("x"),leafState=state,functionsCalled=false};
}
var duty=memory.U64(updateObject+0x228);var currentActivity=memory.U64(duty+0x1E8);var activities=new List<object>();var activityBuckets=memory.U64(duty+0x1A8);var activityCapacity=memory.U64(duty+0x1B0);
if(activityCapacity>512)throw new InvalidDataException("Original activity set exceeds the read-only bound.");var activityNodes=new HashSet<ulong>();
for(ulong bucket=0;bucket<activityCapacity;bucket++)
{
var head=activityBuckets+bucket*8;var entry=memory.U64(head);
while(entry!=head)
{
if(entry<0x10000||!activityNodes.Add(entry)||activityNodes.Count>512)throw new InvalidDataException("Original activity set chain differs.");
activities.Add(Activity(memory.U64(entry+8)));entry=memory.U64(entry);
}
}
dockingConsumer=new{computer=computer.ToString("x"),updateObject=updateObject.ToString("x"),moduleSlot=memory.I32(updateObject+0x3B8),mode=memory.U32(updateObject+0x3BC),
timer3C0=Float(updateObject+0x3C0),timer3C4=Float(updateObject+0x3C4),flags3CAto3CF=flags.Select(x=>(int)x).ToArray(),
source=memory.U64(updateObject+0x2B8).ToString("x"),sourceMatches=memory.U64(updateObject+0x2B8)==dockingThrottle,
configuration=configuration.ToString("x"),threshold18=Float(configuration+0x18),delay1C=Float(configuration+0x1C),
controls=controls.ToString("x"),controlB8=memory.Read(controls+0xB8,1)[0],controlB9=memory.Read(controls+0xB9,1)[0],controlBB=memory.Read(controls+0xBB,1)[0],
policy=policy.ToString("x"),policy08=memory.Read(policy+8,1)[0],stateInterface=stateInterface.ToString("x"),stateTableRva=memory.InImage(stateTable)?(stateTable-memory.ImageBase).ToString("x"):"unverified",
stateGetter90Rva=Getter(0x90),stateGetterA0Rva=Getter(0xA0),profileMode,profileIndex,profileCount,profile=profile.ToString("x"),
profile1E5=memory.Read(profile+0x1E5,1)[0],profile1E6=memory.Read(profile+0x1E6,1)[0],profile1E8=memory.Read(profile+0x1E8,1)[0],
fallback78Rva=memory.InImage(fallback78)?(fallback78-memory.ImageBase).ToString("x"):"unverified",
commandF0=Interface(memory.U64(updateObject+0xF0),0,0x20,0x28,0x30,0x40,0x48,0x58,0x60),notify1B0=Interface(memory.U64(updateObject+0x1B0),0xF0,0xF8),
blockSourceA8=Interface(memory.U64(updateObject+0xA8),0x30),blockSet1C8=memory.U64(updateObject+0x1C8).ToString("x"),
duty228=Interface(memory.U64(updateObject+0x228),0xA0,0xA8),padC0=Interface(memory.U64(updateObject+0xC0),0xC8),
currentActivity=Activity(currentActivity),activities,activityCapacity,stationHandle2B0=memory.U64(updateObject+0x2B0).ToString("x"),functionsCalled=false,fieldsAreRawOffsets=true};
}
catch(Exception error){dockingConsumer=new{issue=error.GetType().Name,detail=error.Message,functionsCalled=false};}
var graph=ReadInputGraph(memory,dockingActions,dockingThrottle,dockingBindings,eventSources,token);
object stationService;
if(stationMarketId is null)stationService=new{requested=false,functionsCalled=false};
else try
{
var expectedMarket=(ulong)stationMarketId.Value;
var context=memory.U64(life+0x38);var count=memory.U64(context+0x148);var capacity=memory.U64(context+0x150);var storage=memory.U64(context+0x158);
if(capacity is <8 or >65536||(capacity&(capacity-1))!=0||count>capacity)throw new InvalidDataException("Original entity directory bound differs.");
var bits=memory.Read(storage,checked((int)((capacity+7)/8)));var entries=storage+(((capacity>>3)+7)&~7UL);var own=false;ulong stationLife=0;
for(ulong i=0;i<capacity;i++)
{
if((bits[i>>3]&(1<<(int)(i&7)))==0)continue;var entry=memory.Read(entries+i*16,16);var key=BitConverter.ToUInt64(entry,0);var value=BitConverter.ToUInt64(entry,8);
if(key==entity)own=value==life;if(key==expectedMarket){if(stationLife!=0)throw new InvalidDataException("Original target station is ambiguous.");stationLife=value;}
}
if(!own||stationLife==0||memory.U32(stationLife+0xC0)!=8||memory.U64(stationLife+0xB0)!=expectedMarket)throw new InvalidDataException("Original target station identity is unavailable.");
var stationRoot=memory.U64(stationLife+0xC8)-0x30;var type=memory.U32(memory.ImageBase+0x5F771D0);var metadata=memory.U64(stationRoot+0x10);var componentCount=memory.U64(metadata+0x18);
if(type is 0 or >99999||componentCount is 0 or >1024)throw new InvalidDataException("Original station component layout differs.");
var slots=memory.U64(metadata+0x30);var slot=memory.U64(slots+type*16UL);
if(slot>=componentCount)stationService=new{marketId=expectedMarket,entityVerified=true,componentAvailable=false,type,slot,componentCount,functionsCalled=false};
else
{
var service=memory.U64(memory.U64(stationRoot+0x18)+slot*8);var table=memory.U64(service);var active=memory.U64(table+0x28);var allowed=memory.U64(table+0x58);
if(!memory.InImage(table)||!memory.InImage(active)||!memory.InImage(allowed))throw new InvalidDataException("Original target station service is outside the pinned image.");
stationService=new{marketId=expectedMarket,entityVerified=true,componentAvailable=true,type,slot,componentCount,address=service.ToString("x"),vtableRva=(table-memory.ImageBase).ToString("x"),activeGetterRva=(active-memory.ImageBase).ToString("x"),allowedGetterRva=(allowed-memory.ImageBase).ToString("x"),functionsCalled=false};
}
}
catch(Exception error){stationService=new{issue=error.GetType().Name,detail=error.Message,functionsCalled=false};}
var after=await pipe.CallAsync(new{op="health.read"},token);FreshHealth(after);
if(after.GetProperty("entityId").GetString()!=frame.GetProperty("entityId").GetString())throw new InvalidDataException("Main ship changed during read-only input diagnostics.");
memory.Validate();var afterPlayer=memory.Player(entity);
if(afterPlayer!=(fsd,life,root)||memory.U64(fsd+0x6F8)!=fsdActions||memory.U64(computer+0x230)!=dockingActions)throw new InvalidDataException("Main ship, FSD or original registry changed during diagnostics.");
return new{entity=entity.ToString(),fsd=fsd.ToString("x"),computer=computer.ToString("x"),sameRegistry=fsdActions==dockingActions,
controllerThrottle=Float(fsdController+0x680),controllerDemand=Float(fsdController+0x684),
inputRoot=inputRoot.ToString("x"),actualThrottle=Float(inputRoot+0x70),dockingThrottleSource=dockingThrottle.ToString("x"),
dockingThrottleAliasesActualInput=dockingThrottle+0x18==inputRoot+0x70,
dockingThrottle=dockingThrottle>0x10000?Float(dockingThrottle+0x18):float.NaN,
originalFsd,originalDocking,domain,graph,stationService,dockingConsumer,processAccess="QUERY_INFORMATION | VM_READ",mutationsSent=0};
}
private static object ReadInputGraph(ReadOnlyPlayerMemory memory,ulong actions,ulong source,IReadOnlyDictionary<string,ulong> bindings,IReadOnlyCollection<ulong> eventSources,CancellationToken token)
{
// 8F0BA0 owns this bounded registry graph table. Follow only its heap
// pointers; never enumerate arbitrary process regions or call a node.
var buckets=memory.U64(actions+0x78);var count=memory.U64(actions+0x80);
if(buckets==0||count is 0 or >4096)throw new InvalidDataException("Original graph table exceeds bound.");
var records=new HashSet<ulong>();var roots=new List<ulong>();var flightEntries=new List<ulong>();var relatedOriginalFlightNames=new List<string>();
for(ulong i=0;i<count;i++)
{
var item=memory.U64(buckets+i*8);
while(item<buckets||item>=buckets+count*8)
{
if(item==0||!records.Add(item)||records.Count>4096)throw new InvalidDataException("Original graph chain differs.");
var contextName=memory.U64(item+8);
if(contextName>=0x10000&&memory.U64(contextName) is >0 and <=128)
{
var bytes=memory.Read(contextName+0x14,checked((int)memory.U64(contextName)));
if(bytes.All(b=>b is >=32 and <=126))
{
var originalName=System.Text.Encoding.ASCII.GetString(bytes);
// 8FA080 and 8F9DB0 use the original ASCII case-insensitive
// comparison. No cruising/landing prefix match is admitted.
if(originalName.Equals("Bindings_Flight",StringComparison.OrdinalIgnoreCase))flightEntries.Add(item);
if(originalName.Contains("Flight",StringComparison.OrdinalIgnoreCase))relatedOriginalFlightNames.Add(originalName);
}
}
var vm=memory.U64(item+0x38);if(vm!=0)roots.Add(vm);
item=memory.U64(item);
}
}
var queue=new Queue<(ulong Address,int Depth)>();var visited=new HashSet<ulong>();
foreach(var root in roots)queue.Enqueue((root,0));
var matches=new List<object>();var unreadable=0;var readBytes=0;var exhausted=false;
var matchedNodes=new HashSet<ulong>();var vmLists=new List<object>();var matchingSourceNodes=0;var subscriberNodes=new HashSet<ulong>();var activeBindings=new HashSet<ulong>();var flightActiveBindings=new HashSet<ulong>();
bool HeapPointer(ulong value)=>value>=0x10000&&value<0x7fffffff0000&&(value&7)==0&&!memory.InImage(value);
string KnownBinding(ulong value)
{
foreach(var pair in bindings)if(pair.Value==value)return pair.Key;
return "different-original-binding";
}
string InputBindingName(ulong value)
{
var text=memory.U64(value+8);var length=memory.U64(text);if(length is 0 or >64)return "unverified";
var name=memory.Read(text+0x14,(int)length);return name.All(x=>x is >=32 and <=126)?System.Text.Encoding.ASCII.GetString(name):"unverified";
}
void ObserveNode(ulong node,string origin)
{
if(memory.U64(node)==memory.ImageBase+0x512E3A0)node-=0x20;
if(matches.Count>=16||memory.U64(node)!=memory.ImageBase+0x512E330||!matchedNodes.Add(node))return;
var output=memory.U64(node+0x78);var registry=memory.U64(node+0x40);var absolute=memory.U64(node+0xB0);var zero=memory.U64(node+0xB8);var active=memory.U64(node+0x90);var config=memory.U64(node+0x10);
var activeByte=memory.Read(active+0x24,1)[0];var accumulator=BitConverter.ToSingle(memory.Read(node+0xD0,4));
activeBindings.Add(active);
if(output==source)
{
matchingSourceNodes++;
if(registry==actions&&InputBindingName(active)=="Bindings_Flight_Active"&&memory.U64(active+0x10)==registry+0xB8)flightActiveBindings.Add(active);
}
matches.Add(new{node=node.ToString("x"),origin,registry=registry.ToString("x"),registryMatches=registry==actions,output=output.ToString("x"),outputMatches=output==source,
absolute=absolute.ToString("x"),absoluteName=KnownBinding(absolute),zero=zero.ToString("x"),zeroName=KnownBinding(zero),active=active.ToString("x"),activeName=InputBindingName(active),activeRegistryOwnerMatches=memory.U64(active+0x10)==registry+0xB8,activeByte,activePreviousByte=memory.Read(active+0x34,1)[0],activeValue=BitConverter.ToSingle(memory.Read(active+0x18,4)),accumulator,
config=config.ToString("x"),configAbsoluteName=memory.U64(config+0x48).ToString("x"),configZeroName=memory.U64(config+0x50).ToString("x")});
}
foreach(var eventSource in eventSources)
{
// 914D00 subscribes node+20 through 90C170. The latter inserts
// the exact interface pointer into events+9E0 using 662DC0.
var set=eventSource+0x9E0;var slots=memory.U64(set);var capacity=memory.U64(set+8);var entries=new HashSet<ulong>();var used=memory.U64(set+0x18);
if(slots==0||capacity is 0 or >8192||used>4096)throw new InvalidDataException("Original input subscribers exceed bound.");
for(ulong i=0;i<capacity;i++)
{
token.ThrowIfCancellationRequested();var head=slots+i*8;var item=memory.U64(head);
while(item!=head)
{
if(item<0x10000||!entries.Add(item)||entries.Count>4096)throw new InvalidDataException("Original input subscriber chain differs.");
var secondary=memory.U64(item+8);if(secondary>=0x10020)subscriberNodes.Add(secondary-0x20);
ObserveNode(secondary,"original-input-event-subscription");item=memory.U64(item);
}
}
if(memory.U64(set)!=slots||memory.U64(set+8)!=capacity||memory.U64(set+0x18)!=used)throw new InvalidDataException("Original input subscribers changed.");
vmLists.Add(new{eventSource=eventSource.ToString("x"),capacity,used,count=entries.Count,matchingSourceNodes});
}
// 4C92420 stores {model,node} at list-entry +10/+18. 4C91960
// walks core+58 and core+70; this path avoids unrelated heap objects.
foreach(var vm in roots)
foreach(var mapOffset in new ulong[]{0x20,0x90})
{
var map=vm+0x10+mapOffset;var slots=memory.U64(map);var capacity=memory.U64(map+8);var entries=new HashSet<ulong>();
if(slots==0||capacity is 0 or >4096)throw new InvalidDataException("Original VM node map exceeds bound.");
for(ulong i=0;i<capacity;i++)
{
var item=memory.U64(slots+i*8);
while(item<slots||item>=slots+capacity*8)
{
if(item<0x10000||!entries.Add(item)||entries.Count>4096)throw new InvalidDataException("Original VM node map chain differs.");
foreach(var field in new ulong[]{8,0x10,0x18,0x20,0x28})
{
try{var candidate=memory.U64(item+field);if(HeapPointer(candidate)){ObserveNode(candidate,"original-vm-node-map-"+mapOffset.ToString("x"));queue.Enqueue((candidate,0));}}catch{unreadable++;}
}
item=memory.U64(item);
}
}
vmLists.Add(new{vm=vm.ToString("x"),mapOffset=mapOffset.ToString("x"),capacity,count=entries.Count});
}
foreach(var vm in roots)
foreach(var listOffset in new ulong[]{0x58,0x70})
{
var head=vm+0x10+listOffset;var entries=new HashSet<ulong>();var item=memory.U64(head);var types=new Dictionary<string,int>();
while(item!=head)
{
token.ThrowIfCancellationRequested();
if(item<0x10000||!entries.Add(item)||entries.Count>4096)throw new InvalidDataException("Original VM node list differs.");
var node=memory.U64(item+0x18);var table=memory.U64(node);var type=memory.InImage(table)?(table-memory.ImageBase).ToString("x"):"outside-image";
types[type]=types.GetValueOrDefault(type)+1;ObserveNode(node,"original-vm-node-list");item=memory.U64(item);
}
vmLists.Add(new{vm=vm.ToString("x"),listOffset=listOffset.ToString("x"),count=entries.Count,types});
}
if(matchingSourceNodes>0)queue.Clear();
while(queue.Count>0)
{
token.ThrowIfCancellationRequested();
if(visited.Count>=4096){exhausted=true;break;}
var (address,depth)=queue.Dequeue();if(!HeapPointer(address)||!visited.Add(address))continue;
byte[] bytes;try{bytes=memory.Read(address,512);}catch{unreadable++;continue;}
readBytes+=bytes.Length;
for(var offset=0;offset<bytes.Length;offset+=8)
{
var value=BitConverter.ToUInt64(bytes,offset);
if((value==memory.ImageBase+0x512E330||value==memory.ImageBase+0x512E3A0)&&matches.Count<16)
{
var node=address+(ulong)offset;
try
{
ObserveNode(node,"bounded-graph-pointer");
}
catch{unreadable++;}
}
if(depth<6&&HeapPointer(value)&&!visited.Contains(value)&&queue.Count<16384)queue.Enqueue((value,depth+1));
}
}
if(memory.U64(actions+0x78)!=buckets||memory.U64(actions+0x80)!=count)throw new InvalidDataException("Original graph table changed.");
var activeReferences=new List<object>();
foreach(var node in subscriberNodes)
{
try
{
var table=memory.U64(node);if(!memory.InImage(table))continue;var getter=memory.U64(table+8);if(!memory.InImage(getter))continue;
var code=memory.Read(getter,8);if(code[0]!=0x48||code[1]!=0x8D||code[2]!=5||code[7]!=0xC3)continue;
var text=checked((ulong)((long)getter+7+BitConverter.ToInt32(code,3)));if(!memory.InImage(text))continue;
var typeBytes=new List<byte>();for(var n=0;n<96;n++){var b=memory.Read(text+(ulong)n,1)[0];if(b==0)break;if(b is <32 or >126)throw new InvalidDataException("Original node type is not ASCII.");typeBytes.Add(b);}
var typeName=System.Text.Encoding.ASCII.GetString(typeBytes.ToArray());
foreach(var active in activeBindings)
{
var offsets=new List<string>();for(ulong offset=0x78;offset<=0xC8;offset+=8)if(memory.U64(node+offset)==active)offsets.Add(offset.ToString("x"));
if(offsets.Count>0)activeReferences.Add(new{node=node.ToString("x"),typeName,vtableRva=(table-memory.ImageBase).ToString("x"),binding=active.ToString("x"),fieldOffsets=offsets,candidateReferencesOnly=true});
}
}
catch{unreadable++;}
}
object contextAdmission;
try
{
if(flightEntries.Count!=1||flightActiveBindings.Count!=1||matchingSourceNodes!=1)throw new InvalidDataException("One exact Flight profile and current-source Active binding are required.");
var entry=flightEntries[0];var active=flightActiveBindings.Single();var header=memory.Read(entry+0x10,0x30);
var layerCount=BitConverter.ToUInt64(header,0x18);var layerData=BitConverter.ToUInt64(header,0x20);var originalVm=BitConverter.ToUInt64(header,0x28);
if(layerCount>256||layerCount>0&&!HeapPointer(layerData)||originalVm!=0&&!HeapPointer(originalVm))throw new InvalidDataException("Original Flight admission layers exceed their pinned layout bound.");
var rawLayers=layerCount==0?[]:memory.Read(layerData,checked((int)layerCount*32));var activeByte=memory.Read(active+0x24,1)[0];
var layers=new List<object>();int? highest=null;
for(var index=0;index<(int)layerCount;index++)
{
var raw=BitConverter.ToUInt64(rawLayers,index*32+0x18);
if(raw>4096)throw new InvalidDataException("Original Flight layer occupancy exceeds bound.");
if(raw!=0)highest=index;
layers.Add(new{index,priority=index/2,originalEnabledLayer=(index&1)==0,rawOccupancy=raw});
}
var secondLayers=layerCount==0?[]:memory.Read(layerData,rawLayers.Length);var secondHeader=memory.Read(entry+0x10,header.Length);
if(!header.AsSpan().SequenceEqual(secondHeader)||!rawLayers.AsSpan().SequenceEqual(secondLayers)||activeByte!=memory.Read(active+0x24,1)[0]
||memory.U64(actions+0x78)!=buckets||memory.U64(actions+0x80)!=count||memory.U64(active+0x10)!=actions+0xB8||InputBindingName(active)!="Bindings_Flight_Active")throw new InvalidDataException("Original Flight profile, layer array or Active binding changed during the two reads.");
var expectedEnabled=highest is {} layer&&(layer&1)==0;
contextAdmission=new{verified=true,contextName="Bindings_Flight",registry=actions.ToString("x"),entry=entry.ToString("x"),layerCount,layerData=layerData.ToString("x"),originalVm=originalVm.ToString("x"),layers,
highestOriginalLayer=highest,expectedEnabledByOriginalHighestLayer=expectedEnabled,activeBinding=active.ToString("x"),activeByte,activeMatchesHighestLayer=(activeByte!=0)==expectedEnabled,
originalVmMatchesAdmission=(originalVm!=0)==expectedEnabled,metadataReadTwice=true,gameCallsSent=0,gameWritesSent=0};
}
catch(Exception error){contextAdmission=new{verified=false,issue=error.GetType().Name,detail=error.Message,flightProfileMatches=flightEntries.Count,sourceActiveMatches=flightActiveBindings.Count,matchingSourceNodes,relatedOriginalFlightNames,gameCallsSent=0,gameWritesSent=0};}
return new{records=records.Count,roots=roots.Count,vmLists,visited=visited.Count,readBytes,unreadable,exhausted,matches,activeReferences,contextAdmission,gameCallsSent=0,gameWritesSent=0};
}
}
using System.Diagnostics;
using System.Globalization;
using XFEEDToolBox.Core.Navigation;
namespace XFEEDToolBox.Core.Native;
/// <summary>Read-only provenance check for the original FSD and docking action registries.</summary>
public static class NativeDockingInputDiagnostics
{
public static Task<object> ReadAsync(int processId,CancellationToken token=default)=>ReadCoreAsync(processId,null,token);
public static Task<object> ReadForStationAsync(int processId,long marketId,CancellationToken token=default)
=>marketId is <=0 or >uint.MaxValue?throw new ArgumentOutOfRangeException(nameof(marketId)):ReadCoreAsync(processId,marketId,token);
private static async Task<object> ReadCoreAsync(int processId,long? stationMarketId,CancellationToken token)
{
using var process=Process.GetProcessById(processId);
var started=process.StartTime.ToUniversalTime().Ticks;
var pipe=new NavigationPipeClient(processId);
var frame=await pipe.CallAsync(new{op="health.read"},token);
static void FreshHealth(System.Text.Json.JsonElement value)
{
var age=DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()-value.GetProperty("observedAtUnixMilliseconds").GetInt64();
if(!value.GetProperty("localPlayerShipVerified").GetBoolean()||!value.GetProperty("shipHealth").GetProperty("verified").GetBoolean()||age is <0 or >500)
throw new InvalidDataException("Fresh verified main ship health is required for read-only input diagnostics.");
}
FreshHealth(frame);
var entity=ulong.Parse(frame.GetProperty("entityId").GetString()!,CultureInfo.InvariantCulture);
using var memory=new ReadOnlyPlayerMemory(processId,started,NativeSystemLocationReader.GameSha256);
var (fsd,life,root)=memory.Player(entity);
var computer=memory.Component(root,memory.U32(memory.ImageBase+0x5F5317C));
if(memory.U64(computer)!=memory.ImageBase+0x5165248)throw new InvalidDataException("Original docking computer differs.");
var fsdController=memory.U64(fsd+0x700);var dockingController=memory.U64(computer+0x240);
if(fsdController==0||fsdController!=dockingController||memory.U64(memory.U64(fsdController)+0x158)!=memory.ImageBase+0x25E5D10)
throw new InvalidDataException("Original throttle controller ownership differs.");
var fsdActions=memory.U64(fsd+0x6F8);var dockingActions=memory.U64(computer+0x230);
object Registry(ulong value,out Dictionary<string,ulong> bindings)
{
if(memory.U64(value)!=memory.ImageBase+0x512FD90||memory.U64(memory.U64(value)+0x40)!=memory.ImageBase+0x8FEDC0)
throw new InvalidDataException("Original action registry differs.");
var buckets=memory.U64(value+0xC0);var count=memory.U64(value+0xC8);
if(buckets==0||count is 0 or >4096)throw new InvalidDataException("Original action registry exceeds bound.");
bindings=new(StringComparer.Ordinal);
var seen=new HashSet<ulong>();object? zero=null,absoluteSet=null,thrust=null;
for(ulong i=0;i<count;i++)
{
var node=memory.U64(buckets+i*8);
while(node<buckets||node>=buckets+count*8)
{
if(node==0||!seen.Add(node)||seen.Count>4096)throw new InvalidDataException("Original action registry chain differs.");
var name=memory.U64(node+8);var length=memory.U64(name);
if(length is 0 or >128)throw new InvalidDataException("Original action name exceeds bound.");
var originalName=System.Text.Encoding.ASCII.GetString(memory.Read(name+0x14,(int)length));
if(originalName is "SetSpeedZero" or "ForwardReverseThrust_Set" or "ForwardReverseThrust")
{
var binding=memory.U64(node+0x10);var bytes=memory.Read(binding,0x38);bindings.Add(originalName,binding);
var bindingName=BitConverter.ToUInt64(bytes,8);var bindingLength=memory.U64(bindingName);
var bindingNameMatches=bindingLength==(ulong)originalName.Length&&memory.Read(bindingName+0x14,originalName.Length).AsSpan().SequenceEqual(System.Text.Encoding.ASCII.GetBytes(originalName));
var input=new{originalName,binding=binding.ToString("x"),mapName=name.ToString("x"),bindingName=bindingName.ToString("x"),bindingLength,bindingNameEqualsMapKey=bindingName==name,bindingNameMatches,
registryOwnerMatches=BitConverter.ToUInt64(bytes,0x10)==value+0xB8,
value=BitConverter.ToSingle(bytes,0x18),pending=BitConverter.ToInt32(bytes,0x1C),forced=bytes[0x24],consumed=BitConverter.ToInt32(bytes,0x2C),consumedForced=bytes[0x34]};
if(originalName=="SetSpeedZero")zero=input;else if(originalName=="ForwardReverseThrust_Set")absoluteSet=input;else thrust=input;
}
node=memory.U64(node);
}
}
if(memory.U64(value+0xC0)!=buckets||memory.U64(value+0xC8)!=count)throw new InvalidDataException("Original action registry changed.");
return new{address=value.ToString("x"),zero,absoluteSet,thrust};
}
var originalFsd=Registry(fsdActions,out _);var originalDocking=Registry(dockingActions,out var dockingBindings);
var physics=memory.U64(fsd+0x2A8)-0x58;var inputRoot=memory.U64(physics+0x3B0)-0x60;
if(memory.U64(inputRoot+0x2F8)!=fsdController)throw new InvalidDataException("Original ship input controller differs.");
var dockingThrottle=memory.U64(computer+0x260);
float Float(ulong address)=>BitConverter.ToSingle(memory.Read(address,4));
object domain;ulong domainTable=0,domainGetter=0,domainRoot=0;uint domainEventType=0;var eventSources=new HashSet<ulong>();
try
{
var context=memory.U64(fsdActions-0x50);var getter=memory.U64(context);var objectRoot=memory.LeafReference(fsdActions-0x50,getter);
domainTable=context;domainGetter=getter;domainRoot=objectRoot;var eventType=memory.U32(memory.ImageBase+0x5F2DFF4);domainEventType=eventType;
// Same original service-domain chain used by 869FC0. The pinned
// leaf getters and registry key are also checked by GalaxyCatalog.
if(memory.U64(memory.U64(objectRoot)+0x20)!=memory.ImageBase+0x8C51E0)throw new InvalidDataException("Original input service-domain getter differs.");
var scope=memory.LeafReference(objectRoot,memory.ImageBase+0x8C51E0);
if(scope==0||memory.U64(memory.U64(scope))!=memory.ImageBase+0x5299D0||memory.U32(memory.ImageBase+0x5F05AB8)!=1)throw new InvalidDataException("Original input scope differs.");
var scopeSlots=memory.U64(scope+0x20);var scopeCount=memory.U64(scope+0x28);if(scopeSlots==0||scopeCount is 0 or >1024)throw new InvalidDataException("Original input scope map exceeds bound.");
ulong serviceContext=0;var scopeSeen=new HashSet<ulong>();var scopeContexts=new List<(uint Kind,ulong Context)>();
for(ulong i=0;i<scopeCount;i++)
{
var head=scopeSlots+i*8;var item=memory.U64(head);
while(item!=head)
{
if(item<0x10000||!scopeSeen.Add(item)||scopeSeen.Count>1024)throw new InvalidDataException("Original input scope chain differs.");
var kind=memory.U32(item+8);var childContext=memory.U64(item+0x10);scopeContexts.Add((kind,childContext));
if(kind==1){if(serviceContext!=0)throw new InvalidDataException("Original service context is ambiguous.");serviceContext=childContext;}
item=memory.U64(item);
}
}
if(serviceContext==0||memory.U64(memory.U64(serviceContext)+0x18)!=memory.ImageBase+0x1F3150)throw new InvalidDataException("Original service context differs.");
var serviceLife=memory.U64(serviceContext+0x568);if(serviceLife==0||memory.U32(serviceLife+0xC0)!=8)throw new InvalidDataException("Original service life differs.");
var services=memory.U64(serviceLife+0xC8)-0x30;var events=memory.Component(services,eventType);
var table=events!=0?memory.U64(events):0;
if(table!=memory.ImageBase+0x512EC98||memory.U64(table+0x18)!=memory.ImageBase+0x90C170)throw new InvalidDataException("Original input event registry differs.");
eventSources.Add(events);var contextEvents=new List<object>();
foreach(var (kind,child) in scopeContexts)
{
try
{
var lifeGetter=memory.U64(memory.U64(child)+0x18);
if(lifeGetter!=memory.ImageBase+0x1F3150){contextEvents.Add(new{kind,lifeGetterRva=memory.InImage(lifeGetter)?(lifeGetter-memory.ImageBase).ToString("x"):"outside-image",checkedContext=false});continue;}
var childLife=memory.U64(child+0x568);if(childLife==0||memory.U32(childLife+0xC0)!=8)continue;
var childRegistry=memory.U64(childLife+0xC8)-0x30;var inputEvents=memory.Component(childRegistry,eventType);
if(memory.U64(inputEvents)!=memory.ImageBase+0x512EC98||memory.U64(memory.U64(inputEvents)+0x18)!=memory.ImageBase+0x90C170)throw new InvalidDataException("Different original event registry.");
eventSources.Add(inputEvents);contextEvents.Add(new{kind,events=inputEvents.ToString("x"),checkedContext=true});
}
catch(Exception error){contextEvents.Add(new{kind,issue=error.GetType().Name});}
}
domain=new{contextTable=context.ToString("x"),getterRva=memory.InImage(getter)?(getter-memory.ImageBase).ToString("x"):"outside-image",objectRoot=objectRoot.ToString("x"),serviceContext=serviceContext.ToString("x"),services=services.ToString("x"),eventType,events=events.ToString("x"),eventTableRva=memory.InImage(table)?(table-memory.ImageBase).ToString("x"):"outside-image",
subscribeRva=table!=0?(memory.U64(table+0x18)-memory.ImageBase).ToString("x"):"0",contextEvents};
}
catch(Exception error){domain=new{issue=error.GetType().Name,detail=error.Message,contextTable=domainTable.ToString("x"),getterRva=memory.InImage(domainGetter)?(domainGetter-memory.ImageBase).ToString("x"):"outside-image",objectRoot=domainRoot.ToString("x"),eventType=domainEventType};}
object dockingConsumer;
try
{
var updateObject=computer-0x58;var configuration=memory.U64(updateObject+0x50);var controls=memory.U64(updateObject+0x180);var policy=memory.U64(updateObject+0x198);
var stateInterface=memory.U64(updateObject+0x298);var stateTable=memory.U64(stateInterface);var flags=memory.Read(updateObject+0x3CA,6);
var profileMode=memory.I32(stateInterface+0x5F8);var profileIndex=memory.I32(stateInterface+0x560);var profileCount=memory.U32(stateInterface+0x530);var profile=stateInterface+0x2E0;
if(profileMode==2&&profileIndex>=0&&(uint)profileIndex<profileCount&&profileCount<=128)profile=memory.U64(stateInterface+0x538)+(ulong)profileIndex*0x248+0x18;
var fallback=memory.U64(stateInterface+0x80);var fallbackTable=memory.U64(fallback);var fallback78=memory.U64(fallbackTable+0x78);
string Getter(ulong offset){var getter=memory.U64(stateTable+offset);return memory.InImage(getter)?(getter-memory.ImageBase).ToString("x"):"unverified";}
object Interface(ulong pointer,params ulong[] offsets)
{
var table=memory.U64(pointer);if(!memory.InImage(table))throw new InvalidDataException("Original consumer interface is outside the pinned image.");
return new{address=pointer.ToString("x"),vtableRva=(table-memory.ImageBase).ToString("x"),slots=offsets.ToDictionary(offset=>offset.ToString("x"),offset=>{var method=memory.U64(table+offset);return memory.InImage(method)?(method-memory.ImageBase).ToString("x"):"unverified";})};
}
object Activity(ulong address)
{
if(address==0)return new{available=false};var table=memory.U64(address);var getter=memory.U64(table+0x20);
if(!memory.InImage(table)||!memory.InImage(getter))throw new InvalidDataException("Original activity interface is outside the pinned image.");
var code=memory.Read(getter,16);int? state=null;
if(code[0]==0xB8&&code[5]==0xC3)state=BitConverter.ToInt32(code,1);
else if(code[0]==0x31&&code[1]==0xC0&&code[2]==0xC3)state=0;
else if(code[0]==0x8B&&code[1]==0x81&&code[6]==0xC3)state=memory.I32(checked((ulong)((long)address+BitConverter.ToInt32(code,2))));
else if(code[0]==0x8B&&code[1]==0x41&&code[3]==0xC3)state=memory.I32(checked((ulong)((long)address+(sbyte)code[2])));
return new{available=true,address=address.ToString("x"),vtableRva=(table-memory.ImageBase).ToString("x"),stateGetterRva=(getter-memory.ImageBase).ToString("x"),leafState=state,functionsCalled=false};
}
var duty=memory.U64(updateObject+0x228);var currentActivity=memory.U64(duty+0x1E8);var activities=new List<object>();var activityBuckets=memory.U64(duty+0x1A8);var activityCapacity=memory.U64(duty+0x1B0);
if(activityCapacity>512)throw new InvalidDataException("Original activity set exceeds the read-only bound.");var activityNodes=new HashSet<ulong>();
for(ulong bucket=0;bucket<activityCapacity;bucket++)
{
var head=activityBuckets+bucket*8;var entry=memory.U64(head);
while(entry!=head)
{
if(entry<0x10000||!activityNodes.Add(entry)||activityNodes.Count>512)throw new InvalidDataException("Original activity set chain differs.");
activities.Add(Activity(memory.U64(entry+8)));entry=memory.U64(entry);
}
}
dockingConsumer=new{computer=computer.ToString("x"),updateObject=updateObject.ToString("x"),moduleSlot=memory.I32(updateObject+0x3B8),mode=memory.U32(updateObject+0x3BC),
timer3C0=Float(updateObject+0x3C0),timer3C4=Float(updateObject+0x3C4),flags3CAto3CF=flags.Select(x=>(int)x).ToArray(),
source=memory.U64(updateObject+0x2B8).ToString("x"),sourceMatches=memory.U64(updateObject+0x2B8)==dockingThrottle,
configuration=configuration.ToString("x"),threshold18=Float(configuration+0x18),delay1C=Float(configuration+0x1C),
controls=controls.ToString("x"),controlB8=memory.Read(controls+0xB8,1)[0],controlB9=memory.Read(controls+0xB9,1)[0],controlBB=memory.Read(controls+0xBB,1)[0],
policy=policy.ToString("x"),policy08=memory.Read(policy+8,1)[0],stateInterface=stateInterface.ToString("x"),stateTableRva=memory.InImage(stateTable)?(stateTable-memory.ImageBase).ToString("x"):"unverified",
stateGetter90Rva=Getter(0x90),stateGetterA0Rva=Getter(0xA0),profileMode,profileIndex,profileCount,profile=profile.ToString("x"),
profile1E5=memory.Read(profile+0x1E5,1)[0],profile1E6=memory.Read(profile+0x1E6,1)[0],profile1E8=memory.Read(profile+0x1E8,1)[0],
fallback78Rva=memory.InImage(fallback78)?(fallback78-memory.ImageBase).ToString("x"):"unverified",
commandF0=Interface(memory.U64(updateObject+0xF0),0,0x20,0x28,0x30,0x40,0x48,0x58,0x60),notify1B0=Interface(memory.U64(updateObject+0x1B0),0xF0,0xF8),
blockSourceA8=Interface(memory.U64(updateObject+0xA8),0x30),blockSet1C8=memory.U64(updateObject+0x1C8).ToString("x"),
duty228=Interface(memory.U64(updateObject+0x228),0xA0,0xA8),padC0=Interface(memory.U64(updateObject+0xC0),0xC8),
currentActivity=Activity(currentActivity),activities,activityCapacity,stationHandle2B0=memory.U64(updateObject+0x2B0).ToString("x"),functionsCalled=false,fieldsAreRawOffsets=true};
}
catch(Exception error){dockingConsumer=new{issue=error.GetType().Name,detail=error.Message,functionsCalled=false};}
var graph=ReadInputGraph(memory,dockingActions,dockingThrottle,dockingBindings,eventSources,token);
object stationService;
if(stationMarketId is null)stationService=new{requested=false,functionsCalled=false};
else try
{
var expectedMarket=(ulong)stationMarketId.Value;
var context=memory.U64(life+0x38);var count=memory.U64(context+0x148);var capacity=memory.U64(context+0x150);var storage=memory.U64(context+0x158);
if(capacity is <8 or >65536||(capacity&(capacity-1))!=0||count>capacity)throw new InvalidDataException("Original entity directory bound differs.");
var bits=memory.Read(storage,checked((int)((capacity+7)/8)));var entries=storage+(((capacity>>3)+7)&~7UL);var own=false;ulong stationLife=0;
for(ulong i=0;i<capacity;i++)
{
if((bits[i>>3]&(1<<(int)(i&7)))==0)continue;var entry=memory.Read(entries+i*16,16);var key=BitConverter.ToUInt64(entry,0);var value=BitConverter.ToUInt64(entry,8);
if(key==entity)own=value==life;if(key==expectedMarket){if(stationLife!=0)throw new InvalidDataException("Original target station is ambiguous.");stationLife=value;}
}
if(!own||stationLife==0||memory.U32(stationLife+0xC0)!=8||memory.U64(stationLife+0xB0)!=expectedMarket)throw new InvalidDataException("Original target station identity is unavailable.");
var stationRoot=memory.U64(stationLife+0xC8)-0x30;var type=memory.U32(memory.ImageBase+0x5F771D0);var metadata=memory.U64(stationRoot+0x10);var componentCount=memory.U64(metadata+0x18);
if(type is 0 or >99999||componentCount is 0 or >1024)throw new InvalidDataException("Original station component layout differs.");
var slots=memory.U64(metadata+0x30);var slot=memory.U64(slots+type*16UL);
if(slot>=componentCount)stationService=new{marketId=expectedMarket,entityVerified=true,componentAvailable=false,type,slot,componentCount,functionsCalled=false};
else
{
var service=memory.U64(memory.U64(stationRoot+0x18)+slot*8);var table=memory.U64(service);var active=memory.U64(table+0x28);var allowed=memory.U64(table+0x58);
if(!memory.InImage(table)||!memory.InImage(active)||!memory.InImage(allowed))throw new InvalidDataException("Original target station service is outside the pinned image.");
stationService=new{marketId=expectedMarket,entityVerified=true,componentAvailable=true,type,slot,componentCount,address=service.ToString("x"),vtableRva=(table-memory.ImageBase).ToString("x"),activeGetterRva=(active-memory.ImageBase).ToString("x"),allowedGetterRva=(allowed-memory.ImageBase).ToString("x"),functionsCalled=false};
}
}
catch(Exception error){stationService=new{issue=error.GetType().Name,detail=error.Message,functionsCalled=false};}
var after=await pipe.CallAsync(new{op="health.read"},token);FreshHealth(after);
if(after.GetProperty("entityId").GetString()!=frame.GetProperty("entityId").GetString())throw new InvalidDataException("Main ship changed during read-only input diagnostics.");
memory.Validate();var afterPlayer=memory.Player(entity);
if(afterPlayer!=(fsd,life,root)||memory.U64(fsd+0x6F8)!=fsdActions||memory.U64(computer+0x230)!=dockingActions)throw new InvalidDataException("Main ship, FSD or original registry changed during diagnostics.");
return new{entity=entity.ToString(),fsd=fsd.ToString("x"),computer=computer.ToString("x"),sameRegistry=fsdActions==dockingActions,
controllerThrottle=Float(fsdController+0x680),controllerDemand=Float(fsdController+0x684),
inputRoot=inputRoot.ToString("x"),actualThrottle=Float(inputRoot+0x70),dockingThrottleSource=dockingThrottle.ToString("x"),
dockingThrottleAliasesActualInput=dockingThrottle+0x18==inputRoot+0x70,
dockingThrottle=dockingThrottle>0x10000?Float(dockingThrottle+0x18):float.NaN,
originalFsd,originalDocking,domain,graph,stationService,dockingConsumer,processAccess="QUERY_INFORMATION | VM_READ",mutationsSent=0};
}
private static object ReadInputGraph(ReadOnlyPlayerMemory memory,ulong actions,ulong source,IReadOnlyDictionary<string,ulong> bindings,IReadOnlyCollection<ulong> eventSources,CancellationToken token)
{
// 8F0BA0 owns this bounded registry graph table. Follow only its heap
// pointers; never enumerate arbitrary process regions or call a node.
var buckets=memory.U64(actions+0x78);var count=memory.U64(actions+0x80);
if(buckets==0||count is 0 or >4096)throw new InvalidDataException("Original graph table exceeds bound.");
var records=new HashSet<ulong>();var roots=new List<ulong>();var flightEntries=new List<ulong>();var relatedOriginalFlightNames=new List<string>();
for(ulong i=0;i<count;i++)
{
var item=memory.U64(buckets+i*8);
while(item<buckets||item>=buckets+count*8)
{
if(item==0||!records.Add(item)||records.Count>4096)throw new InvalidDataException("Original graph chain differs.");
var contextName=memory.U64(item+8);
if(contextName>=0x10000&&memory.U64(contextName) is >0 and <=128)
{
var bytes=memory.Read(contextName+0x14,checked((int)memory.U64(contextName)));
if(bytes.All(b=>b is >=32 and <=126))
{
var originalName=System.Text.Encoding.ASCII.GetString(bytes);
// 8FA080 and 8F9DB0 use the original ASCII case-insensitive
// comparison. No cruising/landing prefix match is admitted.
if(originalName.Equals("Bindings_Flight",StringComparison.OrdinalIgnoreCase))flightEntries.Add(item);
if(originalName.Contains("Flight",StringComparison.OrdinalIgnoreCase))relatedOriginalFlightNames.Add(originalName);
}
}
var vm=memory.U64(item+0x38);if(vm!=0)roots.Add(vm);
item=memory.U64(item);
}
}
var queue=new Queue<(ulong Address,int Depth)>();var visited=new HashSet<ulong>();
foreach(var root in roots)queue.Enqueue((root,0));
var matches=new List<object>();var unreadable=0;var readBytes=0;var exhausted=false;
var matchedNodes=new HashSet<ulong>();var vmLists=new List<object>();var matchingSourceNodes=0;var subscriberNodes=new HashSet<ulong>();var activeBindings=new HashSet<ulong>();var flightActiveBindings=new HashSet<ulong>();
bool HeapPointer(ulong value)=>value>=0x10000&&value<0x7fffffff0000&&(value&7)==0&&!memory.InImage(value);
string KnownBinding(ulong value)
{
foreach(var pair in bindings)if(pair.Value==value)return pair.Key;
return "different-original-binding";
}
string InputBindingName(ulong value)
{
var text=memory.U64(value+8);var length=memory.U64(text);if(length is 0 or >64)return "unverified";
var name=memory.Read(text+0x14,(int)length);return name.All(x=>x is >=32 and <=126)?System.Text.Encoding.ASCII.GetString(name):"unverified";
}
void ObserveNode(ulong node,string origin)
{
if(memory.U64(node)==memory.ImageBase+0x512E3A0)node-=0x20;
if(matches.Count>=16||memory.U64(node)!=memory.ImageBase+0x512E330||!matchedNodes.Add(node))return;
var output=memory.U64(node+0x78);var registry=memory.U64(node+0x40);var absolute=memory.U64(node+0xB0);var zero=memory.U64(node+0xB8);var active=memory.U64(node+0x90);var config=memory.U64(node+0x10);
var activeByte=memory.Read(active+0x24,1)[0];var accumulator=BitConverter.ToSingle(memory.Read(node+0xD0,4));
activeBindings.Add(active);
if(output==source)
{
matchingSourceNodes++;
if(registry==actions&&InputBindingName(active)=="Bindings_Flight_Active"&&memory.U64(active+0x10)==registry+0xB8)flightActiveBindings.Add(active);
}
matches.Add(new{node=node.ToString("x"),origin,registry=registry.ToString("x"),registryMatches=registry==actions,output=output.ToString("x"),outputMatches=output==source,
absolute=absolute.ToString("x"),absoluteName=KnownBinding(absolute),zero=zero.ToString("x"),zeroName=KnownBinding(zero),active=active.ToString("x"),activeName=InputBindingName(active),activeRegistryOwnerMatches=memory.U64(active+0x10)==registry+0xB8,activeByte,activePreviousByte=memory.Read(active+0x34,1)[0],activeValue=BitConverter.ToSingle(memory.Read(active+0x18,4)),accumulator,
config=config.ToString("x"),configAbsoluteName=memory.U64(config+0x48).ToString("x"),configZeroName=memory.U64(config+0x50).ToString("x")});
}
foreach(var eventSource in eventSources)
{
// 914D00 subscribes node+20 through 90C170. The latter inserts
// the exact interface pointer into events+9E0 using 662DC0.
var set=eventSource+0x9E0;var slots=memory.U64(set);var capacity=memory.U64(set+8);var entries=new HashSet<ulong>();var used=memory.U64(set+0x18);
if(slots==0||capacity is 0 or >8192||used>4096)throw new InvalidDataException("Original input subscribers exceed bound.");
for(ulong i=0;i<capacity;i++)
{
token.ThrowIfCancellationRequested();var head=slots+i*8;var item=memory.U64(head);
while(item!=head)
{
if(item<0x10000||!entries.Add(item)||entries.Count>4096)throw new InvalidDataException("Original input subscriber chain differs.");
var secondary=memory.U64(item+8);if(secondary>=0x10020)subscriberNodes.Add(secondary-0x20);
ObserveNode(secondary,"original-input-event-subscription");item=memory.U64(item);
}
}
if(memory.U64(set)!=slots||memory.U64(set+8)!=capacity||memory.U64(set+0x18)!=used)throw new InvalidDataException("Original input subscribers changed.");
vmLists.Add(new{eventSource=eventSource.ToString("x"),capacity,used,count=entries.Count,matchingSourceNodes});
}
// 4C92420 stores {model,node} at list-entry +10/+18. 4C91960
// walks core+58 and core+70; this path avoids unrelated heap objects.
foreach(var vm in roots)
foreach(var mapOffset in new ulong[]{0x20,0x90})
{
var map=vm+0x10+mapOffset;var slots=memory.U64(map);var capacity=memory.U64(map+8);var entries=new HashSet<ulong>();
if(slots==0||capacity is 0 or >4096)throw new InvalidDataException("Original VM node map exceeds bound.");
for(ulong i=0;i<capacity;i++)
{
var item=memory.U64(slots+i*8);
while(item<slots||item>=slots+capacity*8)
{
if(item<0x10000||!entries.Add(item)||entries.Count>4096)throw new InvalidDataException("Original VM node map chain differs.");
foreach(var field in new ulong[]{8,0x10,0x18,0x20,0x28})
{
try{var candidate=memory.U64(item+field);if(HeapPointer(candidate)){ObserveNode(candidate,"original-vm-node-map-"+mapOffset.ToString("x"));queue.Enqueue((candidate,0));}}catch{unreadable++;}
}
item=memory.U64(item);
}
}
vmLists.Add(new{vm=vm.ToString("x"),mapOffset=mapOffset.ToString("x"),capacity,count=entries.Count});
}
foreach(var vm in roots)
foreach(var listOffset in new ulong[]{0x58,0x70})
{
var head=vm+0x10+listOffset;var entries=new HashSet<ulong>();var item=memory.U64(head);var types=new Dictionary<string,int>();
while(item!=head)
{
token.ThrowIfCancellationRequested();
if(item<0x10000||!entries.Add(item)||entries.Count>4096)throw new InvalidDataException("Original VM node list differs.");
var node=memory.U64(item+0x18);var table=memory.U64(node);var type=memory.InImage(table)?(table-memory.ImageBase).ToString("x"):"outside-image";
types[type]=types.GetValueOrDefault(type)+1;ObserveNode(node,"original-vm-node-list");item=memory.U64(item);
}
vmLists.Add(new{vm=vm.ToString("x"),listOffset=listOffset.ToString("x"),count=entries.Count,types});
}
if(matchingSourceNodes>0)queue.Clear();
while(queue.Count>0)
{
token.ThrowIfCancellationRequested();
if(visited.Count>=4096){exhausted=true;break;}
var (address,depth)=queue.Dequeue();if(!HeapPointer(address)||!visited.Add(address))continue;
byte[] bytes;try{bytes=memory.Read(address,512);}catch{unreadable++;continue;}
readBytes+=bytes.Length;
for(var offset=0;offset<bytes.Length;offset+=8)
{
var value=BitConverter.ToUInt64(bytes,offset);
if((value==memory.ImageBase+0x512E330||value==memory.ImageBase+0x512E3A0)&&matches.Count<16)
{
var node=address+(ulong)offset;
try
{
ObserveNode(node,"bounded-graph-pointer");
}
catch{unreadable++;}
}
if(depth<6&&HeapPointer(value)&&!visited.Contains(value)&&queue.Count<16384)queue.Enqueue((value,depth+1));
}
}
if(memory.U64(actions+0x78)!=buckets||memory.U64(actions+0x80)!=count)throw new InvalidDataException("Original graph table changed.");
var activeReferences=new List<object>();
foreach(var node in subscriberNodes)
{
try
{
var table=memory.U64(node);if(!memory.InImage(table))continue;var getter=memory.U64(table+8);if(!memory.InImage(getter))continue;
var code=memory.Read(getter,8);if(code[0]!=0x48||code[1]!=0x8D||code[2]!=5||code[7]!=0xC3)continue;
var text=checked((ulong)((long)getter+7+BitConverter.ToInt32(code,3)));if(!memory.InImage(text))continue;
var typeBytes=new List<byte>();for(var n=0;n<96;n++){var b=memory.Read(text+(ulong)n,1)[0];if(b==0)break;if(b is <32 or >126)throw new InvalidDataException("Original node type is not ASCII.");typeBytes.Add(b);}
var typeName=System.Text.Encoding.ASCII.GetString(typeBytes.ToArray());
foreach(var active in activeBindings)
{
var offsets=new List<string>();for(ulong offset=0x78;offset<=0xC8;offset+=8)if(memory.U64(node+offset)==active)offsets.Add(offset.ToString("x"));
if(offsets.Count>0)activeReferences.Add(new{node=node.ToString("x"),typeName,vtableRva=(table-memory.ImageBase).ToString("x"),binding=active.ToString("x"),fieldOffsets=offsets,candidateReferencesOnly=true});
}
}
catch{unreadable++;}
}
object contextAdmission;
try
{
if(flightEntries.Count!=1||flightActiveBindings.Count!=1||matchingSourceNodes!=1)throw new InvalidDataException("One exact Flight profile and current-source Active binding are required.");
var entry=flightEntries[0];var active=flightActiveBindings.Single();var header=memory.Read(entry+0x10,0x30);
var layerCount=BitConverter.ToUInt64(header,0x18);var layerData=BitConverter.ToUInt64(header,0x20);var originalVm=BitConverter.ToUInt64(header,0x28);
if(layerCount>256||layerCount>0&&!HeapPointer(layerData)||originalVm!=0&&!HeapPointer(originalVm))throw new InvalidDataException("Original Flight admission layers exceed their pinned layout bound.");
var rawLayers=layerCount==0?[]:memory.Read(layerData,checked((int)layerCount*32));var activeByte=memory.Read(active+0x24,1)[0];
var layers=new List<object>();int? highest=null;
for(var index=0;index<(int)layerCount;index++)
{
var raw=BitConverter.ToUInt64(rawLayers,index*32+0x18);
if(raw>4096)throw new InvalidDataException("Original Flight layer occupancy exceeds bound.");
if(raw!=0)highest=index;
layers.Add(new{index,priority=index/2,originalEnabledLayer=(index&1)==0,rawOccupancy=raw});
}
var secondLayers=layerCount==0?[]:memory.Read(layerData,rawLayers.Length);var secondHeader=memory.Read(entry+0x10,header.Length);
if(!header.AsSpan().SequenceEqual(secondHeader)||!rawLayers.AsSpan().SequenceEqual(secondLayers)||activeByte!=memory.Read(active+0x24,1)[0]
||memory.U64(actions+0x78)!=buckets||memory.U64(actions+0x80)!=count||memory.U64(active+0x10)!=actions+0xB8||InputBindingName(active)!="Bindings_Flight_Active")throw new InvalidDataException("Original Flight profile, layer array or Active binding changed during the two reads.");
var expectedEnabled=highest is {} layer&&(layer&1)==0;
contextAdmission=new{verified=true,contextName="Bindings_Flight",registry=actions.ToString("x"),entry=entry.ToString("x"),layerCount,layerData=layerData.ToString("x"),originalVm=originalVm.ToString("x"),layers,
highestOriginalLayer=highest,expectedEnabledByOriginalHighestLayer=expectedEnabled,activeBinding=active.ToString("x"),activeByte,activeMatchesHighestLayer=(activeByte!=0)==expectedEnabled,
originalVmMatchesAdmission=(originalVm!=0)==expectedEnabled,metadataReadTwice=true,gameCallsSent=0,gameWritesSent=0};
}
catch(Exception error){contextAdmission=new{verified=false,issue=error.GetType().Name,detail=error.Message,flightProfileMatches=flightEntries.Count,sourceActiveMatches=flightActiveBindings.Count,matchingSourceNodes,relatedOriginalFlightNames,gameCallsSent=0,gameWritesSent=0};}
return new{records=records.Count,roots=roots.Count,vmLists,visited=visited.Count,readBytes,unreadable,exhausted,matches,activeReferences,contextAdmission,gameCallsSent=0,gameWritesSent=0};
}
}