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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

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

XFEstudio/XFEEDToolBox

Keep ship protection alive after navigation failures

Run health protection in an independent C# service and require fresh process-bound health witnesses before automation input. Preserve damage handling after navigation cleanup, restore protection state after rebuy, and permit verified pause-state health reads without enabling flight input. Prepare normal-space zero throttle before requesting docking clearance and reconcile original rebuy costs with the native wallet. The throttle change still requires live empty-cargo validation. Validation: 14 protection and 1 rebuy regressions; 29 navigation regressions plus runner tests; isolated CLI publish; 656 isolated native self-test invocations and original ABI checks. Installed package health protection is actually armed; live flight acceptance remains in progress.

f665835
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

21 个文件 +385 -46
Modified XFEEDToolBox.Core/Data/PlayerProjector.cs +6 -1
@@ -146,6 +146,12 @@ public sealed class PlayerProjector
146 146 case "UnderAttack":
147 147 case "Interdicted": s = s with { InDanger = true }; break;
148 148 case "Died": s = s with { InDanger = true, InGame = false }; break;
149 case "Resurrect":
150 // Rebuy resumes this client without a second LoadGame event.
151 s = s with { InGame = true, InDanger = false, LoadGameAt = time, Docked = false,
152 StationName = "", MarketId = null, StationServices = [], StationServicesKnown = false,
153 CreditsReliable = false, CargoReliable = false, StatusReadAt = null };
154 break;
149 155 case "Shutdown": s = s with { InGame = false }; break;
150 156 case "RefuelAll":
151 157 case "RefuelPartial":
@@ -180,7 +186,6 @@ public sealed class PlayerProjector
180 186 // Transactions with complex pricing cannot safely be inferred from one field.
181 187 case "ShipyardSell":
182 188 case "ShipyardSwap":
183 case "Resurrect":
184 189 case "CarrierBankTransfer":
185 190 case "BuyMicroResources":
186 191 case "SellMicroResources":
Modified XFEEDToolBox.Core/Navigation/NativeSystemFlightControlDriver.cs +2 -2
@@ -245,8 +245,8 @@ public sealed class NativeSystemFlightControlDriver(int processId,string gameHas
245 245 if(expiresAt<=DateTimeOffset.UtcNow)throw new FlightTelemetryUnavailableException("正在刷新星系内飞行数据。");
246 246 if(decision.Actions.HasFlag(SystemNavigationAction.ExitForFailedHandoff))
247 247 {
248 await Session.NativeFlightTestProtection.ExitForFailedHandoffAsync(processId);
249 emergencyExited=true;ownsController=false;return;
248 emergencyExited=await Session.NativeFlightTestProtection.HandleFailedHandoffAsync(processId);
249 if(emergencyExited)ownsController=false;return;
250 250 }
251 251 var actions=decision.Actions;
252 252 if((actions&SystemNavigationAction.CancelOwnedAssists)!=0)
Modified XFEEDToolBox.Core/Navigation/SystemFlightNavigator.cs +14 -0
@@ -7,6 +7,8 @@ public sealed class SystemFlightNavigator
7 7 private DateTimeOffset lastDockRequest=DateTimeOffset.MinValue,lastAssistRequest=DateTimeOffset.MinValue,lastEntryRequest=DateTimeOffset.MinValue,lastDropRequest=DateTimeOffset.MinValue;
8 8 private DateTimeOffset? nativeLandingSince;
9 9 private bool nativeLandingObserved;
10 private bool preparingNormalThrottle;
11 private DateTimeOffset? normalThrottleZeroSince;
10 12 private DateTimeOffset? assistAlignedSince;
11 13 private bool softwareCruise;
12 14 private bool softwareLanding;
@@ -180,6 +182,18 @@ public sealed class SystemFlightNavigator
180 182 !softwareLanding?1:NormalThrottle(frame,frame.TargetDistance,6500));
181 183 if(clearance is {Permission:DockingPermission.Denied,Retryable:false}&&clearance.MarketId==target.MarketId)
182 184 return Stop("空间站拒绝停靠:"+clearance.Reason);
185 if(frame.Assists.AutoDock&&!softwareLanding&&(Math.Abs(frame.Throttle)>.001||preparingNormalThrottle))
186 {
187 // A drop can place the ship directly inside ATC range while
188 // its input producer still holds SCA's .75. Exercise ordinary
189 // normal-space zero input before a grant makes that lease yield.
190 preparingNormalThrottle=true;
191 if(Math.Abs(frame.Throttle)>.001)normalThrottleZeroSince=null;
192 else normalThrottleZeroSince??=now;
193 if(normalThrottleZeroSince is null||now-normalThrottleZeroSince<TimeSpan.FromMilliseconds(100))
194 return new(SystemNavigationPhase.Preparing,"正在确认正常空间零油门,再请求空管。",direction,0);
195 preparingNormalThrottle=false;normalThrottleZeroSince=null;
196 }
183 197 var action=SystemNavigationAction.None;
184 198 if(now-lastDockRequest>=TimeSpan.FromSeconds(10))
185 199 {lastDockRequest=now;action=SystemNavigationAction.RequestDocking;}
Modified XFEEDToolBox.Core/Session/NativeFlightTestProtection.cs +11 -6
@@ -18,6 +18,7 @@ public sealed class NativeFlightTestProtection:IAsyncDisposable
18 18 private readonly bool ownsMonitor;
19 19 private readonly bool automaticExit;
20 20 private readonly bool exitForFailedHandoff;
21 private readonly Action? externalProtectionCheck;
21 22 private readonly IGameSessionDriver sessionDriver;
22 23 private bool independentHealth;
23 24 private Task? polling;
@@ -28,26 +29,28 @@ public sealed class NativeFlightTestProtection:IAsyncDisposable
28 29 public string? LastIssue{get;private set;}
29 30 public bool Stopped=>Volatile.Read(ref unsafeState)!=0;
30 31 public bool Fresh=>!Stopped&&DateTimeOffset.UtcNow.UtcTicks-Interlocked.Read(ref lastHealthTicks)<=TimeSpan.FromMilliseconds(500).Ticks;
31 private NativeFlightTestProtection(int pid,GameSessionTarget target,IGameSessionDriver sessions,Func<PlayerState> player,Action stopAutomation,ShipProtectionMonitor? sharedMonitor,bool automaticExit,bool exitForFailedHandoff)
32 private NativeFlightTestProtection(int pid,GameSessionTarget target,IGameSessionDriver sessions,Func<PlayerState> player,Action stopAutomation,ShipProtectionMonitor? sharedMonitor,bool automaticExit,bool exitForFailedHandoff,Action? externalProtectionCheck)
32 33 {
33 pipe=new(pid);this.target=target;this.player=player;this.stopAutomation=stopAutomation;sessionDriver=sessions;this.automaticExit=automaticExit;this.exitForFailedHandoff=exitForFailedHandoff;
34 pipe=new(pid);this.target=target;this.player=player;this.stopAutomation=stopAutomation;sessionDriver=sessions;this.automaticExit=automaticExit;this.exitForFailedHandoff=exitForFailedHandoff;this.externalProtectionCheck=externalProtectionCheck;
34 35 ownsMonitor=sharedMonitor is null;
35 36 monitor=sharedMonitor??new(sessions,player,()=>{Interlocked.Exchange(ref unsafeState,1);StopAutomation();},automaticExit);
36 37 }
37 38 private void StopAutomation(){try{stopAutomation();}catch{/* A stopped worker cannot disarm health monitoring. */}}
38 public static async Task ExitForFailedHandoffAsync(int pid)
39 public static async Task ExitForFailedHandoffAsync(int pid)=>await HandleFailedHandoffAsync(pid);
40 public static async Task<bool> HandleFailedHandoffAsync(int pid)
39 41 {
40 42 var protection=Volatile.Read(ref active);
41 43 if(protection is null||protection.target.ProcessId!=pid)throw new InvalidOperationException("无法为未绑定的游戏实例执行保护退出。");
42 44 Interlocked.Exchange(ref protection.unsafeState,1);
43 45 protection.LastIssue="自动降落未接管,飞船仍在移动;已停止操作并保护退出。";
44 46 protection.StopAutomation();
45 if(!protection.automaticExit||!protection.exitForFailedHandoff){protection.LastIssue="自动降落未接管,已停止自动输入并保留游戏现场。";return;}
47 if(!protection.automaticExit||!protection.exitForFailedHandoff){protection.LastIssue="自动降落未接管,已停止自动输入并保留游戏现场。";return false;}
46 48 // Do not restart into the same unresolved approach or let cancelling
47 49 // the navigation worker interrupt the already required protective exit.
48 50 await new GameSessionController(protection.sessionDriver,()=>false).EmergencyExitAsync(protection.target,CancellationToken.None);
51 return true;
49 52 }
50 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,bool exitForFailedHandoff=true)
53 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,bool exitForFailedHandoff=true,Action? externalProtectionCheck=null)
51 54 {
52 55 if(Volatile.Read(ref active) is not null)throw new InvalidOperationException("已有飞行测试保护正在运行。");
53 56 var pipe=new NavigationPipeClient(pid);var hello=await pipe.CallAsync(new{op="describe"},token);
@@ -59,7 +62,8 @@ public sealed class NativeFlightTestProtection:IAsyncDisposable
59 62 var target=await sessions.FindTargetAsync(token);if(target.ProcessId!=pid)throw new InvalidOperationException("飞行测试与受保护游戏实例不一致。");
60 63 if(sharedMonitor is not null&&sharedMonitor.BoundTarget!=target)throw new InvalidOperationException("连续血量监测与受保护游戏实例不一致。");
61 64 if(sharedMonitor is not null&&sharedMonitor.AutomaticExitEnabled!=automaticExit)throw new InvalidOperationException("连续血量监测的退出模式不一致。");
62 var protection=new NativeFlightTestProtection(pid,target,sessions,player,stopAutomation,sharedMonitor,automaticExit,exitForFailedHandoff);
65 externalProtectionCheck?.Invoke();
66 var protection=new NativeFlightTestProtection(pid,target,sessions,player,stopAutomation,sharedMonitor,automaticExit,exitForFailedHandoff,externalProtectionCheck);
63 67 protection.independentHealth=hello.TryGetProperty("independentShipHealth",out var independent)&&independent.ValueKind==JsonValueKind.True;
64 68 try
65 69 {
@@ -75,6 +79,7 @@ public sealed class NativeFlightTestProtection:IAsyncDisposable
75 79 {
76 80 var protection=Volatile.Read(ref active);
77 81 if(protection is null)return;
82 protection.externalProtectionCheck?.Invoke();
78 83 if(protection.target.ProcessId!=pid||Volatile.Read(ref protection.unsafeState)!=0||protection.monitor.EmergencyActive||protection.monitor.TestingBlocked)
79 84 throw new InvalidOperationException("飞船保护已停止测试,禁止继续发送飞行输入。");
80 85 if(DateTimeOffset.UtcNow.UtcTicks-Interlocked.Read(ref protection.lastHealthTicks)>TimeSpan.FromMilliseconds(500).Ticks)
Modified XFEEDToolBox.Test/NativeFlightTestProtectionTests.cs +40 -0
@@ -183,6 +183,46 @@ public class NativeFlightTestProtectionTests
183 183 }
184 184 finally{stop.Cancel();await server;}
185 185 }
186 [Test] public async Task IndependentJournalProtectionSurvivesFailedNavigationCleanup()
187 {
188 var pid=Random.Shared.Next(310000000,320000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5));
189 var sessions=new Sessions();sessions.Target=sessions.Target with{ProcessId=pid};var hash=new string('a',64);var now=DateTimeOffset.UtcNow;
190 var player=new PlayerState{CommanderId="F1",InGame=true,CurrentShipId=7,LoadGameAt=now.AddMinutes(-1),StatusAt=now,
191 Status=new((1L<<24)|8,0,1,1,0,null,null,null,null,[]),Ships=[new(){ShipId=7,HullHealth=1,HullHealthAt=now,ConfigurationAt=now}]};
192 await using var independent=new ShipProtectionMonitor(sessions,()=>player,()=>{});
193 await independent.EnableAsync(stop.Token);var server=ServeHealth(pid,hash,_=>99,stop.Token,independent:true);
194 try
195 {
196 await using(var navigation=await NativeFlightTestProtection.ArmAsync(pid,hash,sessions,()=>player,()=>{},stop.Token,automaticExit:false))
197 {
198 await NativeFlightTestProtection.ExitForFailedHandoffAsync(pid);
199 Check.That(navigation.Stopped);Check.That(!sessions.Calls.Contains("exit"));
200 }
201 // The failed navigation and its native monitor are both disposed.
202 // The independently owned journal guard must still process damage.
203 var at=DateTimeOffset.UtcNow;
204 independent.Observe(new(1,at,Check.Event(new{timestamp=at,@event="HullDamage",Health=.35,PlayerPilot=true,Fighter=false})),player);
205 while(!sessions.Calls.Contains("exit"))await Task.Delay(20,stop.Token);
206 Check.Equal(1,sessions.Calls.Count(x=>x=="exit"));
207 await independent.Completion;
208 }
209 finally{stop.Cancel();await server;}
210 }
211 [Test] public async Task LostIndependentAuthorityBarsInputEvenWhenLocalHealthIsFresh()
212 {
213 var pid=Random.Shared.Next(320000000,330000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5));
214 var sessions=new Sessions();sessions.Target=sessions.Target with{ProcessId=pid};var hash=new string('a',64);var now=DateTimeOffset.UtcNow;var live=true;
215 var player=new PlayerState{CommanderId="F1",InGame=true,CurrentShipId=7,LoadGameAt=now.AddMinutes(-1),Ships=[new(){ShipId=7,HullHealth=1,HullHealthAt=now,ConfigurationAt=now}]};
216 var server=ServeHealth(pid,hash,_=>99,stop.Token,independent:true);
217 try
218 {
219 await using var p=await NativeFlightTestProtection.ArmAsync(pid,hash,sessions,()=>player,()=>{},stop.Token,automaticExit:false,
220 externalProtectionCheck:()=>{if(!live)throw new InvalidOperationException("Independent protection lost.");});
221 NativeFlightTestProtection.CheckBeforeInput(pid);live=false;Check.That(p.Fresh);
222 Check.Throws<InvalidOperationException>(()=>NativeFlightTestProtection.CheckBeforeInput(pid));Check.That(!sessions.Calls.Contains("exit"));
223 }
224 finally{stop.Cancel();await server;}
225 }
186 226 [Test] public async Task BriefHealthStallBarsEveryInputThenResumesOnlyWithFreshOriginalSamples()
187 227 {
188 228 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};
Added XFEEDToolBox.Test/ResurrectionProtectionTests.cs +22 -0
@@ -0,0 +1,22 @@
1 using XFEEDToolBox.Core.Data;
2 using XFEExtension.NetCore.XUnit.Attributes;
3
4 namespace XFEEDToolBox.Test;
5
6 [TestFixture]
7 public class ResurrectionProtectionTests
8 {
9 [Test] public void RebuyStartsNewProtectionIdentityWithoutTrustingOldDockWalletOrStatus()
10 {
11 var p=new PlayerProjector();var at=DateTimeOffset.UtcNow.AddMinutes(-2);
12 p.Apply(Check.Event(new{timestamp=at,@event="LoadGame",FID="F1",Commander="Fixture",Ship="corsair",GameMode="Solo",Credits=10000000}));
13 p.Apply(Check.Event(new{timestamp=at.AddSeconds(1),@event="Location",Docked=true,StarSystem="Fixture",StationName="Old",MarketID=1}));
14 p.Apply(Check.Event(new{timestamp=at.AddSeconds(2),@event="Died"}));Check.That(!p.State.InGame);
15 var rebuy=at.AddSeconds(3);p.Apply(Check.Event(new{timestamp=rebuy,@event="Resurrect",Option="rebuy",Cost=4427098}));
16 Check.That(p.State.InGame&&!p.State.InDanger&&!p.State.Docked);Check.Equal(rebuy,p.State.LoadGameAt!.Value);
17 Check.That(!p.State.CreditsReliable&&!p.State.CargoReliable&&!p.State.StationServicesKnown&&p.State.StatusReadAt is null);
18 Check.That(p.State.MarketId is null);Check.Equal("F1",p.State.CommanderId);Check.Equal("Solo",p.State.GameMode);
19 p.Apply(Check.Event(new{timestamp=at.AddSeconds(4),@event="Location",Docked=true,StarSystem="Fixture",StationName="Recovered",MarketID=2}));
20 Check.That(p.State.Docked);Check.Equal(2L,p.State.MarketId!.Value);
21 }
22 }
Modified XFEEDToolBox.Test/SystemNavigationTests.cs +22 -1
@@ -23,6 +23,25 @@ public class SystemNavigationTests
23 23 var aligned=policy.Decide(frame with{Forward=new(0,0,1)},Station,Now);Check.Equal(1d,aligned.Throttle);Check.That(aligned.Actions.HasFlag(SystemNavigationAction.EnterSupercruise));
24 24 var locked=new SystemFlightNavigator().Decide(frame with{Forward=new(0,0,1),FsdReady=false},Station,Now);Check.That(!locked.Actions.HasFlag(SystemNavigationAction.EnterSupercruise));
25 25 }
26 [Test] public void DirectAtcRangeDropUsesOrdinaryZeroBeforeGrantCanYieldItsLease()
27 {
28 var p=new SystemFlightNavigator();var f=Frame with{Assists=new(false,true,false),DockingAssist=NativeAssistState.Ready,Speed=222.6,Throttle=.75,TargetDistance=6900};
29 var first=p.Decide(f,Station,Now);Check.Equal(SystemNavigationPhase.Preparing,first.Phase);Check.Equal(0d,first.Throttle);
30 Check.That(!first.Actions.HasFlag(SystemNavigationAction.RequestDocking)&&!first.Actions.HasFlag(SystemNavigationAction.ReleaseFlightAxes));
31 var zero=f with{Throttle=0,ObservedAt=Now.AddMilliseconds(60),Sequence=2};
32 Check.That(!p.Decide(zero,Station,Now.AddMilliseconds(60)).Actions.HasFlag(SystemNavigationAction.RequestDocking));
33 Check.That(p.Decide(zero with{ObservedAt=Now.AddMilliseconds(180),Sequence=3},Station,Now.AddMilliseconds(180)).Actions.HasFlag(SystemNavigationAction.RequestDocking));
34 }
35 [Test] public void AReturningScaThrottleResetsThePreAtcZeroConfirmation()
36 {
37 var p=new SystemFlightNavigator();var f=Frame with{Assists=new(false,true,false),DockingAssist=NativeAssistState.Ready,Throttle=.75};
38 p.Decide(f,Station,Now);p.Decide(f with{Throttle=0,ObservedAt=Now.AddMilliseconds(30)},Station,Now.AddMilliseconds(30));
39 p.Decide(f with{ObservedAt=Now.AddMilliseconds(90)},Station,Now.AddMilliseconds(90));
40 var zero=f with{Throttle=0,ObservedAt=Now.AddMilliseconds(120)};
41 Check.That(!p.Decide(zero,Station,Now.AddMilliseconds(120)).Actions.HasFlag(SystemNavigationAction.RequestDocking));
42 Check.That(!p.Decide(zero with{ObservedAt=Now.AddMilliseconds(180)},Station,Now.AddMilliseconds(180)).Actions.HasFlag(SystemNavigationAction.RequestDocking));
43 Check.That(p.Decide(zero with{ObservedAt=Now.AddMilliseconds(240)},Station,Now.AddMilliseconds(240)).Actions.HasFlag(SystemNavigationAction.RequestDocking));
44 }
26 45 [Test] public void ContactIsRequestedAtSevenPointFiveKilometresOnlyInNormalFlight()
27 46 {
28 47 var policy=new SystemFlightNavigator();var far=policy.Decide(Frame with{TargetDistance=7500.1},Station,Now);
@@ -96,7 +115,9 @@ public class SystemNavigationTests
96 115 foreach(var assist in new[]{new NavigationAssists(true,false,false),new NavigationAssists(false,true,false)})
97 116 {
98 117 var policy=new SystemFlightNavigator();var frame=Frame with{Assists=assist,DockingAssist=NativeAssistState.Ready,Throttle=.5};
99 var request=policy.Decide(frame,Station,Now);Check.That(request.Actions.HasFlag(SystemNavigationAction.RequestDocking));Check.Equal(SystemNavigationPhase.RequestingDocking,request.Phase);
118 var preparing=policy.Decide(frame,Station,Now);Check.Equal(SystemNavigationPhase.Preparing,preparing.Phase);Check.Equal(0d,preparing.Throttle);
119 policy.Decide(frame with{Throttle=0},Station,Now.AddMilliseconds(20));
120 var request=policy.Decide(frame with{Throttle=0},Station,Now.AddMilliseconds(140));Check.That(request.Actions.HasFlag(SystemNavigationAction.RequestDocking));Check.Equal(SystemNavigationPhase.RequestingDocking,request.Phase);
100 121 var allowed=policy.Decide(frame with{Clearance=Grant},Station,Now);Check.Equal(0d,allowed.Throttle);Check.Equal(SystemNavigationPhase.NativeLanding,allowed.Phase);
101 122 var accepted=policy.Decide(frame with{Clearance=Grant,Throttle=0,DockingAssist=NativeAssistState.Requested},Station,Now);Check.That(accepted.Actions.HasFlag(SystemNavigationAction.ReleaseFlightAxes));
102 123 var engaged=policy.Decide(frame with{Clearance=Grant,Throttle=0,DockingAssist=NativeAssistState.Engaged},Station,Now);Check.That(engaged.Actions.HasFlag(SystemNavigationAction.ReleaseFlightAxes));Check.That(engaged.Throttle is null);
Modified docs/CG-TRADING-VALIDATION.md +15 -1
@@ -10,6 +10,18 @@
10 10 - 订单 intent 在提交前落盘;恢复使用原检查点和 operation key,未知回执阻止重放。单目录锁阻止同一旅程并行执行。
11 11 - 持续独立健康观测与输入保护保留:实际受损后破盾或船体低于50%立即退出,初次无盾不触发受损退出。原始遥测未就绪时禁止输入。
12 12
13 ## 保护失效、修正及恢复条件
14
15 0.5.12进一步返程验证仍在脱出后直接请求空管的路径失败:原始输入缓冲已经归零,停靠电脑使用的输入来源仍为.75。随后发现导航清理会同时释放该段的受损监测,实际发生飞船损失。此前通过的单次到站报告不能证明持续跑商安全,不能绕过这个失败继续采购。
16
17 故障后使用 `ship-loss-hold.json` 和停止标记暂停飞行。用户已恢复原船,并在独立保护实际武装后明确要求继续跑商;据此解除人工等待,先进行空载连续交接验证,通过后继续采购和交付。重购成本已取原始Resurrect回执并与原始钱包核对,作为OtherExpense入账,不隐去损失。
18
19 0.5.13将保护改为独立常驻C#服务,导航和贸易只检查、复用它,不拥有它的生命周期。导航异常退出不停止该服务,自动流程每次输入前核对观察进程、游戏进程启动身份和500ms内的真实健康凭据;服务失联或数据过期立即禁止输入。实际受损退出由独立服务执行,流程内监测只负责停止输入,避免重复退出。13项生命周期等保护回归后又补充独立服务失联场景,共14项通过,另有重购状态恢复回归通过。
20
21 原生采样允许经过原始状态及主船身份核验的Pause状态继续读健康,飞行输入仍只允许原有InGame条件;没有延长健康数据有效期。部署包744次原生自测、原ABI/导出/PE与20状态/3生命周期握手通过。用户本机会话已验证独立保护实际武装并持续读取原始船体和护盾;不通过实机制造伤害测试退出。
22
23 重购后的Resurrect会恢复新的保护会话身份,并使旧停靠、旧状态读取及旧钱包重新等待验证。直接空管路径另增加正常空间零油门确认阶段,先让普通输入生效,再请求许可并交接;许可、真正归零、AI接管及原超时要求保持。29项导航回归通过,这项后续控制修正仍需实际空载航程验收,不能把离线通过当作稳定跑商的证明。
24
13 25 ## 连续交接与待确认问题
14 26
15 27 首批载货到站时,SCA→自动降落交接触发保护退出,随后正常空间恢复完成交付。因此 `passed` 仅确认货物、维护、CG和钱包已对账,`continuousArrivalAccepted=false` 保留事实。不得延长保护时限或降低遥测要求以取得成功标记。
@@ -18,7 +30,7 @@
18 30
19 31 2026-10-24的人工观察航程首次连续完成原始跃迁、SCA、空管授权、原生自动降落接管及Homer实际停靠,最终报告的 `passed/arrived/released/cruiseEngaged/dockingGranted/dockingEngaged/finalDocked` 全部为true。该轮经用户明确授权使用 `manualVisual=true/automaticExitEnabled=false`,交接判定、真实遥测及原有超时均未放宽;空管授权至首次AI接管采样约2.03秒。随后用户明确恢复正式顺序跑商。历史载货旅程的 `continuousArrivalAccepted=false` 不回填为成功。
20 32
21 此次加载0.5.11的新增停靠更新点 `dockingZeroUpdateMatched/Applied` 均为0,成功不能归因于它。离线发现该更新点误把选择确认的短TTL当成整个飞行目标的寿命;源码已改为保留目标身份、代次和控制所有权核验,以当前输入命令自己的有效期约束提交,回归覆盖过期确认、过期命令和变化目标。这项后续修正尚未更换到正在运行的游戏,仍需后续独立实测。故障根因与后台持续复现情况尚未确认。
33 此次加载0.5.11的新增停靠更新点 `dockingZeroUpdateMatched/Applied` 均为0,成功不能归因于它。离线发现该更新点误把选择确认的短TTL当成整个飞行目标的寿命;后续0.5.12保留目标身份、代次和控制所有权核验,以当前输入命令自己的有效期约束提交。它已在普通接近路径取得接管,但直接空管路径仍失败;不能认定根因已解决。
22 34
23 35 恢复正式流程后的0.5.11返程再次出现实际油门.75、空管已授权而AI未接管,随后保护退出;没有新采购。单次人工观察通过不代表持续自动化稳定。0.5.12已包含上述有效期修正,728次原生自测、原ABI/导出及握手通过,已安装,实机复测仍在进行。
24 36
@@ -38,6 +50,8 @@ CG流程现在明确指定 `keepHandoffScene=true`:交接超时仍立即停止
38 50
39 51 `automatic-trading/runner.lock` 阻止重复控制,`state.json` 记录实际阶段。写入 `automatic-trading/stop` 可取消当前工作流;已接受的金钱请求仍按原操作键完成回执核对。中断后复用原工作目录和旅程检查点,未知订单只能核对,不能重放。原始卖出和已确认费用全部入账后才接受新收益。
40 52
53 `observe-health-service <PID> <新报告>` 是独立C#保护服务,不创建计划任务。`active-ship-protection.json` 与报告的 `.ready.json` 提供已绑定进程及真实采样凭据;同目录服务锁阻止重复启动。它自行持有日志与原生血量监测,导航结束不关闭它。显式用户停止时写该报告的 `.stop`;游戏实例退出时服务也结束。服务日志直接写入自己的文件,交易进程退出不会关闭它的输出管道。
54
41 55 用户需要保留现场做视觉检查时,先暂停正式流程和其自动退出观察器,再使用 `prepare-cg-visual <PID> <本机工作目录>` 只读核对游戏、会话、主船及空舱并绑定准备记录。只有用户明确“开始”后才执行 `run-cg-visual <PID> <同一目录>`;该单次空载航程不采购、出售、维护或报名,异常时停止本程序输入并保留游戏。该模式覆盖交接和真实受损均不自动退出,生产默认保护保持启用。用户明确恢复后才能再次启动正式流程。
42 56
43 57 `cg-cargo-journey <PID> <旅程目录>` 单独执行或恢复一批 Corsair/5、192t 容量、Duamta/Polyakov→AD Leonis/Homer 的 CG 运输;直接调用时默认 Uranium 192t,已有 `plan.json` 决定恢复的商品、数量和限价。首次采购前要求本机原1t已完成回执及CG成员证明。执行前仍读原始价格和库存。每站维护与交易均完成才离站,保留实际重购资金;提高后的CG报价不得再乘8。
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),("DockingZeroUpdate",255),("ShipHealth",255),("LocalTargets",255),("LocalEntityDirectory",255),("LocalSelection",255),("LocalDockingAvailability",31),("LocalPlayerIdentity",255),
38 ("Observer",7),("CommerceDocked",127),("LocalDockingHandoff",16777215),("DockingZeroUpdate",255),("ShipHealth",255),("HealthFlow",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.HealthFlow.cs +39 -0
@@ -0,0 +1,39 @@
1 namespace XFEEDToolBox.Plugin;
2
3 internal static unsafe partial class Navigation
4 {
5 private static volatile bool healthFlowActive;
6 private static bool HealthFlowNames(bool currentGame,bool currentPause,bool nextGame,bool nextPause)
7 =>(currentGame||currentPause)&&(nextGame||nextPause);
8 private static bool HealthFlowState(nuint context)
9 {
10 try
11 {
12 var flow=Pointer(context+0x30);if(Pointer(Pointer(flow)+0x20)!=image+FlowAction)return false;
13 var csm=Pointer(flow+0x310);var current=Pointer(Pointer(csm+0x100));var next=Pointer(Pointer(csm+0x108));
14 return HealthFlowNames(Bytes(current,"InGame\0"u8),Bytes(current,"Pause\0"u8),Bytes(next,"InGame\0"u8),Bytes(next,"Pause\0"u8));
15 }
16 catch{return false;}
17 }
18 private static bool HealthFrameReady(bool active,in Snapshot health,ulong now)
19 =>active&&health.healthValid&&health.entity!=0&&health.life!=0&&health.inputRoot!=0&&health.sequence>0
20 &&now>=health.tick&&now-health.tick<=300;
21 private static uint HealthFlowSelfTest()
22 {
23 uint passed=0;
24 if(HealthFlowNames(true,false,true,false))passed|=1;
25 if(HealthFlowNames(false,true,false,true))passed|=2;
26 if(HealthFlowNames(true,false,false,true)&&HealthFlowNames(false,true,true,false))passed|=4;
27 if(!HealthFlowNames(false,false,true,false)&&!HealthFlowNames(true,false,false,false))passed|=8;
28 var h=new Snapshot{healthValid=true,entity=7,life=0x10000,inputRoot=0x20000,sequence=1,tick=100};
29 if(HealthFrameReady(true,h,400)&&!HealthFrameReady(true,h,401))passed|=16;
30 if(!HealthFrameReady(false,h,150)&&!HealthFrameReady(true,h,99))passed|=32;
31 var bad=h;bad.entity=0;if(!HealthFrameReady(true,bad,150)){bad=h;bad.healthValid=false;if(!HealthFrameReady(true,bad,150))passed|=64;}
32 bad=h;bad.inputRoot=0;if(!HealthFrameReady(true,bad,150)){bad=h;bad.life=0;if(!HealthFrameReady(true,bad,150))passed|=128;}
33 return passed;
34 }
35 #if PLUGIN_NAVIGATION
36 [UnmanagedCallersOnly(EntryPoint="XFENavigationHealthFlowSelfTest")]
37 internal static uint ExportHealthFlowSelfTest()=>RunTest(HealthFlowSelfTest);
38 #endif
39 }
Modified native/ManagedPlugin/Navigation.Observation.cs +4 -2
@@ -81,7 +81,7 @@ internal static unsafe partial class Navigation
81 81 [UnmanagedCallersOnly]
82 82 static void ObserveFlow(nuint unused,nuint context)
83 83 {
84 try{if(!Enabled())return;var thread=GetCurrentThreadId();var expected=owningThread;if(expected!=0&&expected!=thread)return;owningThread=thread;ObserveSessionFlow(context);var state=FlowState(context);inGame=state;if(!state){Expire();fsdOwned=false;}}catch{}
84 try{if(!Enabled())return;var thread=GetCurrentThreadId();var expected=owningThread;if(expected!=0&&expected!=thread)return;owningThread=thread;ObserveSessionFlow(context);var state=FlowState(context);healthFlowActive=HealthFlowState(context);inGame=state;if(!state){Expire();fsdOwned=false;}}catch{}
85 85 }
86 86 static bool Sample(nuint physics,out Snapshot result)
87 87 {
@@ -111,7 +111,9 @@ internal static unsafe partial class Navigation
111 111 }
112 112 static void ObservePhysics(nuint physics)
113 113 {
114 if(!Enabled()||!inGame||GetCurrentThreadId()!=owningThread)return;var now=GetTickCount64();if(now-sampleAt<20)return;ObserveIndependentHealth(physics);
114 if(!Enabled()||GetCurrentThreadId()!=owningThread)return;
115 if(healthFlowActive)ObserveIndependentHealth(physics);
116 if(!inGame)return;var now=GetTickCount64();if(now-sampleAt<20)return;
115 117 if(!Sample(physics,out var next)){bool lost;lock(snapshotLock){lost=snapshot.physics==physics;if(lost)snapshot.valid=false;}if(lost)Expire();return;}
116 118 sampleAt=now;var previous=Current();var same=previous.valid&&previous.entity==next.entity&&previous.life==next.life;
117 119 next.healthDamageObserved=same&&previous.healthValid&&next.healthValid&&(next.hullFraction+.000001<previous.hullFraction||previous.shieldKnown&&next.shieldKnown&&next.shieldFraction+.000001<previous.shieldFraction);
Modified native/ManagedPlugin/Navigation.Protocol.cs +1 -1
@@ -61,7 +61,7 @@ internal static unsafe partial class Navigation
61 61 if(sessionMenuUntil>GetTickCount64()&&op is "apply" or "local.apply" or "local.action" or "local.select" or "route.plan" or "route.clear" or "commerce.trade" or "commerce.trade.batch" or "cg.join" or "station.services" or "calibrate.axes" or "calibrate.fsd" or "fsd")throw new InvalidOperationException("An explicit session workflow owns the game; stop it before automatic flight or commerce");
62 62 if(op=="fsd.describe")return FsdDescription();if(op=="geometry.describe")return GeometryDescription();var current=Current();var now=GetTickCount64();
63 63 if(op=="health.read"){
64 var h=CurrentHealth();if(!inGame||!h.healthValid||now<h.tick||now-h.tick>300)throw new InvalidOperationException("Native main-ship health unavailable or stale");
64 var h=CurrentHealth();if(!HealthFrameReady(healthFlowActive,h,now))throw new InvalidOperationException("Native main-ship health unavailable or stale");
65 65 return Obj(("sequence",h.sequence),("observedAtUnixMilliseconds",h.utcMilliseconds),("entityId",h.entity.ToString()),("localPlayerShipVerified",true),("shipHealth",ShipHealth(h)));
66 66 }
67 67 if(op=="read"){
Modified native/ManagedPlugin/Platform.cs +1 -1
Modified research/NativeAcceptance/CgCargoJourney.cs +11 -15
Modified research/NativeAcceptance/CgTradeLoop.cs +12 -7
Modified research/NativeAcceptance/FlightTestProtectionScope.cs +3 -1
Modified research/NativeAcceptance/ManualVisualHandoff.cs +1 -1
Added research/NativeAcceptance/PersistentProtectionProcess.cs +75 -0
Modified research/NativeAcceptance/Program.cs +28 -1
Added research/NativeAcceptance/RebuyReconciliation.cs +51 -0
Modified research/NativeAcceptance/ShipHealthObservation.cs +26 -5