XFEstudio/XFEEDToolBox
Add controlled visual handoff runs and preserve docking request identity
Add read-only visual-run preparation tied to the exact game session and an explicit single empty-flight command. Requested observation mode stops control without exiting or restarting the game; production damage and handoff protections keep their defaults. Record the first continuous SCA, ATC, native docking and actual arrival success separately from historical recovered cargo journeys. Keep a fresh docking action valid after its selection acknowledgement expires, while retaining identity, generation, ownership, health, original clearance and input receipt checks. This follow-up native change is verified offline and has not replaced the running game plugin. Validation: 648 native self-test invocations, original exports/PE/ABI and unsupported-build checks; isolated CLI publish; 11 protection regressions and two selection recovery tests. Live empty visual journey completed with the game preserved; production trading resumed on the same session.
7dbb976
代码差异
@@ -56,7 +56,8 @@ public static class NativeDockingInputDiagnostics
memory.Validate();if(memory.U64(life+0xB0)!=entity)throw new InvalidDataException("Main ship identity changed.");
return new{entity=entity.ToString(),fsd=fsd.ToString("x"),computer=computer.ToString("x"),sameRegistry=fsdActions==dockingActions,
controllerThrottle=Float(fsdController+0x680),controllerDemand=Float(fsdController+0x684),
actualThrottle=Float(inputRoot+0x70),dockingThrottleSource=dockingThrottle.ToString("x"),
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,processAccess="QUERY_INFORMATION | VM_READ",mutationsSent=0};
}
@@ -48,7 +48,7 @@ public sealed class NativeSystemFlightControlDriver(int processId,string gameHas
{
target.Validate();if(target.NativeKey<0||target.NativeKind<=0||target.NativeIdentity.Length!=64)throw new ArgumentException("目标缺少当前游戏目录中的完整身份。");
if(!(await InspectAsync(token)).LocalTargets)throw new NotSupportedException("当前插件尚不支持星系内目标选择。");
var state=player();if(!state.InGame||state.Navigation.CurrentSystemAddress!=target.SystemAddress||NavigationSafety.UnhealthyHull(state)||NavigationSafety.HasExternalThreat(state))throw new InvalidOperationException("当前会话不适合选择自动驾驶目标。");
var state=player();NavigationSafety.RequireLocalSelection(state,target.SystemAddress);
await SelectNativeAsync(target,token);selected=nativeSelected=target;last=null;attitude.Reset();ownsCruiseAssist=ownsDockingAssist=false;
journalShip=state.CurrentShipId;commander=state.CommanderId;sessionAt=state.LoadGameAt;boundEntity=replacesEntity=0;
entryRequestedAt=exitRequestedAt=assistRequestedAt=sceneTransitionAt=null;
@@ -10,6 +10,15 @@ public static class NavigationSafety
||player.Status is {} status&&(status.Flags&Interdiction)!=0
||player.InDanger&&(player.Status is null||(player.Status.Flags&(Danger|HeatWarning|Interdiction))==0);
public static void RequireLocalSelection(PlayerState player,long systemAddress)
{
var hull=UnhealthyHull(player);var threat=HasExternalThreat(player);
if(!player.InGame||hull||threat)
throw new InvalidOperationException($"自动驾驶目标选择被安全状态拒绝:inGame={player.InGame}, unhealthyHull={hull}, externalThreat={threat}。");
if(player.Navigation.CurrentSystemAddress!=systemAddress)
throw new FlightTelemetryUnavailableException("正在等待跃迁后的原始目录与当前星系一致。",recovery:FlightRecovery.ReselectTarget);
}
public static bool MustStop(PlayerState player,FlightFrame? frame,DateTimeOffset now)
{
if(HasExternalThreat(player))return true;
@@ -16,6 +16,7 @@ public sealed class NativeFlightTestProtection:IAsyncDisposable
private readonly Func<PlayerState> player;
private readonly Action stopAutomation;
private readonly bool ownsMonitor;
private readonly bool automaticExit;
private readonly IGameSessionDriver sessionDriver;
private bool independentHealth;
private Task? polling;
@@ -26,11 +27,11 @@ public sealed class NativeFlightTestProtection:IAsyncDisposable
public string? LastIssue{get;private set;}
public bool Stopped=>Volatile.Read(ref unsafeState)!=0;
public bool Fresh=>!Stopped&&DateTimeOffset.UtcNow.UtcTicks-Interlocked.Read(ref lastHealthTicks)<=TimeSpan.FromMilliseconds(500).Ticks;
private NativeFlightTestProtection(int pid,GameSessionTarget target,IGameSessionDriver sessions,Func<PlayerState> player,Action stopAutomation,ShipProtectionMonitor? sharedMonitor)
private NativeFlightTestProtection(int pid,GameSessionTarget target,IGameSessionDriver sessions,Func<PlayerState> player,Action stopAutomation,ShipProtectionMonitor? sharedMonitor,bool automaticExit)
{
pipe=new(pid);this.target=target;this.player=player;this.stopAutomation=stopAutomation;sessionDriver=sessions;
pipe=new(pid);this.target=target;this.player=player;this.stopAutomation=stopAutomation;sessionDriver=sessions;this.automaticExit=automaticExit;
ownsMonitor=sharedMonitor is null;
monitor=sharedMonitor??new(sessions,player,()=>{Interlocked.Exchange(ref unsafeState,1);StopAutomation();});
monitor=sharedMonitor??new(sessions,player,()=>{Interlocked.Exchange(ref unsafeState,1);StopAutomation();},automaticExit);
}
private void StopAutomation(){try{stopAutomation();}catch{/* A stopped worker cannot disarm health monitoring. */}}
public static async Task ExitForFailedHandoffAsync(int pid)
@@ -40,11 +41,12 @@ public sealed class NativeFlightTestProtection:IAsyncDisposable
Interlocked.Exchange(ref protection.unsafeState,1);
protection.LastIssue="自动降落未接管,飞船仍在移动;已停止操作并保护退出。";
protection.StopAutomation();
if(!protection.automaticExit){protection.LastIssue="人工观察模式:自动降落未接管,已停止自动输入,不退出游戏。";return;}
// Do not restart into the same unresolved approach or let cancelling
// the navigation worker interrupt the already required protective exit.
await new GameSessionController(protection.sessionDriver,()=>false).EmergencyExitAsync(protection.target,CancellationToken.None);
}
public static async Task<NativeFlightTestProtection> ArmAsync(int pid,string hash,IGameSessionDriver sessions,Func<PlayerState> player,Action stopAutomation,CancellationToken token=default,ShipProtectionMonitor? sharedMonitor=null)
public static async Task<NativeFlightTestProtection> ArmAsync(int pid,string hash,IGameSessionDriver sessions,Func<PlayerState> player,Action stopAutomation,CancellationToken token=default,ShipProtectionMonitor? sharedMonitor=null,bool automaticExit=true)
{
if(Volatile.Read(ref active) is not null)throw new InvalidOperationException("已有飞行测试保护正在运行。");
var pipe=new NavigationPipeClient(pid);var hello=await pipe.CallAsync(new{op="describe"},token);
@@ -55,7 +57,8 @@ public sealed class NativeFlightTestProtection:IAsyncDisposable
throw new NotSupportedException("当前插件尚未提供已验证的连续船体血量,已禁止自动飞行测试。");
var target=await sessions.FindTargetAsync(token);if(target.ProcessId!=pid)throw new InvalidOperationException("飞行测试与受保护游戏实例不一致。");
if(sharedMonitor is not null&&sharedMonitor.BoundTarget!=target)throw new InvalidOperationException("连续血量监测与受保护游戏实例不一致。");
var protection=new NativeFlightTestProtection(pid,target,sessions,player,stopAutomation,sharedMonitor);
if(sharedMonitor is not null&&sharedMonitor.AutomaticExitEnabled!=automaticExit)throw new InvalidOperationException("连续血量监测的退出模式不一致。");
var protection=new NativeFlightTestProtection(pid,target,sessions,player,stopAutomation,sharedMonitor,automaticExit);
protection.independentHealth=hello.TryGetProperty("independentShipHealth",out var independent)&&independent.ValueKind==JsonValueKind.True;
try
{
@@ -6,7 +6,7 @@ namespace XFEEDToolBox.Core.Session;
public sealed record ShipProtectionProgress(bool EmergencyActive,bool TestingBlocked,string Message);
/// <summary>Shared production/test protection. Damage processing never waits for the UI dispatcher.</summary>
public sealed class ShipProtectionMonitor(IGameSessionDriver driver,Func<PlayerState> player,Action stopAutomation):IAsyncDisposable
public sealed class ShipProtectionMonitor(IGameSessionDriver driver,Func<PlayerState> player,Action stopAutomation,bool automaticExit=true):IAsyncDisposable
{
private readonly object gate=new();
private readonly ShieldRestartGuard guard=new();
@@ -18,6 +18,7 @@ public sealed class ShipProtectionMonitor(IGameSessionDriver driver,Func<PlayerS
private readonly SemaphoreSlim nativeGate=new(1,1);
public GameSessionTarget? BoundTarget{get{lock(gate)return target;}}
public bool ContinuousHealthReady=>nativeProtection?.Fresh==true;
public bool AutomaticExitEnabled=>automaticExit;
public event Action<ShipProtectionProgress>? Progress;
public event Action<SessionReentryEvent>? Reentry;
public bool TestingBlocked{get{lock(gate)return guard.TestingBlocked;}}
@@ -36,7 +37,7 @@ public sealed class ShipProtectionMonitor(IGameSessionDriver driver,Func<PlayerS
target=next;enabled=true;guard.SetEnabled(true,DateTimeOffset.UtcNow);guard.Prime(player(),next);
}
if(TestingBlocked)StopAutomation();
Report(false,TestingBlocked?"船体低于 50% · 自动操作和测试已停止,等待修复":"日志监测中 · 破盾或低血量时大退");
Report(false,TestingBlocked?"船体低于 50% · 自动操作和测试已停止,等待修复":automaticExit?"日志监测中 · 破盾或低血量时大退":"人工观察模式 · 异常只停自动输入,不退出游戏");
}
public async Task AttachNativeAsync(string gameHash,CancellationToken token=default)
{
@@ -46,7 +47,7 @@ public sealed class ShipProtectionMonitor(IGameSessionDriver driver,Func<PlayerS
RequireHealthyShip();var expected=BoundTarget??throw new InvalidOperationException("飞船保护尚未绑定游戏实例。");
if(nativeProtection is {Stopped:false})return;
if(nativeProtection is {} previous){nativeProtection=null;await previous.DisposeAsync();}
nativeProtection=await NativeFlightTestProtection.ArmAsync(expected.ProcessId,gameHash,driver,player,StopAutomation,token,this);
nativeProtection=await NativeFlightTestProtection.ArmAsync(expected.ProcessId,gameHash,driver,player,StopAutomation,token,this,automaticExit);
Report(false,"连续船体与护盾监测中 · 受损后破盾或低血量立即大退");
}
finally{nativeGate.Release();}
@@ -68,7 +69,7 @@ public sealed class ShipProtectionMonitor(IGameSessionDriver driver,Func<PlayerS
if(restart is not null)
{
StopAutomation();
emergency=Task.Run(()=>ProtectAsync(restart,trigger));
if(automaticExit)emergency=Task.Run(()=>ProtectAsync(restart,trigger));
}
}
if(blocked)StopAutomation();
@@ -81,7 +82,7 @@ public sealed class ShipProtectionMonitor(IGameSessionDriver driver,Func<PlayerS
if(!enabled||target is null||emergency is {IsCompleted:false})return;
if(guard.ObserveHealth(sample,player(),target,now)){restart=target;trigger=guard.LastTrigger;}
blocked=guard.TestingBlocked;
if(restart is not null){StopAutomation();emergency=Task.Run(()=>ProtectAsync(restart,trigger));}
if(restart is not null){StopAutomation();if(automaticExit)emergency=Task.Run(()=>ProtectAsync(restart,trigger));}
}
if(blocked)StopAutomation();
}
@@ -1,4 +1,5 @@
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Models;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
@@ -14,4 +15,15 @@ public class LocalSelectionRecoveryTests
using var stop=new CancellationTokenSource();stop.Cancel();
Check.That(!NavigationRecovery.CanRetry(new InvalidOperationException("Stop flight control before selecting a local destination"),stop.Token));
}
[Test]public void SystemTransitionCanWaitWhileUnsafeSessionAndHullRemainTerminal()
{
var healthy=new PlayerState{InGame=true,CurrentShipId=5,Ships=[new(){ShipId=5,HullHealth=1}],Navigation=new(){CurrentSystemAddress=1}};
NavigationSafety.RequireLocalSelection(healthy,1);
Check.Throws<FlightTelemetryUnavailableException>(()=>NavigationSafety.RequireLocalSelection(healthy,2));
foreach(var unsafeState in new[]{healthy with{InGame=false},healthy with{Ships=[new(){ShipId=5,HullHealth=.49}]},healthy with{Navigation=new(){CurrentSystemAddress=1,ExternalThreat=true}}})
{
try{NavigationSafety.RequireLocalSelection(unsafeState,2);throw new Exception("Unsafe target selection was accepted.");}
catch(InvalidOperationException error){Check.That(!NavigationRecovery.CanRetry(error,CancellationToken.None));}
}
}
}
@@ -132,6 +132,37 @@ public class NativeFlightTestProtectionTests
catch(OperationCanceledException)when(token.IsCancellationRequested){}
catch(IOException)when(token.IsCancellationRequested){}
}
[Test] public async Task ManualVisualHandoffStopsInputsWithoutExitOrRestart()
{
var pid=Random.Shared.Next(280000000,290000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5));
var sessions=new Sessions();sessions.Target=sessions.Target with{ProcessId=pid};var hash=new string('a',64);var now=DateTimeOffset.UtcNow;
var player=new PlayerState{CommanderId="F1",InGame=true,CurrentShipId=7,LoadGameAt=now.AddMinutes(-1),Ships=[new(){ShipId=7,HullHealth=1,HullHealthAt=now,ConfigurationAt=now}]};
var server=ServeHealth(pid,hash,_=>99,stop.Token,independent:true);
try
{
await using var protection=await NativeFlightTestProtection.ArmAsync(pid,hash,sessions,()=>player,()=>{},stop.Token,automaticExit:false);
await NativeFlightTestProtection.ExitForFailedHandoffAsync(pid);
Check.That(protection.Stopped);Check.Throws<InvalidOperationException>(()=>NativeFlightTestProtection.CheckBeforeInput(pid));
Check.That(!sessions.Calls.Contains("exit")&&!sessions.Calls.Contains("launch")&&!sessions.Calls.Contains("validate-exit"));
}
finally{stop.Cancel();await server;}
}
[Test] public async Task ManualVisualDamageStopsInputsWithoutExitOrRestart()
{
var pid=Random.Shared.Next(290000000,300000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5));
var sessions=new Sessions();sessions.Target=sessions.Target with{ProcessId=pid};var hash=new string('a',64);var now=DateTimeOffset.UtcNow;
var player=new PlayerState{CommanderId="F1",InGame=true,CurrentShipId=7,LoadGameAt=now.AddMinutes(-1),Ships=[new(){ShipId=7,HullHealth=1,HullHealthAt=now,ConfigurationAt=now}]};
player=player with{StatusAt=now,Status=new((1L<<24)|8,0,1,1,0,null,null,null,null,[])};
var server=ServeHealth(pid,hash,_=>99,stop.Token,n=>n==1?1:.49,independent:true);
try
{
await using var protection=await NativeFlightTestProtection.ArmAsync(pid,hash,sessions,()=>player,()=>{},stop.Token,automaticExit:false);
while(!protection.Stopped)await Task.Delay(20,stop.Token);
Check.Throws<InvalidOperationException>(()=>NativeFlightTestProtection.CheckBeforeInput(pid));
Check.That(!sessions.Calls.Contains("exit")&&!sessions.Calls.Contains("launch")&&!sessions.Calls.Contains("validate-exit"));
}
finally{stop.Cancel();await server;}
}
[Test] public async Task BriefHealthStallBarsEveryInputThenResumesOnlyWithFreshOriginalSamples()
{
var pid=Random.Shared.Next(240000000,250000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5));var sessions=new Sessions();sessions.Target=sessions.Target with{ProcessId=pid};
@@ -10,11 +10,15 @@
- 订单 intent 在提交前落盘;恢复使用原检查点和 operation key,未知回执阻止重放。单目录锁阻止同一旅程并行执行。
- 持续独立健康观测与输入保护保留:实际受损后破盾或船体低于50%立即退出,初次无盾不触发受损退出。原始遥测未就绪时禁止输入。
## 尚未完整通过的路径
## 连续交接与待确认问题
首批载货到站时,SCA→自动降落交接触发保护退出,随后正常空间恢复完成交付。因此 `passed` 仅确认货物、维护、CG和钱包已对账,`continuousArrivalAccepted=false` 保留事实。不得延长保护时限或降低遥测要求以取得成功标记。
0.5.9 的空载复测仍在空管授权后触发两秒保护:控制器为零、原始输入已有回执,但实际油门仍为 .75,原生降落 duty 未接管。0.5.10 补充持久零油门的提交代次,阻止嵌套临时输入恢复再次写回旧值,且只影响这次原始请求确认过的当前主船油门;玩家后续输入与其余轴不会被恢复覆盖。只读遥测提供提交与跳过旧恢复的计数用于实机诊断。新增内存边界及嵌套恢复回归通过;连续交接仍以实际到站报告为准,不能用离线测试替代。
0.5.9、0.5.10的空载复测仍在空管授权后触发两秒保护:控制器为零、原始输入已有回执,但实际油门仍为 .75,原生降落 duty 未接管。0.5.10补充持久零油门的提交代次,阻止嵌套临时输入恢复再次写回旧值,且只影响这次原始请求确认过的当前主船油门;玩家后续输入与其余轴不会被恢复覆盖。新增内存边界及嵌套恢复回归通过,但不能据此认定已解决实机故障。
2026-10-24的人工观察航程首次连续完成原始跃迁、SCA、空管授权、原生自动降落接管及Homer实际停靠,最终报告的 `passed/arrived/released/cruiseEngaged/dockingGranted/dockingEngaged/finalDocked` 全部为true。该轮经用户明确授权使用 `manualVisual=true/automaticExitEnabled=false`,交接判定、真实遥测及原有超时均未放宽;空管授权至首次AI接管采样约2.03秒。随后用户明确恢复正式顺序跑商。历史载货旅程的 `continuousArrivalAccepted=false` 不回填为成功。
此次加载0.5.11的新增停靠更新点 `dockingZeroUpdateMatched/Applied` 均为0,成功不能归因于它。离线发现该更新点误把选择确认的短TTL当成整个飞行目标的寿命;源码已改为保留目标身份、代次和控制所有权核验,以当前输入命令自己的有效期约束提交,回归覆盖过期确认、过期命令和变化目标。这项后续修正尚未更换到正在运行的游戏,仍需后续独立实测。故障根因与后台持续复现情况尚未确认。
原始返回菜单路径未通过实机;只有插件明确声明 `nativeReturnToMenuVerified=true` 才允许该路径。从已就绪主菜单进入Solo使用独立入口能力。
@@ -30,6 +34,8 @@
`automatic-trading/runner.lock` 阻止重复控制,`state.json` 记录实际阶段。写入 `automatic-trading/stop` 可取消当前工作流;已接受的金钱请求仍按原操作键完成回执核对。中断后复用原工作目录和旅程检查点,未知订单只能核对,不能重放。原始卖出和已确认费用全部入账后才接受新收益。
用户需要保留现场做视觉检查时,先暂停正式流程和其自动退出观察器,再使用 `prepare-cg-visual <PID> <本机工作目录>` 只读核对游戏、会话、主船及空舱并绑定准备记录。只有用户明确“开始”后才执行 `run-cg-visual <PID> <同一目录>`;该单次空载航程不采购、出售、维护或报名,异常时停止本程序输入并保留游戏。该模式覆盖交接和真实受损均不自动退出,生产默认保护保持启用。用户明确恢复后才能再次启动正式流程。
`cg-cargo-journey <PID> <旅程目录>` 单独执行或恢复一批 Corsair/5、192t 容量、Duamta/Polyakov→AD Leonis/Homer 的 CG 运输;直接调用时默认 Uranium 192t,已有 `plan.json` 决定恢复的商品、数量和限价。首次采购前要求本机原1t已完成回执及CG成员证明。执行前仍读原始价格和库存。每站维护与交易均完成才离站,保留实际重购资金;提高后的CG报价不得再乘8。
继续已有货物必须复用原目录。原始回执、钱包、会话身份及日志只存本机,不能提交到仓库。`continuousArrivalAccepted` 必须来自同一完整到站报告,不能用正常空间重试替代。未领取CG奖励不计入净利润。
@@ -35,7 +35,7 @@ Require(Call(navigation, "XFENavigationGetAbiVersion") == 1, "Navigation ABI");
var checks = new (string Suffix, uint Expected)[]
{
("CommerceBatch",255),("CommunityGoal",255),("LocalDockingKind",255),
("Observer",7),("CommerceDocked",127),("LocalDockingHandoff",16777215),("ShipHealth",255),("LocalTargets",255),("LocalEntityDirectory",255),("LocalSelection",255),("LocalDockingAvailability",31),("LocalPlayerIdentity",255),
("Observer",7),("CommerceDocked",127),("LocalDockingHandoff",16777215),("DockingZeroUpdate",255),("ShipHealth",255),("LocalTargets",255),("LocalEntityDirectory",255),("LocalSelection",255),("LocalDockingAvailability",31),("LocalPlayerIdentity",255),
("OverlayLifetime",31),("ComponentLookup",63),("FsdCancellation",127),("ThrottleControl",255),
("TargetLookup",63),("ActorSelection",63),("FsdReturn",3),("CommitGate",31),("OutwardThrottle",31),
("RouteArguments",31),("StellarSpeed",255),("ReferenceGeometry",8191),("AdaptiveMotion",1023),
@@ -0,0 +1,65 @@
namespace XFEEDToolBox.Plugin;
internal static unsafe partial class Navigation
{
private const nuint DockingUpdate=0xE9A160;
private static nuint originalDockingUpdate;
private struct DockingZeroUpdate
{
public LocalActionRequest command;
public nuint computer,controller,fsd;
public uint market;
public float throttle;
}
private static DockingZeroUpdate dockingZeroUpdate;
private static ulong dockingZeroUpdateCalls,dockingZeroUpdateMatched,dockingZeroUpdateApplied;
private static bool DockingZeroUpdateMatches(in DockingZeroUpdate wanted,in Snapshot frame,in LocalSelection selection,
nuint computer,ulong now,ulong epoch,bool ownerActive,bool enabled,bool active,bool playerOverride)
=>enabled&&active&&ownerActive&&!playerOverride&&frame.valid&&frame.healthValid&&frame.hullFraction>=.5&&!frame.healthEmergency&&frame.rawFsdState==2
&&now>=frame.tick&&now-frame.tick<=200&&wanted.command.expires>now
&&wanted.computer==computer&&computer!=0&&wanted.command.entity==frame.entity&&wanted.command.life==frame.life
&&wanted.command.inputRoot==frame.inputRoot&&wanted.command.world==frame.referenceWorld&&wanted.fsd==frame.fsd
&&wanted.command.sequence>0&&wanted.command.sequence<=frame.sequence
&&wanted.command.epoch==epoch&&selection.epoch==epoch&&wanted.command.selection==selection.generation
&&selection.entity==frame.entity&&selection.life==frame.life&&selection.inputRoot==frame.inputRoot&&selection.fsd==frame.fsd
&&wanted.market!=0&&selection.target.market==wanted.market;
// selection.expires is the selection acknowledgement's short TTL. Its
// identity and generation remain bound throughout the flight; the fresh
// action's own expires controls this input request's lifetime.
// The normal input refresh can replace a zero written at the end of FSD
// processing. Deliver the already authorised real throttle input at the
// original docking computer's consumption point, before its own update.
// No AI state, permission, reported telemetry, steering or translation is
// written here. The request expires with its original control frame.
private static void ApplyDockingZeroBeforeUpdate(nuint obj)
{
dockingZeroUpdateCalls++;
if(GetCurrentThreadId()!=owningThread)return;
if(Volatile.Read(ref dockingZeroUpdate.command.expires)<=GetTickCount64())return;
var frame=Current();DockingZeroUpdate wanted;LocalSelection selection;LocalFlightObservation observed;
lock(localLock){wanted=dockingZeroUpdate;selection=localSelection;observed=localObservation;}
if(!DockingZeroUpdateMatches(wanted,frame,selection,obj+0x58,GetTickCount64(),controllerEpoch,
clientOwner.Length!=0,Enabled(),inGame,overridden)||localConfirmed!=selection.generation||fsdOwned||!BoundFsd(frame,frame.fsd))return;
var computer=LocalDockingComputer(frame);if(computer!=wanted.computer||computer!=obj+0x58||!LocalDockingAvailable(computer))return;
dockingZeroUpdateMatched++;
if((byte)Call(image+0xED6620,computer)!=0)
{lock(localLock)dockingZeroUpdate.command.expires=0;return;}
var now=GetTickCount64();var receipt=dockingZeroInputReceipt;
if(!observed.valid||observed.entity!=frame.entity||observed.inSupercruise||observed.distance>7500
||now<observed.tick||now-observed.tick>200||!SameLocal(observed.target,selection.target))return;
if(receipt.entity!=frame.entity||receipt.life!=frame.life||receipt.market!=wanted.market||receipt.inputService!=Pointer(computer+0x230)
||now<receipt.tick||now-receipt.tick>=30000||Pointer(computer+0x240)!=wanted.controller)return;
var dock=LocalDocking(frame);if(dock==0||Read<uint>(dock+0x614)!=2||Read<uint>(dock+0x4C0)is not(3 or 4)||Read<byte>(dock+0x4E9)==255)return;
var station=Pointer(dock+0x4E0);if(station==0||Read<uint>(station+0xC0)!=8||Read<ulong>(station+0xB0)!=wanted.market)return;
if(!LocalRecordMatches(Pointer(frame.fsd+0x338),selection.target))return;
bool released;lock(leaseLock)released=lease.expires<=now&&fsdLease.expires<=now;
if(CommitOriginalDockingZeroAxis(frame.inputRoot,wanted.controller,wanted.throttle,true,released,out _))dockingZeroUpdateApplied++;
}
[UnmanagedCallersOnly]
private static ulong DockingUpdateHook(nuint obj,float dt)
{
try{ApplyDockingZeroBeforeUpdate(obj);}catch{}
try{return Call(originalDockingUpdate,(ulong)obj,F32(dt));}catch{return 0;}
}
}
@@ -0,0 +1,37 @@
namespace XFEEDToolBox.Plugin;
internal static unsafe partial class Navigation
{
private static uint DockingZeroUpdateSelfTest()
{
var frame=new Snapshot{valid=true,healthValid=true,hullFraction=1,rawFsdState=2,tick=100,sequence=20,entity=7,life=0x10000,inputRoot=0x20000,fsd=0x30000,referenceWorld=9};
var selection=new LocalSelection{generation=3,expires=200,epoch=5,entity=frame.entity,life=frame.life,inputRoot=frame.inputRoot,fsd=frame.fsd,target=new(){market=11}};
var wanted=new DockingZeroUpdate{computer=0x40058,controller=0x50000,fsd=frame.fsd,market=11,throttle=.75f,
command=new(){expires=200,sequence=20,entity=frame.entity,life=frame.life,inputRoot=frame.inputRoot,world=9,epoch=5,selection=3}};
bool Match(DockingZeroUpdate p,Snapshot f,LocalSelection s,ulong now=150,ulong epoch=5,bool owner=true,bool enabled=true,bool active=true,bool overridden=false,nuint computer=0x40058)
=>DockingZeroUpdateMatches(p,f,s,computer,now,epoch,owner,enabled,active,overridden);
uint passed=0;
if(Match(wanted,frame,selection))passed|=1;
if(!Match(wanted,frame,selection,200)&&Match(wanted,frame,selection,199))passed|=2;
var other=wanted;other.command.entity++;
if(!Match(other,frame,selection)){other=wanted;other.command.world++;if(!Match(other,frame,selection))passed|=4;}
other=wanted;other.command.epoch++;
if(!Match(other,frame,selection)&&!Match(wanted,frame,selection,owner:false))passed|=8;
var unsafeFrame=frame;unsafeFrame.hullFraction=.49;
if(!Match(wanted,unsafeFrame,selection)){unsafeFrame=frame;unsafeFrame.healthEmergency=true;if(!Match(wanted,unsafeFrame,selection))passed|=16;}
if(!Match(wanted,frame,selection,computer:0)&&!Match(wanted,frame,selection,computer:0x60058))passed|=32;
if(!Match(wanted,frame,selection,enabled:false)&&!Match(wanted,frame,selection,active:false)&&!Match(wanted,frame,selection,overridden:true))passed|=64;
var acknowledged=selection;acknowledged.expires=1;
if(Match(wanted,frame,acknowledged)&&!Match(wanted,frame,acknowledged,200))
{
var changed=acknowledged;changed.target.market++;
if(!Match(wanted,frame,changed))
{changed=acknowledged;changed.generation++;if(!Match(wanted,frame,changed))passed|=128;}
}
return passed;
}
#if PLUGIN_NAVIGATION
[UnmanagedCallersOnly(EntryPoint="XFENavigationDockingZeroUpdateSelfTest")]
internal static uint ExportDockingZeroUpdateSelfTest()=>RunTest(DockingZeroUpdateSelfTest);
#endif
}
@@ -12,19 +12,21 @@ internal static unsafe partial class Navigation
if(initialized)return true;enabledProvider=enabled;if(installationAttempted)return false;
if(Platform.ImageHash(Platform.ModulePath())!=GameHash)return false;image=Platform.ImageBase;imageEnd=Platform.ImageEnd;
if(!Prefix(FlowUpdate,[0x40,0x53,0x48,0x83,0xEC,0x30,0x33,0xC0])||!Prefix(PhysicsUpdate,[0x48,0x8B,0xC4,0x55,0x53,0x56,0x41,0x54])||!Prefix(InputEvaluator,[0x48,0x8B,0xC4,0x57,0x48,0x81,0xEC,0x30,0x01,0,0])||!Prefix(FsdUpdate,[0x48,0x89,0x5C,0x24,0x18,0x55,0x56,0x57])||!Prefix(BodyLookup,[0x48,0x89,0x5C,0x24,0x10,0x57,0x48,0x83,0xEC,0x20])||!Prefix(HeatInput,[0x48,0x89,0x5C,0x24,0x08,0x57,0x48,0x83,0xEC,0x50]))return false;
if(!Prefix(DockingUpdate,[0x40,0x55,0x53,0x56,0x57,0x41,0x54,0x41,0x55,0x41,0x56]))return false;
installationAttempted=true;var status=Platform.HookInitialize();if(status!=0&&status!=1)return false;
flowStub=Platform.Observer((nuint)(delegate* unmanaged<nuint,nuint,void>)&ObserveFlow,out originalFlow,image+FlowUpdate);if(flowStub==0)return false;
if(Platform.HookCreate(image+PhysicsUpdate,(nuint)(delegate* unmanaged<nuint,float,void>)&PhysicsHook,out originalPhysics)!=0){Platform.HookRemove(image+FlowUpdate);return false;}
if(Platform.HookCreate(image+InputEvaluator,(nuint)(delegate* unmanaged<nuint,float,ulong>)&InputEvaluationHook,out originalInputEvaluation)!=0){Platform.HookRemove(image+PhysicsUpdate);Platform.HookRemove(image+FlowUpdate);return false;}
if(Platform.HookCreate(image+FsdUpdate,(nuint)(delegate* unmanaged<nuint,float,ulong>)&FsdHook,out originalFsd)!=0){Platform.HookRemove(image+InputEvaluator);Platform.HookRemove(image+PhysicsUpdate);Platform.HookRemove(image+FlowUpdate);return false;}
if(Platform.HookCreate(image+BodyLookup,(nuint)(delegate* unmanaged<nuint,nuint,nuint,nuint>)&BodyLookupHook,out originalBodyLookup)!=0){Platform.HookRemove(image+FsdUpdate);Platform.HookRemove(image+InputEvaluator);Platform.HookRemove(image+PhysicsUpdate);Platform.HookRemove(image+FlowUpdate);return false;}
if(Platform.HookCreate(image+DockingUpdate,(nuint)(delegate* unmanaged<nuint,float,ulong>)&DockingUpdateHook,out originalDockingUpdate)!=0){Platform.HookRemove(image+FsdUpdate);Platform.HookRemove(image+InputEvaluator);Platform.HookRemove(image+PhysicsUpdate);Platform.HookRemove(image+FlowUpdate);return false;}
if(Platform.HookCreate(image+BodyLookup,(nuint)(delegate* unmanaged<nuint,nuint,nuint,nuint>)&BodyLookupHook,out originalBodyLookup)!=0){Platform.HookRemove(image+DockingUpdate);Platform.HookRemove(image+FsdUpdate);Platform.HookRemove(image+InputEvaluator);Platform.HookRemove(image+PhysicsUpdate);Platform.HookRemove(image+FlowUpdate);return false;}
heatInputStub=Platform.Observer((nuint)(delegate* unmanaged<nuint,float,nuint,float,void>)&ObserveHeatInput,out originalHeatInput,image+HeatInput);
heatTickStub=Platform.Observer((nuint)(delegate* unmanaged<nuint,float,void>)&ObserveHeatTick,out originalHeatTick,image+HeatTick);
nuint[] targets=[FlowUpdate,PhysicsUpdate,InputEvaluator,FsdUpdate,BodyLookup,HeatInput,HeatTick];var queued=heatInputStub!=0&&heatTickStub!=0;
nuint[] targets=[FlowUpdate,PhysicsUpdate,InputEvaluator,FsdUpdate,DockingUpdate,BodyLookup,HeatInput,HeatTick];var queued=heatInputStub!=0&&heatTickStub!=0;
if(queued)foreach(var target in targets)if(Platform.HookQueueEnable(image+target)!=0){queued=false;break;}
if(!queued||Platform.HookApplyQueued()!=0){foreach(var target in targets){Platform.HookDisable(image+target);Platform.HookRemove(image+target);}return false;}
try{PipeServer.StartJson("Navigation",65536,Handle);}catch{
Platform.HookDisable(image+FlowUpdate);Platform.HookDisable(image+PhysicsUpdate);Platform.HookDisable(image+InputEvaluator);Platform.HookDisable(image+FsdUpdate);Platform.HookDisable(image+BodyLookup);return false;
Platform.HookDisable(image+FlowUpdate);Platform.HookDisable(image+PhysicsUpdate);Platform.HookDisable(image+InputEvaluator);Platform.HookDisable(image+FsdUpdate);Platform.HookDisable(image+DockingUpdate);Platform.HookDisable(image+BodyLookup);return false;
}
initialized=true;
if(Prefix(SessionMenuUpdate,[0x40,0x56,0x41,0x54,0x48,0x83,0xEC,0x58])&&Prefix(SessionPauseUpdate,[0x40,0x53,0x41,0x57,0x48,0x83,0xEC,0x58])){