namespace XFEEDToolBox.Plugin; internal static unsafe partial class Navigation { // Original input-graph node. Its SetSpeedZero branch clears its own // throttle accumulator before producing ForwardReverseThrust. A pulse // delivered after the FSD update misses this node's legal evaluation. private const nuint DockingInputMapper=0x8F3500; private static nuint originalDockingInputMapper; private static bool dockingInputMapperReady; private static ulong dockingInputMapperCalls,dockingInputMapperMatched,dockingInputMapperApplied; private struct DockingMapperReceipt { public ulong sequence,entity,utc; public float before,after,source; public bool verified; } private static DockingMapperReceipt dockingInputMapperReceipt; private struct DockingMapperPulse { public nuint binding,source,node; public uint beforeCount,afterCount; public byte beforeFlags,afterFlags; public float beforeAccumulator; public ulong entity,sequence; public bool applied; } private static bool DockingMapperNodeMatches(nuint node,nuint binding,nuint source) { if(node<0x10000||binding<0x10000||source<0x10000)return false; // Both input and output must alias the exact current ship's original // registry bindings. Matching a node type or offset alone is unsafe. return Pointer(node+0xB8)==binding&&Pointer(node+0x78)==source; } private static bool BeginDockingMapperPulse(nuint node,nuint binding,nuint source,out DockingMapperPulse pulse) { pulse=default; if(!DockingMapperNodeMatches(node,binding,source)||Read(binding)<=0)return false; var count=Read(binding+0x1C);var flags=Read(binding+0x24); var accumulator=Read(node+0xD0); if(count==uint.MaxValue||!float.IsFinite(accumulator)||Math.Abs(accumulator)>1.001f)return false; pulse=new(){node=node,binding=binding,source=source,beforeCount=count,afterCount=count+1,beforeFlags=flags,afterFlags=(byte)(flags|1),beforeAccumulator=accumulator,applied=true}; Write(binding+0x1C,pulse.afterCount);Write(binding+0x24,pulse.afterFlags); return true; } private static void EndDockingMapperPulse(in DockingMapperPulse pulse) { if(!pulse.applied||!DockingMapperNodeMatches(pulse.node,pulse.binding,pulse.source))return; // Do not erase a later physical input or another original consumer's // update. Only restore the temporary logical pulse we actually wrote. if(Read(pulse.binding+0x1C)!=pulse.afterCount||Read(pulse.binding+0x24)!=pulse.afterFlags)return; Write(pulse.binding+0x1C,pulse.beforeCount);Write(pulse.binding+0x24,pulse.beforeFlags); } private static bool PrepareDockingMapperPulse(nuint node,float dt,out DockingMapperPulse pulse) { pulse=default;dockingInputMapperCalls++; if(!dockingInputMapperReady||!float.IsFinite(dt)||dt<=0||dt>.25f||GetCurrentThreadId()!=owningThread)return false; var now=GetTickCount64();if(Volatile.Read(ref dockingZeroUpdate.command.expires)<=now)return false; var frame=Current();DockingZeroUpdate wanted;LocalSelection selection;LocalFlightObservation observed; lock(localLock){wanted=dockingZeroUpdate;selection=localSelection;observed=localObservation;} if(wanted.command.action!=10)return false; if(!DockingZeroUpdateMatches(wanted,frame,selection,wanted.computer,now,controllerEpoch, clientOwner.Length!=0,Enabled(),inGame,overridden)||localConfirmed!=selection.generation||fsdOwned||!BoundFsd(frame,frame.fsd))return false; var computer=LocalDockingComputer(frame);var receipt=dockingZeroInputReceipt; if(computer!=wanted.computer||computer==0||!LocalDockingAvailable(computer)||Pointer(computer+0x240)!=wanted.controller ||receipt.entity!=frame.entity||receipt.life!=frame.life||receipt.market!=wanted.market||receipt.inputService!=Pointer(computer+0x230) ||now=30000||!observed.valid||observed.entity!=frame.entity||observed.inSupercruise ||observed.distance>7500||now200||!SameLocal(observed.target,selection.target))return false; if((byte)Call(image+0xED6620,computer)!=0) {lock(localLock)dockingZeroUpdate.command.expires=0;return false;} var source=Pointer(computer+0x260); if(!DockingMapperNodeMatches(node,receipt.binding,source))return false; // The registry clears this frame's aggregate before evaluating the // node. Its zero at entry does not describe the retained accumulator; // let the original SetSpeedZero branch handle that state itself. if(!ReadOriginalDockingThrottle(computer,out _))return false; var active=Pointer(node+0x90); if(active<0x10000||Read(active+0x24)==0)return false; var dock=LocalDocking(frame); if(dock==0||Read(dock+0x614)!=2||Read(dock+0x4C0)is not(3 or 4)||Read(dock+0x4E9)==255)return false; var station=Pointer(dock+0x4E0); if(station==0||Read(station+0xC0)!=8||Read(station+0xB0)!=wanted.market||!LocalRecordMatches(Pointer(frame.fsd+0x338),selection.target))return false; bool released;lock(leaseLock)released=lease.expires<=now&&fsdLease.expires<=now; if(!released)return false; dockingInputMapperMatched++; if(!BeginDockingMapperPulse(node,receipt.binding,source,out pulse))return false; pulse.entity=frame.entity;pulse.sequence=frame.sequence;return true; } [UnmanagedCallersOnly] private static ulong DockingInputMapperHook(nuint node,float dt) { DockingMapperPulse pulse=default; DockingFixturePulse fixture=default; try{if(!PrepareDockingFixturePulse(node,dt,out fixture))PrepareDockingMapperPulse(node,dt,out pulse);}catch{try{FixtureExceptionEvidence();}catch{}} try{return Call(originalDockingInputMapper,(ulong)node,F32(dt));} catch{return 0;} finally { try { if(pulse.applied&&DockingMapperNodeMatches(pulse.node,pulse.binding,pulse.source)) { var accumulator=Read(pulse.node+0xD0);var source=Read(pulse.source+0x18); var valid=float.IsFinite(accumulator)&&Math.Abs(accumulator)<=1.001f&&float.IsFinite(source)&&Math.Abs(source)<=1.001f; lock(localLock)dockingInputMapperReceipt=new(){sequence=pulse.sequence,entity=pulse.entity,utc=UtcMilliseconds(),before=pulse.beforeAccumulator, after=valid?accumulator:0,source=valid?source:0,verified=valid}; if(valid&&Math.Abs(accumulator)<=.001f&&Math.Abs(source)<=.001f)dockingInputMapperApplied++; } } catch{} finally { try{EndDockingMapperPulse(pulse);}catch{} try{EndDockingFixturePulse(fixture);}catch{} try{ObserveDockingFixtureMapper(node,fixture,pulse);}catch{} } } } }