XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
返回提交历史

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
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

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