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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

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

XFEstudio/XFEEDToolBox

Fix persistent throttle reset before native auto docking

Connect production local navigation to the pinned C# docking extension, clear the original retained Thrust accumulator, verify real zero output and invoke the eligible original docking activity. Preserve expiring intents, identity checks, original receipts and the two-second takeover bound. Add owned lowest-priority input-context lifetime handling, fixed empty-ship .75 reproduction and safe typed token release. Bundle the extension through the existing build pipeline. Validate two real .75-to-zero/AI/docked/service rounds at Homer, including the production configuration; 48 native fixture calls, 20 docking regressions and a clean WinUI Release build. Polyakov continuous-route verification remains separate and unclaimed.

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

代码差异

19 个文件 +1083 -29
Modified .gitignore +2 -0
@@ -31,3 +31,5 @@ XFEEDToolBox/PluginPackage/interaction.json
31 31 .env
32 32 .env.*
33 33 !.env.example
34
35 XFEEDToolBox/PluginPackage/DockingControl/
Modified XFEEDToolBox.Core/Execution/NativeTradeNavigation.cs +8 -3
@@ -12,7 +12,12 @@ public sealed class NativeTradeNavigation(int processId,string gameHash,Func<Pla
12 12 private readonly SemaphoreSlim gate=new(1,1);
13 13 public event Action<string>? Status;
14 14 private NativeGameInputChannel Inputs()=>new(processId,gameHash,pluginDirectory);
15 private NativeSystemFlightControlDriver Local()=>new(processId,gameHash,player,Inputs(),ensureProtection,useNativeSupercruiseAssist:false);
15 private async Task<NativeSystemFlightControlDriver> LocalAsync(CancellationToken token)
16 {
17 var driver=new NativeSystemFlightControlDriver(processId,gameHash,player,Inputs(),ensureProtection,useNativeSupercruiseAssist:false);
18 await driver.ConfigureOriginalDockingControlAsync(Path.Combine(pluginDirectory,"DockingControl"),token);
19 return driver;
20 }
16 21 private async Task<(CurrentSystemSnapshot Catalogue,SystemLocation Location)> ResolveAsync(Station destination,CancellationToken token)
17 22 {
18 23 using var source=new NativeCurrentSystemSource(processId,gameHash,player);
@@ -39,7 +44,7 @@ public sealed class NativeTradeNavigation(int processId,string gameHash,Func<Pla
39 44 var catalogue=await NavigationRecovery.ReadWithRecoveryAsync(source.ReadLocationsAsync,null,token);
40 45 var alternate=catalogue.Locations.FirstOrDefault(value=>value.Category==SystemLocationCategory.Station&&value.MarketId is >0&&value.MarketId!=initial.MarketId)
41 46 ??throw new InvalidOperationException("当前目录尚未提供可确认的离场目标。");
42 var departure=Local();
47 var departure=await LocalAsync(token);
43 48 try
44 49 {
45 50 Status?.Invoke("正在自动离场");await departure.SelectAsync(Target(catalogue.SystemAddress,alternate),token);await departure.PrepareAsync(token);
@@ -65,7 +70,7 @@ public sealed class NativeTradeNavigation(int processId,string gameHash,Func<Pla
65 70 await beforeInput(token);await ensureProtection(token);
66 71 var resolved=await ResolveAsync(destination,token);
67 72 var target=Target(resolved.Catalogue.SystemAddress,resolved.Location);
68 var local=Local();var arrival=new SystemNavigationRunner(player);string? lastIssue=null;
73 var local=await LocalAsync(token);var arrival=new SystemNavigationRunner(player);string? lastIssue=null;
69 74 arrival.Progress+=value=>{lastIssue=value.Message;Status?.Invoke(value.Message);};
70 75 await arrival.RunAsync(target,local,token);
71 76 if(arrival.State!=NavigationRunState.Completed||!player().Docked||player().MarketId!=destination.MarketId)
Modified XFEEDToolBox.Core/Navigation/NativeFlightControlDriver.cs +1 -1
@@ -9,7 +9,7 @@ namespace XFEEDToolBox.Core.Navigation;
9 9 public sealed class NavigationPipeClient(int processId,string service="Navigation")
10 10 {
11 11 public int ProcessId { get; }=processId>0?processId:throw new ArgumentOutOfRangeException(nameof(processId));
12 private readonly string serviceName=service is "Navigation" or "Interaction" or "SessionRecovery0514" or "SessionRecovery0514c" or "SessionRecovery0514d" or "SessionRecovery0514e" or "SessionRecovery0514f" or "SessionRecovery0514g" or "CruiseObservation0514a" or "CruiseObservation0514b" or "CruiseObservation0514c" or "CruiseControl0514a" or "CruiseControl0514b" or "CruiseControl0514c"?service:throw new ArgumentException("Unknown game service.",nameof(service));
12 private readonly string serviceName=service is "Navigation" or "Interaction" or "SessionRecovery0514" or "SessionRecovery0514c" or "SessionRecovery0514d" or "SessionRecovery0514e" or "SessionRecovery0514f" or "SessionRecovery0514g" or "SessionRecovery0514h" or "SessionRecovery0514i" or "SessionRecovery0514j" or "SessionRecovery0514k" or "CruiseObservation0514a" or "CruiseObservation0514b" or "CruiseObservation0514c" or "CruiseControl0514a" or "CruiseControl0514b" or "CruiseControl0514c"?service:throw new ArgumentException("Unknown game service.",nameof(service));
13 13 private readonly SemaphoreSlim gate=new(1,1);
14 14 public async Task<JsonElement> CallAsync(object request,CancellationToken token=default)
15 15 {
Modified XFEEDToolBox.Core/Navigation/NativeSystemFlightControlDriver.DockingReset.cs +24 -6
@@ -1,4 +1,5 @@
1 1 using System.Text.Json;
2 using XFEEDToolBox.Core.Plugins;
2 3 namespace XFEEDToolBox.Core.Navigation;
3 4
4 5 public sealed partial class NativeSystemFlightControlDriver
@@ -7,14 +8,30 @@ public sealed partial class NativeSystemFlightControlDriver
7 8 private string? dockingResetRequestId;
8 9 private DateTimeOffset dockingResetDeadline;
9 10 private bool requestOriginalDockingStart;
11 private bool emptyDockingDiagnostic=true;
10 12 public JsonElement? LastOriginalDockingResetReceipt{get;private set;}
11 13
12 /// <summary>Explicit empty-ship diagnostic only; normal production retains its existing driver.</summary>
14 /// <summary>Explicit empty-ship validation retains its narrower ship/cargo limits.</summary>
13 15 public Task EnableOriginalDockingResetTestAsync(CancellationToken token=default)=>EnableOriginalDockingResetTestAsync(false,token);
14 16 public async Task EnableOriginalDockingResetTestAsync(bool startDockingAfterZero,CancellationToken token=default)
17 =>await ConfigureOriginalDockingAsync(startDockingAfterZero,true,token);
18 public async Task ConfigureOriginalDockingControlAsync(string packageDirectory,CancellationToken token=default)
19 {
20 var native=new NavigationPipeClient(processId,OriginalDockingService);
21 try{await native.CallAsync(new{op="describe"},token);}
22 catch(Exception error)when(!token.IsCancellationRequested&&error is IOException or OperationCanceledException)
23 {
24 var main=await pipe.CallAsync(new{op="describe"},token);
25 if(main.GetProperty("processId").GetInt32()!=processId||main.GetProperty("gameSha256").GetString()!=gameHash)
26 throw new InvalidDataException("Original docking package belongs to another game.");
27 await RuntimeInteractionModule.LoadAsync(processId,gameHash,main.GetProperty("owningThread").GetInt32(),packageDirectory,token);
28 }
29 await ConfigureOriginalDockingAsync(true,false,token);
30 }
31 private async Task ConfigureOriginalDockingAsync(bool startDockingAfterZero,bool diagnostic,CancellationToken token)
15 32 {
16 33 if(dockingResetRequestId is not null)throw new InvalidOperationException("An original docking test request is already immutable.");
17 const string service="SessionRecovery0514g";
34 const string service=OriginalDockingService;
18 35 var candidate=new NavigationPipeClient(processId,service);var hello=await candidate.CallAsync(new{op="describe"},token);
19 36 if(hello.GetProperty("processId").GetInt32()!=processId||hello.GetProperty("gameSha256").GetString()!=gameHash
20 37 ||hello.GetProperty("service").GetString()!=service||!Flag(hello,"nativeDockingReset")||!Flag(hello,"enabled")
@@ -22,7 +39,7 @@ public sealed partial class NativeSystemFlightControlDriver
22 39 ||DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()-hello.GetProperty("observedAtUnixMilliseconds").GetInt64() is <0 or >200
23 40 ||startDockingAfterZero&&!Flag(hello,"nativeDockingStart"))
24 41 throw new NotSupportedException("Verified original docking reset test extension is not loaded.");
25 dockingResetPipe=candidate;requestOriginalDockingStart=startDockingAfterZero;
42 dockingResetPipe=candidate;requestOriginalDockingStart=startDockingAfterZero;emptyDockingDiagnostic=diagnostic;
26 43 }
27 44 private void AdoptOriginalResetReceipt(JsonElement value)
28 45 {
@@ -33,10 +50,11 @@ public sealed partial class NativeSystemFlightControlDriver
33 50 }
34 51 private async Task OriginalDockingResetAsync(SystemFlightFrame frame,DateTimeOffset deadline,CancellationToken token)
35 52 {
36 var service=dockingResetPipe??throw new InvalidOperationException("Original reset diagnostic is not enabled.");
53 var service=dockingResetPipe??throw new InvalidOperationException("Original docking reset is not configured.");
37 54 if(frame.Mode!=LocalFlightMode.Normal||frame.PlayerOverride||frame.ExternalThreat||selected is null||frame.Clearance?.Allows(selected,DateTimeOffset.UtcNow)!=true
38 ||frame.TargetDistance>7500||player().CargoCount!=0||player().CurrentShipId!=5||player().CargoCapacity!=192)
39 throw new InvalidOperationException("Original reset diagnostic requires the healthy empty selected ship and current original docking clearance.");
55 ||frame.TargetDistance>7500||player().CurrentShipId is null
56 ||emptyDockingDiagnostic&&(player().CargoCount!=0||player().CurrentShipId!=5||player().CargoCapacity!=192))
57 throw new InvalidOperationException("Original reset requires the same selected main ship, current ATC and the configured validation scope.");
40 58 if(dockingResetRequestId is not null)
41 59 {
42 60 if(LastOriginalDockingResetReceipt is {} prior&&Flag(prior,"applied")
Added XFEEDToolBox.Core/Navigation/NativeSystemFlightControlDriver.FlightContext.cs +34 -0
@@ -0,0 +1,34 @@
1 using System.Text.Json;
2 namespace XFEEDToolBox.Core.Navigation;
3
4 public sealed partial class NativeSystemFlightControlDriver
5 {
6 public const string OriginalDockingService="SessionRecovery0514k";
7 public async Task<JsonElement> ArmOriginalPersistentThrottleTrialAsync(string trialId,SystemFlightFrame frame,CancellationToken token)
8 {
9 if(frame.Mode!=LocalFlightMode.Normal||frame.PlayerOverride||frame.ExternalThreat||selected is null
10 ||frame.Clearance?.Allows(selected,DateTimeOffset.UtcNow)!=true||frame.TargetDistance is <4500 or >7200||frame.Speed>15
11 ||player().CurrentShipId!=5||player().CargoCount!=0||player().CargoCapacity!=192)
12 throw new InvalidOperationException("Fixed .75 trial requires the empty selected main ship, low speed and original ATC grant.");
13 await pipe.CallAsync(new{op="local.axes.release",owner,generation},token);
14 return await OriginalFlightTrialAsync("flight.context.arm",trialId,frame,token);
15 }
16 private async Task<JsonElement> OriginalFlightTrialAsync(string op,string trialId,SystemFlightFrame? frame,CancellationToken token,bool preserve=false)
17 {
18 if(trialId.Length!=32||trialId.Any(c=>c is not(>='0' and <='9') and not(>='a' and <='f')))throw new ArgumentException("One immutable original trial ID required.");
19 var native=new NavigationPipeClient(processId,OriginalDockingService);
20 if(frame is null)return await native.CallAsync(new{op,requestId=trialId,owner,preserveOwnedSeedForReset=preserve},token);
21 var deadline=frame.ObservedAt.AddMilliseconds(250);var remaining=(int)Math.Floor((deadline-DateTimeOffset.UtcNow).TotalMilliseconds);
22 if(remaining is <1 or >250)throw new FlightTelemetryUnavailableException("Original Flight trial frame has expired.");
23 return await native.CallAsync(new{op,requestId=trialId,owner,purpose="after-grant-persistent075",selectionGeneration=generation,
24 sequence=frame.Sequence,ttlMilliseconds=remaining,expiresAtUnixMilliseconds=deadline.ToUnixTimeMilliseconds()},token);
25 }
26 public Task<JsonElement> ReadOriginalPersistentThrottleTrialAsync(string id,CancellationToken token)
27 =>OriginalFlightTrialAsync("flight.context.status",id,null,token);
28 public Task<JsonElement> RenewOriginalPersistentThrottleTrialAsync(string id,SystemFlightFrame frame,CancellationToken token)
29 =>OriginalFlightTrialAsync("flight.context.renew",id,frame,token);
30 public Task<JsonElement> SeedOriginalPersistentThrottleTrialAsync(string id,CancellationToken token)
31 =>OriginalFlightTrialAsync("flight.context.seed",id,null,token);
32 public Task<JsonElement> ReleaseOriginalPersistentThrottleTrialAsync(string id,bool preserveForReset,CancellationToken token)
33 =>OriginalFlightTrialAsync("flight.context.release",id,null,token,preserveForReset);
34 }
Added docs/DOCKING-THROTTLE-FIX.md +15 -0
@@ -0,0 +1,15 @@
1 # 原始油门归零与自动降落
2
3 停靠前单独清除控制器油门不能清除原始 `Thrust` 节点保留的积分。该节点下一次更新会再次发布非零油门,导致自动降落没有获得真实零油门条件。
4
5 正式导航现在明确接入独立 C# / NativeAOT 停靠扩展。取得匹配主船和目标站的原空管授权后,程序核对原输入来源及订阅,调用原注册 Reset 回调 `8F5320`,等待原 mapper 发布零值,再通过原 `EE9F30` 和原空管通知发起自动降落。实际输入、控制器、原来源及积分均需确认归零,保留两秒内 AI 接管和独立受损退出保护。
6
7 扩展使用既有 ABI,通过 `PluginPackage/DockingControl` 的独立清单部署。原游戏 PID、构建、线程、目标和请求期限均需匹配。未知回执只按原请求 ID 核对,不重新提交原操作。
8
9 固定 `.75` 复现仅供空载验收:使用原 `Bindings_Flight` 最低优先级上下文和一个原 absolute 输入帧;撤回该输入后,至少三个不同原帧必须同时观测到积分、来源和实际油门为 `.75`。上下文通过原 `77A6A0` 释放,注册表析构前处理自有 token,不直接修改 Active、引用计数、积分或 AI。原 `25E9360` 的 `-100` 是控制器 override 已释放的标记,不能当作最终实际归零证据。
10
11 先取得空管许可再复现,避免保持非零油门等待服务器。原 profile 启用后可能发布有限的 `.05` 基线;absolute `.75` 复现接受这一有效基线,保留原速度、几何、空管、期限及最终零值门槛。
12
13 Homer 已完成两次同轮 `.75` 持续、原 Reset、原 AI、实际停靠和维护,其中第二次使用正式导航相同的配置入口。测试要求三个不同原帧均为 `.75`,原输入命令已撤回,归零后实际输入、控制器、来源和积分均为零。Polyakov 连续航程尚未复测,SCO、拦截和后续跑商不属于本次通过的验收项目。
14
15 构建使用 `scripts/Build-DockingControl.ps1`;完整插件构建会包含该步骤。独立交付快照的 WinUI Release 构建为 0 警告、0 错误,相关 20 项 Docking 回归通过。验收工具命令为 `accept-flight-context-admission` 和 `accept-persistent-docking-throttle --production-reset`,不创建定时任务或模拟键鼠。
Added native/ManagedPlugin/Navigation.RecoveryFlightContext.cs +175 -0
@@ -0,0 +1,175 @@
1 #if PLUGIN_SESSION_RECOVERY
2 using System.Text;
3 namespace XFEEDToolBox.Plugin;
4
5 internal static unsafe partial class Navigation
6 {
7 // The returned token is an original intrusive-ref object, NOT a binding
8 // wrapper. Its release ABI is 77A6A0(slot), never 204010(bindingSlot).
9 internal sealed class RecoveryFlightContextBinding
10 {
11 internal RecoveryResetBinding ship=null!;
12 internal nuint entry,active;
13 internal bool reproduction;
14 }
15 internal sealed record RecoveryFlightContextLayers(nuint entry,nuint data,ulong count,nuint vm,byte active,
16 ulong[] occupancy,bool ownMember,ulong foreignCount)
17 {
18 internal bool Empty=>occupancy.All(value=>value==0);
19 internal bool OnlyOwn=>ownMember&&occupancy.Length>0&&occupancy[0]==1&&foreignCount==0;
20 }
21 private static bool FlightContextHeap(nuint value)=>value>=0x10000&&value<0x7fffffff0000&&(value&7)==0&&!InImage(value);
22 internal static bool FlightContextNoSeedThrottle(float[] values)=>values.Length==4
23 &&values.Take(3).All(value=>float.IsFinite(value)&&Math.Abs(value)<=.001f)
24 // Original 25E9360 clears the optional controller override to -100.
25 // This is kept as a raw released marker, never reported as throttle 0.
26 &&float.IsFinite(values[3])&&(Math.Abs(values[3])<=.001f||values[3]==-100f);
27 private static bool FlightContextName(nuint value)
28 {
29 if(!FlightContextHeap(value)||Read<ulong>(value)!=15)return false;
30 var name=Platform.ReadBytes(value+0x14,15);
31 return Encoding.ASCII.GetString(name).Equals("Bindings_Flight",StringComparison.OrdinalIgnoreCase);
32 }
33 private static nuint FlightContextEntry(nuint registry,nuint expected=0)
34 {
35 if(!FlightContextHeap(registry)||Pointer(registry)!=image+0x512FD90||Pointer(Pointer(registry)+0x20)!=image+0x8FA080
36 ||Pointer(Pointer(registry)+0x10)!=image+0x8ED50C)throw new InvalidOperationException("Original Flight registry ABI differs");
37 var buckets=Pointer(registry+0x78);var count=Read<ulong>(registry+0x80);
38 if(!FlightContextHeap(buckets)||count is 0 or >4096)throw new InvalidOperationException("Original Flight registry map bound differs");
39 var seen=new HashSet<nuint>();nuint match=0;
40 for(nuint i=0;i<(nuint)count;i++)
41 {
42 var head=buckets+i*8;var item=Pointer(head);
43 while(item<buckets||item>=buckets+(nuint)count*8)
44 {
45 if(!FlightContextHeap(item)||!seen.Add(item)||seen.Count>4096)throw new InvalidOperationException("Original Flight registry chain differs");
46 if(FlightContextName(Pointer(item+8))){if(match!=0)throw new InvalidOperationException("Original Flight profile is ambiguous");match=item;}
47 item=Pointer(item);
48 }
49 }
50 if(match==0||expected!=0&&match!=expected||Pointer(registry+0x78)!=buckets||Read<ulong>(registry+0x80)!=count)
51 throw new InvalidOperationException("Original Flight registry changed");
52 return match;
53 }
54 internal static RecoveryFlightContextBinding BindRecoveryFlightContext(ulong entity,ulong world,JsonElement target,double hull,double? shield,nuint node,bool reproduction)
55 {
56 var ship=BindRecoveryReset(entity,world,target,hull,shield,node);
57 var active=Pointer(node+0x90);
58 if(!FixtureBindingName(active,"Bindings_Flight_Active")||Pointer(active+0x10)!=ship.registry+0xB8)
59 throw new InvalidOperationException("Original current-source Flight Active binding differs");
60 return new(){ship=ship,entry=FlightContextEntry(ship.registry),active=active,reproduction=reproduction};
61 }
62 internal static RecoveryFlightContextLayers ReadRecoveryFlightContextLayers(RecoveryFlightContextBinding proof,nuint token)
63 {
64 var ship=proof.ship;var registry=ship.registry;var entry=FlightContextEntry(registry,proof.entry);
65 var header=Platform.ReadBytes(entry+0x10,0x30);var count=Read<ulong>(entry+0x28);var data=Pointer(entry+0x30);var vm=Pointer(entry+0x38);
66 if(count>256||count>0&&!FlightContextHeap(data)||vm!=0&&!FlightContextHeap(vm))throw new InvalidOperationException("Original Flight layer array exceeds its bound");
67 var raw=count==0?[]:Platform.ReadBytes(data,checked((int)count*32));var occupancy=new ulong[count];var member=false;ulong foreign=0;
68 for(nuint i=0;i<(nuint)count;i++)
69 {
70 var set=data+i*32;var used=Read<ulong>(set+0x18);occupancy[i]=used;
71 if(used>4096)throw new InvalidOperationException("Original Flight layer occupancy exceeds its bound");
72 if(used==0)continue;
73 // 9091E0 inserts the exact raw token into this original hash set.
74 var buckets=Pointer(set);var capacity=Read<ulong>(set+8);
75 if(!FlightContextHeap(buckets)||capacity is 0 or >8192)throw new InvalidOperationException("Original Flight token set bound differs");
76 var seen=new HashSet<nuint>();ulong matches=0;
77 for(nuint b=0;b<(nuint)capacity;b++)
78 {
79 var head=buckets+b*8;var item=Pointer(head);
80 while(item!=head)
81 {
82 if(!FlightContextHeap(item)||!seen.Add(item)||seen.Count>4096)throw new InvalidOperationException("Original Flight token set chain differs");
83 var value=Pointer(item+8);if(token!=0&&i==0&&value==token)matches++;else foreign++;
84 item=Pointer(item);
85 }
86 }
87 if((ulong)seen.Count!=used||matches>1||Pointer(set)!=buckets||Read<ulong>(set+8)!=capacity||Read<ulong>(set+0x18)!=used)
88 throw new InvalidOperationException("Original Flight token membership changed");
89 member|=matches==1;
90 }
91 var active=Read<byte>(proof.active+0x24);
92 if(Pointer(proof.ship.node+0x90)!=proof.active||!FixtureBindingName(proof.active,"Bindings_Flight_Active")||Pointer(proof.active+0x10)!=registry+0xB8
93 ||!header.AsSpan().SequenceEqual(Platform.ReadBytes(entry+0x10,header.Length))
94 ||raw.Length!=0&&!raw.AsSpan().SequenceEqual(Platform.ReadBytes(data,raw.Length))||active!=Read<byte>(proof.active+0x24))
95 throw new InvalidOperationException("Original Flight layer metadata changed during verification");
96 return new(entry,data,count,vm,active,occupancy,member,foreign);
97 }
98 internal static void CheckRecoveryFlightContextFrame(RecoveryFlightContextBinding proof,bool initial,bool seed=false)
99 {
100 var baseline=proof.ship.baseline;var current=RecoveryPlayer(baseline.entity);
101 if(current.life!=baseline.life||current.fsd!=baseline.fsd||current.physics!=baseline.physics||current.inputRoot!=baseline.inputRoot
102 ||!Sample(current.physics,out var frame)||frame.entity!=baseline.entity||frame.life!=baseline.life||frame.referenceWorld!=baseline.referenceWorld
103 ||!frame.healthValid||frame.hullFraction<.95||HealthDamageEmergency(baseline,frame)||frame.rawFsdState is not(1 or 2)||!BoundFsd(frame,frame.fsd))
104 throw new InvalidOperationException("Flight scope current main ship, normal space or health changed");
105 var ship=proof.ship;var controller=Pointer(frame.inputRoot+0x2F8);var inventory=ReadCommerceShipInventory(frame);
106 if(!inventory.verified||inventory.count!=0||inventory.capacity!=192||LocalDockingComputer(frame)!=ship.computer||Pointer(ship.computer+0x240)!=controller
107 ||Pointer(frame.fsd+0x6F8)!=ship.registry||Pointer(ship.computer+0x230)!=ship.registry||Pointer(ship.node)!=image+0x512E330
108 ||Pointer(ship.node+0x40)!=ship.registry||Pointer(ship.node+0x78)!=ship.source||Pointer(ship.computer+0x260)!=ship.source||!RecoverySubscriber(ship))
109 throw new InvalidOperationException("Flight scope original empty main ship input ownership changed");
110 if(!SampleLocalFlight(frame,out var local,out var stage))throw new InvalidOperationException("Flight scope original local telemetry unavailable: "+stage);
111 var target=ship.target;
112 if(!local.selected||local.target.key!=target.GetProperty("key").GetInt32()||local.target.body.ToString()!=target.GetProperty("bodyAddress").GetString()
113 ||local.target.kind!=target.GetProperty("kind").GetUInt32()||local.target.market!=target.GetProperty("marketId").GetUInt32()
114 ||LocalIdentityName(local.target)!=target.GetProperty("name").GetString()||local.target.market==0||local.inSupercruise
115 ||local.nativeDockingActive||local.nativeLaunchActive||local.nativeLaunchRequested||local.cruiseAssistEnabled||local.cruiseAssistEngaged
116 ||!double.IsFinite(local.distance)||local.distance>7500||!double.IsFinite(local.speed)||local.speed>(initial||seed?15:100))
117 throw new InvalidOperationException("Flight scope original target, duty, SCA or bounded geometry changed");
118 if(proof.reproduction)
119 {
120 var outward=Subtract(local.ship.position,local.destination.position);var length=Math.Sqrt(Dot(outward,outward));
121 if(local.distance<4500||local.distance>7200||length<=0||Dot(outward,local.ship.forward)/length<.98||!local.dockingVerified||!local.dockingGranted)
122 throw new InvalidOperationException("Flight reproduction requires original ATC and must face away from its original station");
123 }
124 else if(local.dockingState!=5||local.speed>.1)throw new InvalidOperationException("No-seed Flight admission probe must remain actually docked");
125 var values=new[]{Read<float>(ship.node+0xD0),Read<float>(ship.source+0x18),Read<float>(frame.inputRoot+0x70),Read<float>(controller+0x680)};
126 if(!proof.reproduction)
127 {if(!FlightContextNoSeedThrottle(values))throw new InvalidOperationException("No-seed Flight scope requires zero actual/source/accumulator and zero or originally released override");}
128 else if(values.Take(3).Any(value=>!float.IsFinite(value)||Math.Abs(value)>1.001f)
129 ||!float.IsFinite(values[3])||Math.Abs(values[3])>1.001f&&values[3]!=-100f
130 ||initial&&values.Take(3).Any(value=>Math.Abs(value)>.001f))throw new InvalidOperationException("Flight scope original throttle is not eligible");
131 }
132 internal static bool RecoveryFlightContextTokenValid(RecoveryFlightContextBinding proof,nuint token)
133 =>FlightContextHeap(token)&&Pointer(token)==image+0x512CF40&&Read<int>(token+8)>0&&Read<int>(token+8)<=32
134 &&Pointer(token+0x10)==proof.ship.registry-0x58&&Pointer(token+0x18)==proof.entry+0x10&&Read<ulong>(token+0x20)==0;
135 internal static float[] ReadRecoveryFlightContextThrottle(RecoveryFlightContextBinding proof)
136 {
137 var ship=proof.ship;var controller=Pointer(ship.baseline.inputRoot+0x2F8);
138 if(Pointer(ship.node+0x78)!=ship.source||Pointer(ship.computer+0x240)!=controller||LocalPlayerIdentity.ReadLife(ship.baseline.inputRoot,image,imageEnd)!=ship.baseline.life)
139 throw new InvalidOperationException("Original Flight scope throttle identity changed");
140 var values=new[]{Read<float>(ship.node+0xD0),Read<float>(ship.source+0x18),Read<float>(ship.baseline.inputRoot+0x70),Read<float>(controller+0x680)};
141 if(values.Any(value=>!float.IsFinite(value)))throw new InvalidOperationException("Original Flight scope throttle sample is non-finite");return values;
142 }
143 internal static void CreateRecoveryFlightContext(RecoveryFlightContextBinding proof,ref nuint owned)
144 {
145 if(owned!=0)throw new InvalidOperationException("Original Flight output slot is already owned");
146 CheckRecoveryFlightContextFrame(proof,true);var layers=ReadRecoveryFlightContextLayers(proof,0);
147 if(!layers.Empty||layers.active!=0||layers.vm!=0)throw new InvalidOperationException("Every original Flight layer must be empty before the lowest scope is created");
148 if(!Platform.ReadBytes(image+0x8FA080,22).AsSpan().SequenceEqual(new byte[]{0x48,0x89,0x5C,0x24,0x10,0x48,0x89,0x6C,0x24,0x18,0x48,0x89,0x74,0x24,0x20,0x57,0x41,0x56,0x41,0x57,0x48,0x83}))
149 throw new InvalidOperationException("Original Flight context constructor signature differs");
150 var name=Encoding.ASCII.GetBytes("Bindings_Flight\0");
151 fixed(byte* text=name)
152 fixed(nuint* slot=&owned)
153 {
154 // Win64: RCX registry, RDX address of SDK-owned slot, R8 char*,
155 // R9 priority=0. RAX is that same output-slot address.
156 var returned=Call(image+0x8FA080,(ulong)proof.ship.registry,(ulong)(nuint)slot,(ulong)(nuint)text,0);
157 if(returned!=(ulong)(nuint)slot)throw new InvalidOperationException("Original Flight constructor returned a different output slot");
158 }
159 }
160 internal static void ReleaseRecoveryFlightContext(RecoveryFlightContextBinding proof,ref nuint owned,Action dispatching)
161 {
162 if(owned==0)return;
163 if(!RecoveryFlightContextTokenValid(proof,owned)||!ReadRecoveryFlightContextLayers(proof,owned).ownMember)
164 throw new InvalidOperationException("Original Flight token lifetime or exact set membership differs; no stale destructor will be called");
165 if(!Platform.ReadBytes(image+0x77A6A0,46).AsSpan().SequenceEqual(new byte[]{0x40,0x53,0x48,0x83,0xEC,0x20,0x48,0x8B,0xD9,0x48,0x8B,0x09,0x48,0x85,0xC9,0x74,0x17,0x83,0x41,0x08,0xFF,0x75,0x0A,0x48,0x8B,0x01,0xBA,0x01,0,0,0,0xFF,0x10,0x48,0xC7,0x03,0,0,0,0,0x48,0x83,0xC4,0x20,0x5B,0xC3}))
166 throw new InvalidOperationException("Original typed Flight release signature differs");
167 fixed(nuint* slot=&owned)
168 {
169 dispatching();
170 Call(image+0x77A6A0,(ulong)(nuint)slot);
171 }
172 if(owned!=0)throw new InvalidOperationException("Original typed Flight release did not clear its owned slot");
173 }
174 }
175 #endif
Added native/ManagedPlugin/Navigation.RecoveryThrottleTrial.cs +50 -0
@@ -0,0 +1,50 @@
1 #if PLUGIN_SESSION_RECOVERY
2 namespace XFEEDToolBox.Plugin;
3
4 internal static unsafe partial class Navigation
5 {
6 internal struct RecoveryThrottlePulse
7 {
8 internal nuint node,binding,source;
9 internal float before;
10 internal bool applied;
11 }
12 internal static RecoveryThrottlePulse BeginRecoveryThrottleTrial(RecoveryFlightContextBinding proof)
13 {
14 if(!proof.reproduction)throw new InvalidOperationException("A fixed after-grant empty trial is required");
15 // This is an absolute .75 input. Original profile activation can
16 // publish its finite .05 baseline before this mapper frame; requiring
17 // it to still be zero would reject a valid reproduction. Admission
18 // itself still starts from zero, and the bounded original frame,
19 // ATC, ownership and eventual four-place zero gates are unchanged.
20 CheckRecoveryFlightContextFrame(proof,false,seed:true);
21 var node=proof.ship.node;var absolute=Pointer(node+0xB0);var source=proof.ship.source;
22 if(Read<byte>(proof.active+0x24)!=1||!FixtureBindingName(absolute,"ForwardReverseThrust_Set")
23 ||!BeginDockingFixturePulse(node,absolute,source,out var pulse))throw new InvalidOperationException("Original absolute command was not eligible");
24 return new(){node=node,binding=absolute,source=source,before=pulse.beforeAccumulator,applied=true};
25 }
26 internal static bool EndRecoveryThrottleTrial(in RecoveryThrottlePulse pulse)
27 {
28 EndDockingFixturePulse(new(){node=pulse.node,binding=pulse.binding,source=pulse.source,beforeAccumulator=pulse.before,applied=pulse.applied});
29 return pulse.applied&&Pointer(pulse.node+0xB0)==pulse.binding&&Pointer(pulse.node+0x78)==pulse.source&&Read<uint>(pulse.binding+0x18)==0;
30 }
31 internal static float RecoveryThrottleTrialCommand(RecoveryFlightContextBinding proof)=>Read<float>(Pointer(proof.ship.node+0xB0)+0x18);
32 internal static bool CleanupRecoveryThrottleTrial(RecoveryFlightContextBinding proof)
33 {
34 var ship=proof.ship;var baseline=ship.baseline;var current=RecoveryPlayer(baseline.entity);
35 if(current.life!=baseline.life||current.fsd!=baseline.fsd||current.physics!=baseline.physics||current.inputRoot!=baseline.inputRoot
36 ||!Sample(current.physics,out var frame)||frame.life!=baseline.life||frame.entity!=baseline.entity||!BoundFsd(frame,frame.fsd))return false;
37 var controller=Pointer(frame.inputRoot+0x2F8);var value=ReadRecoveryFlightContextThrottle(proof);
38 if(Pointer(ship.node)!=image+0x512E330||Pointer(ship.node+0x40)!=ship.registry||Pointer(ship.node+0x78)!=ship.source
39 ||LocalDockingComputer(frame)!=ship.computer||Pointer(ship.computer+0x240)!=controller||Pointer(ship.computer+0x260)!=ship.source
40 ||!RecoverySubscriber(ship)||!FixtureBindingName(Pointer(ship.node+0xB0),"ForwardReverseThrust_Set")
41 ||RecoveryThrottleTrialCommand(proof)!=0||Math.Abs(value[0]-.75f)>.001f||Math.Abs(value[1]-.75f)>.001f
42 ||value[3]!=-100f&&Math.Abs(value[3])>.001f||Pointer(Pointer(controller)+0x158)!=image+ThrottleSetter
43 ||!Platform.ReadBytes(image+0x8F5320,18).AsSpan().SequenceEqual(new byte[]{0xC6,0x81,0xC0,0,0,0,0,0xC7,0x81,0xB0,0,0,0,0,0,0,0,0xC3}))return false;
44 // Undo only this trial's unchanged, retained command. These original
45 // methods own the controller override and the source accumulator.
46 Call(image+ThrottleSetter,(ulong)controller,F32(0));
47 Call(image+0x8F5320,(ulong)(ship.node+0x20));return true;
48 }
49 }
50 #endif
Added native/ManagedPlugin/SessionRecovery.FlightContext.cs +290 -0
@@ -0,0 +1,290 @@
1 #if PLUGIN_SESSION_RECOVERY
2 namespace XFEEDToolBox.Plugin;
3
4 internal static unsafe partial class SessionRecoveryRuntime
5 {
6 private const nuint RegistryDestroy=0x8E9D50;
7 private static nuint originalRegistryDestroy;
8 private sealed class FlightContextWork
9 {
10 internal string id="",owner="",phase="preparing",issue="";
11 internal ulong generation,entity,deadline,absoluteEnd,bridgeAt,createdAt,activeAt,releasedAt,inactiveAt;
12 internal bool pending,createAttempted,created,activeObserved,releaseRequested,explicitRelease,releaseAttempted,releaseCalled,inactiveObserved,failed,registryDestroyed,abandoned;
13 internal nuint token;
14 internal float[]? throttle;
15 internal ulong throttleAt;
16 internal Navigation.RecoveryFlightContextBinding? proof;
17 internal Navigation.RecoveryFlightContextLayers? layers;
18 internal bool reproduction,seedRequested,seedAttempted,seedApplied,seedRestored,seedOutstanding,preserveSeed,seedCleanupCalled,seedZeroObserved;
19 internal uint unseededFrames;
20 internal ulong seedAt,lastTrialFrame,seedCleanupDeadline;
21 internal JsonObject[] trialSamples=[];
22 }
23 private static readonly object flightContextGate=new();
24 private static readonly Dictionary<string,FlightContextWork> flightContextReceipts=new(StringComparer.Ordinal);
25 private static FlightContextWork? flightContext;
26 private static bool stationaryContextQualified;
27 private readonly record struct FlightContextGate(ulong now,ulong deadline,ulong absoluteEnd,ulong bridgeAt,
28 bool inGame,bool owner,bool sameShip,bool health,bool empty,bool ownLayer,bool overridePresent,bool sceneChange);
29 private static bool FlightContextAllowed(in FlightContextGate value)
30 =>value.inGame&&value.owner&&value.sameShip&&value.health&&value.empty&&value.ownLayer&&!value.overridePresent&&!value.sceneChange
31 &&value.now<value.deadline&&value.deadline-value.now<=250&&value.now<value.absoluteEnd
32 &&value.bridgeAt<=value.now&&value.now-value.bridgeAt<=200;
33 private static JsonObject FlightContextDescription(FlightContextWork? selected=null)
34 {
35 lock(flightContextGate)
36 {
37 var value=selected??flightContext;var layers=value?.layers;
38 return Obj(("requestId",value?.id),("owner",value?.owner),("selectionGeneration",value?.generation??0),
39 ("phase",value?.phase),("issue",value?.issue),("pending",value?.pending==true),("failed",value?.failed==true),
40 ("created",value?.created==true),("createAttempted",value?.createAttempted==true),("activeObserved",value?.activeObserved==true),
41 ("releaseRequested",value?.releaseRequested==true),("explicitRelease",value?.explicitRelease==true),("releaseAttempted",value?.releaseAttempted==true),("originalReleaseCalled",value?.releaseCalled==true),
42 ("inactiveObserved",value?.inactiveObserved==true),("owned",value is not null&&value.token!=0),("token",value?.token.ToString("x")),
43 ("registryDestroyed",value?.registryDestroyed==true),("abandonedWithoutStaleCall",value?.abandoned==true),
44 ("entityId",value?.entity.ToString()),("registry",value?.proof?.ship.registry.ToString("x")),("profileEntry",value?.proof?.entry.ToString("x")),
45 ("node",value?.proof?.ship.node.ToString("x")),("source",value?.proof?.ship.source.ToString("x")),
46 ("activeFlag",layers?.active),("originalVm",layers?.vm.ToString("x")),("ownTokenMember",layers?.ownMember==true),
47 ("foreignTokens",layers?.foreignCount??0),("layerOccupancies",layers is null?null:Arr(layers.occupancy.Select(v=>(JsonNode?)JsonValue.Create(v)).ToArray())),
48 ("accumulator",value?.throttle?[0]),("sourceValue",value?.throttle?[1]),("actualThrottle",value?.throttle?[2]),("controllerThrottle",value?.throttle?[3]),
49 ("throttleObservedAtUnixMilliseconds",value?.throttleAt??0),
50 ("createdAtUnixMilliseconds",value?.createdAt??0),("activeAtUnixMilliseconds",value?.activeAt??0),
51 ("originalReleaseAtUnixMilliseconds",value?.releasedAt??0),("inactiveAtUnixMilliseconds",value?.inactiveAt??0),
52 ("expiresAtUnixMilliseconds",value?.deadline??0),("absoluteEndAtUnixMilliseconds",value?.absoluteEnd??0),
53 ("stationaryContextQualified",stationaryContextQualified),("fixedContext","Bindings_Flight"),("fixedPriority",0),
54 ("reproduction",value?.reproduction==true),("seedAttempted",value?.seedAttempted==true),("originalSeedApplied",value?.seedApplied==true),
55 ("commandRestored",value?.seedRestored==true),("unseededFrames",value?.unseededFrames??0),("persistent075",value?.unseededFrames>=3),
56 ("seedOutstanding",value?.seedOutstanding==true),("seedCleanupCalled",value?.seedCleanupCalled==true),("seedZeroObserved",value?.seedZeroObserved==true),
57 ("seedAtUnixMilliseconds",value?.seedAt??0),("seedCleanupDeadline",value?.seedCleanupDeadline??0),
58 ("trialSamples",Arr((value?.trialSamples??[]).Select(v=>(JsonNode?)v.DeepClone()).ToArray())),
59 ("originalConstructorRva","8fa080"),("originalTypedReleaseRva","77a6a0"),("gameInputWritten",value?.seedApplied==true),
60 ("gameActiveWritten",false),("gameReferenceCountWritten",false),("gameAccumulatorWritten",false),("osInputSent",false));
61 }
62 }
63 private static void CheckFlightContextBridge(JsonElement frame,JsonElement health,JsonElement local,ulong sequence,bool reproduction)
64 {
65 var now=Utc();var ship=health.GetProperty("shipHealth");var docking=local.GetProperty("docking");var sca=local.GetProperty("supercruiseAssist");
66 if(!ResetFresh(frame,now)||!ResetFresh(health,now)||!ResetFresh(local,now)||!frame.GetProperty("inGame").GetBoolean()
67 ||!frame.GetProperty("localPlayerShipVerified").GetBoolean()||!health.GetProperty("localPlayerShipVerified").GetBoolean()
68 ||!ship.GetProperty("verified").GetBoolean()||ship.GetProperty("hullFraction").GetDouble()<.95||ship.GetProperty("emergency").GetBoolean()
69 ||frame.GetProperty("entityId").GetString()!=health.GetProperty("entityId").GetString()||frame.GetProperty("entityId").GetString()!=local.GetProperty("entityId").GetString()
70 ||frame.GetProperty("playerOverride").GetBoolean()||frame.GetProperty("fsd").GetProperty("ownedCharging").GetBoolean()
71 ||frame.GetProperty("rawFsdState").GetInt32() is not(1 or 2)||frame.GetProperty("sequence").GetUInt64()<sequence||frame.GetProperty("sequence").GetUInt64()-sequence>32
72 ||!local.GetProperty("geometryVerified").GetBoolean()||!local.GetProperty("selected").GetBoolean()||local.GetProperty("inSupercruise").GetBoolean()
73 ||local.GetProperty("distanceMetres").GetDouble()>7500||local.GetProperty("speedMetresPerSecond").GetDouble()>(reproduction?100:.1)
74 ||(!reproduction&&local.GetProperty("rawDockingState").GetInt32()!=5)
75 ||reproduction&&(!docking.GetProperty("clearanceGranted").GetBoolean()||local.GetProperty("rawDockingState").GetInt32() is not(3 or 4)
76 ||local.GetProperty("rawRequestState").GetInt32()!=2||local.GetProperty("rawPad").GetInt32()==255)
77 ||docking.GetProperty("nativeLaunchActive").GetBoolean()
78 ||docking.GetProperty("nativeLaunchRequested").GetBoolean()||docking.GetProperty("nativeDutyActive").GetBoolean()
79 ||sca.GetProperty("enabled").GetBoolean()||sca.GetProperty("engaged").GetBoolean())
80 throw new InvalidOperationException("Stationary no-seed context probe requires the fresh healthy empty docked main ship and no SCA or original duty");
81 }
82 private static JsonNode FlightContextRequest(string op,JsonElement request)
83 {
84 var id=request.Str("requestId");var owner=request.Str("owner");if(!ResetId(id)||!ResetId(owner))throw new InvalidOperationException("Explicit one-use original Flight scope identity required");
85 FlightContextWork wanted;
86 lock(flightContextGate)
87 {
88 if(flightContextReceipts.TryGetValue(id,out var known))
89 {
90 if(known.owner!=owner)throw new InvalidOperationException("Original Flight scope belongs to another owner");
91 if(op=="flight.context.seed")
92 {
93 if(!known.reproduction||!known.activeObserved||known.token==0||known.failed||known.releaseRequested||known.seedAttempted||known.seedRequested||Utc()>=known.deadline)
94 throw new InvalidOperationException("Only one active original after-grant seed is allowed");
95 known.seedRequested=true;return FlightContextDescription(known);
96 }
97 if(op=="flight.context.release")
98 {
99 known.releaseRequested=true;known.explicitRelease=true;known.pending=false;
100 known.preserveSeed=known.reproduction&&known.unseededFrames>=3&&request.TryGetProperty("preserveOwnedSeedForReset",out var preserve)&&preserve.ValueKind==JsonValueKind.True;
101 known.seedCleanupDeadline=Math.Min(known.absoluteEnd,Utc()+200);
102 }
103 if(op!="flight.context.renew")return FlightContextDescription(known);
104 if(known.failed||known.releaseRequested||known.releaseAttempted||known.token==0||Utc()>=known.deadline||Utc()>=known.absoluteEnd)
105 throw new InvalidOperationException("Original Flight scope is no longer renewable");
106 wanted=known;
107 }
108 else
109 {
110 if(op!="flight.context.arm"||flightContextReceipts.Count>=32||flightContext is {} current&&(current.token!=0||current.pending))
111 throw new InvalidOperationException("No new original Flight scope is available");
112 var purpose=request.Str("purpose");var reproduction=purpose=="after-grant-persistent075";
113 if(purpose!="stationary-no-seed"&&!reproduction||reproduction&&!stationaryContextQualified)
114 throw new InvalidOperationException("The fixed after-grant trial requires its original no-seed admission proof");
115 wanted=new(){id=id,owner=owner,entity=0,generation=request.U64("selectionGeneration"),absoluteEnd=Utc()+(reproduction?1800UL:3000UL),reproduction=reproduction};
116 flightContextReceipts.Add(id,wanted);flightContext=wanted;
117 }
118 }
119 try
120 {
121 var deadline=request.U64("expiresAtUnixMilliseconds");var ttl=request.Int("ttlMilliseconds");var sequence=request.U64("sequence");
122 if(!enabled||!ResetDeadline(deadline,Utc(),ttl)||sequence==0||wanted.generation==0)throw new InvalidOperationException("Original Flight scope deadline or selection is unavailable");
123 Root context;lock(gate)context=root;
124 if(!context.valid||context.state!="InGame"||context.next!=context.state||!Fresh(context.tick,GetTickCount64()))throw new InvalidOperationException("Stable original InGame required for Flight scope");
125 var frame=ResetNav(Obj(("op","read")),deadline);var health=ResetNav(Obj(("op","health.read")),deadline);var local=ResetNav(Obj(("op","local.read")),deadline);
126 CheckFlightContextBridge(frame,health,local,sequence,wanted.reproduction);
127 var selected=ResetNav(Obj(("op","local.status"),("owner",owner),("generation",wanted.generation)),deadline);
128 if(!selected.GetProperty("selected").GetBoolean()||!selected.GetProperty("telemetryReady").GetBoolean()||!SameResetTarget(selected.GetProperty("target"),local.GetProperty("target")))
129 throw new InvalidOperationException("Flight context current base owner or selection changed");
130 var entity=ulong.Parse(frame.GetProperty("entityId").GetString()!);var world=ulong.Parse(frame.GetProperty("referenceWorldAddress").GetString()!);
131 if(wanted.proof is null)
132 {
133 JsonObject[] candidates;lock(gate)candidates=mapperNodes.Values.Where(row=>row["expectedType"]?.GetValue<bool>()==true
134 &&Utc()>=row["observedAtUnixMilliseconds"]!.GetValue<ulong>()&&Utc()-row["observedAtUnixMilliseconds"]!.GetValue<ulong>()<=200).ToArray();
135 var h=health.GetProperty("shipHealth");Navigation.RecoveryFlightContextBinding? proof=null;
136 foreach(var candidate in candidates)
137 {
138 Navigation.RecoveryFlightContextBinding bound;
139 try{bound=Navigation.BindRecoveryFlightContext(entity,world,local.GetProperty("target"),h.GetProperty("hullFraction").GetDouble(),
140 h.GetProperty("shieldFraction").ValueKind==JsonValueKind.Number?h.GetProperty("shieldFraction").GetDouble():null,
141 (nuint)Convert.ToUInt64(candidate["node"]!.GetValue<string>(),16),wanted.reproduction);}catch{continue;}
142 if(proof!=null)throw new InvalidOperationException("Current Flight context mapper is ambiguous");proof=bound;
143 }
144 if(proof is null)throw new InvalidOperationException("Current owned Flight mapper has not been observed");wanted.proof=proof;wanted.entity=entity;
145 }
146 if(entity!=wanted.entity||world!=wanted.proof.ship.baseline.referenceWorld||!SameResetTarget(local.GetProperty("target"),wanted.proof.ship.target))
147 throw new InvalidOperationException("Flight context main ship, world or target changed");
148 lock(flightContextGate)
149 {
150 if(wanted.releaseRequested||wanted.failed||Utc()>=deadline||Utc()>=wanted.absoluteEnd)throw new InvalidOperationException("Original Flight context was cancelled or expired while checking");
151 wanted.deadline=deadline;wanted.bridgeAt=Utc();wanted.pending=!wanted.created;wanted.phase=wanted.created?"owned-renewed":"armed";
152 }
153 }
154 catch(Exception error){lock(flightContextGate){wanted.failed=true;wanted.releaseRequested=true;wanted.pending=false;wanted.issue=error.ToString();wanted.phase="bridge-rejected";}}
155 return FlightContextDescription(wanted);
156 }
157 private static void ReleaseFlightContext(FlightContextWork wanted,string reason,bool destroying=false)
158 {
159 if(wanted.token==0)return;
160 if(GetCurrentThreadId()!=expectedThread)throw new InvalidOperationException("Original Flight token may only be released on its owning thread");
161 if(wanted.proof is null||wanted.registryDestroyed)throw new InvalidOperationException("No live original Flight token proof remains");
162 wanted.pending=false;wanted.phase="checking-original-token-release";
163 var submitted=false;
164 try
165 {
166 Navigation.ReleaseRecoveryFlightContext(wanted.proof,ref wanted.token,()=>
167 {submitted=true;wanted.releaseAttempted=true;wanted.phase="releasing-original-token";});
168 wanted.releaseCalled=true;wanted.releasedAt=Utc();wanted.phase="original-token-released";
169 if(reason!="explicit-release")wanted.issue=reason+(wanted.issue.Length==0?"":": "+wanted.issue);
170 }
171 catch(Exception error)
172 {
173 wanted.failed=true;wanted.releaseRequested=true;wanted.issue=reason+": "+error;
174 if(submitted||destroying)
175 {
176 // An uncertain original call is never replayed, and a token
177 // cannot outlive its raw entry when the registry is destroyed.
178 wanted.token=0;wanted.abandoned=true;wanted.phase="release-unresolved-no-replay";
179 }
180 else
181 {
182 // Validation did not invoke native code. Keep the owned slot
183 // so a later stable frame can release it instead of leaking
184 // an enabled original input layer after a transient refusal.
185 wanted.phase="release-precheck-refused";
186 }
187 if(!destroying)throw;
188 }
189 }
190 private static void PollFlightContextBeforeOriginal()
191 {
192 FlightContextWork? wanted;lock(flightContextGate)wanted=flightContext;
193 if(wanted is null||wanted.proof is null||wanted.registryDestroyed)return;
194 Root context;lock(gate)context=root;var now=Utc();
195 CleanupFlightThrottleTrial(wanted);
196 var allowed=FlightContextAllowed(new(now,wanted.deadline,wanted.absoluteEnd,wanted.bridgeAt,
197 enabled&&context.valid&&context.state=="InGame",true,true,true,true,true,false,context.next!=context.state||!Fresh(context.tick,GetTickCount64())));
198 try
199 {
200 if(GetCurrentThreadId()!=expectedThread)throw new InvalidOperationException("Original Flight scope frame is on another thread");
201 if(wanted.token!=0)
202 {
203 if(!allowed||wanted.releaseRequested||wanted.failed){ReleaseFlightContext(wanted,wanted.releaseRequested?"explicit-release":"expired-or-scene-changed");return;}
204 Navigation.CheckRecoveryFlightContextFrame(wanted.proof,false);wanted.layers=Navigation.ReadRecoveryFlightContextLayers(wanted.proof,wanted.token);
205 if(!wanted.layers.OnlyOwn){wanted.failed=true;ReleaseFlightContext(wanted,"non-owned-original-input-layer");}
206 }
207 else if(wanted.pending&&!wanted.createAttempted)
208 {
209 if(!allowed){wanted.pending=false;wanted.failed=true;wanted.phase="expired-before-create";return;}
210 wanted.createAttempted=true;wanted.pending=false;
211 Navigation.CreateRecoveryFlightContext(wanted.proof,ref wanted.token);
212 if(wanted.token==0||!Navigation.RecoveryFlightContextTokenValid(wanted.proof,wanted.token))throw new InvalidOperationException("Original lowest-priority Flight token was not created");
213 wanted.created=true;wanted.createdAt=Utc();wanted.layers=Navigation.ReadRecoveryFlightContextLayers(wanted.proof,wanted.token);
214 if(!wanted.layers.OnlyOwn)throw new InvalidOperationException("Original Flight layer0 does not contain exactly the owned token");
215 wanted.phase="created-awaiting-original-active";
216 }
217 }
218 catch(Exception error)
219 {
220 wanted.failed=true;wanted.pending=false;wanted.issue=error.ToString();wanted.phase="original-scope-refused";
221 if(wanted.token!=0&&!wanted.releaseAttempted)try{ReleaseFlightContext(wanted,"native-guard-refusal");}catch{}
222 }
223 }
224 private static void ObserveFlightContextAfterMapper(nuint node)
225 {
226 FlightContextWork? wanted;lock(flightContextGate)wanted=flightContext;
227 if(wanted?.proof is null||wanted.registryDestroyed||node!=wanted.proof.ship.node||GetCurrentThreadId()!=expectedThread||!wanted.created)return;
228 try
229 {
230 wanted.throttle=Navigation.ReadRecoveryFlightContextThrottle(wanted.proof);wanted.throttleAt=Utc();
231 wanted.layers=Navigation.ReadRecoveryFlightContextLayers(wanted.proof,wanted.token);
232 if(wanted.token!=0)
233 {
234 if(!wanted.layers.OnlyOwn){wanted.failed=true;ReleaseFlightContext(wanted,"original-ui-or-foreign-layer");return;}
235 if(wanted.layers.active!=0&&wanted.layers.vm!=0)
236 {wanted.activeObserved=true;if(wanted.activeAt==0)wanted.activeAt=Utc();wanted.phase="original-active-observed";}
237 }
238 else if(wanted.releaseCalled&&wanted.layers.Empty&&wanted.layers.active==0&&wanted.layers.vm==0)
239 {
240 wanted.inactiveObserved=true;if(wanted.inactiveAt==0)wanted.inactiveAt=Utc();wanted.phase="original-inactive-observed";
241 stationaryContextQualified|=!wanted.reproduction&&wanted.activeObserved&&wanted.explicitRelease&&!wanted.failed&&!wanted.abandoned;
242 }
243 }
244 catch(Exception error){wanted.failed=true;wanted.releaseRequested=true;wanted.issue=error.ToString();}
245 }
246 private static bool PrepareFlightContextDeactivate()
247 {
248 lock(flightContextGate)
249 {
250 if(flightContext is not {} wanted)return true;
251 wanted.pending=false;wanted.releaseRequested=true;
252 return wanted.token==0;
253 }
254 }
255 [UnmanagedCallersOnly]private static void RegistryDestroyHook(nuint registryRoot)
256 {
257 // 8ED50C's adjusting destructor subtracts 58 then calls this exact
258 // root-only void destructor. Its original profile map is still alive
259 // here. Always forward once, with RCX unmodified, after our cleanup.
260 FlightContextWork? wanted;lock(flightContextGate)wanted=flightContext;
261 if(wanted?.proof is {} proof&&proof.ship.registry==registryRoot+0x58)
262 {
263 try{if(wanted.token!=0)ReleaseFlightContext(wanted,"registry-destroy-before-original",true);}
264 catch(Exception error){wanted.failed=true;wanted.abandoned=true;wanted.issue=error.ToString();wanted.token=0;}
265 finally{wanted.registryDestroyed=true;wanted.pending=false;wanted.failed=true;wanted.phase="registry-destroyed-no-late-release";}
266 }
267 try{Call(originalRegistryDestroy,(ulong)registryRoot);}catch{}
268 }
269 internal static uint FlightContextSelfTest()
270 {
271 if(started||ImageHash(ModulePath())==GameHash)return 0;uint mask=0;
272 var valid=new FlightContextGate(1000,1200,4000,950,true,true,true,true,true,true,false,false);
273 if(FlightContextAllowed(valid))mask|=1;
274 if(!FlightContextAllowed(valid with{deadline=1000})&&!FlightContextAllowed(valid with{deadline=1251}))mask|=2;
275 if(!FlightContextAllowed(valid with{bridgeAt=799})&&!FlightContextAllowed(valid with{bridgeAt=1001}))mask|=4;
276 if(!FlightContextAllowed(valid with{overridePresent=true})&&!FlightContextAllowed(valid with{sceneChange=true}))mask|=8;
277 if(!FlightContextAllowed(valid with{owner=false})&&!FlightContextAllowed(valid with{sameShip=false}))mask|=16;
278 if(!FlightContextAllowed(valid with{health=false})&&!FlightContextAllowed(valid with{empty=false})&&!FlightContextAllowed(valid with{ownLayer=false}))mask|=32;
279 var own=new Navigation.RecoveryFlightContextLayers(1,2,3,4,1,[1,0,0],true,0);
280 if(own.OnlyOwn&&!own.Empty&&!((own with{ownMember=false}).OnlyOwn)&&!((own with{foreignCount=1}).OnlyOwn)&&!((own with{occupancy=[2,0,0]}).OnlyOwn))mask|=64;
281 var empty=own with{vm=0,active=0,occupancy=[0,0,0],ownMember=false};
282 if(empty.Empty&&!empty.OnlyOwn&&!FlightContextAllowed(valid with{absoluteEnd=1000})&&!FlightContextAllowed(valid with{inGame=false}))mask|=128;
283 if(Navigation.FlightContextNoSeedThrottle([0,0,0,0])&&Navigation.FlightContextNoSeedThrottle([0,0,0,-100])
284 &&!Navigation.FlightContextNoSeedThrottle([0,0,.75f,-100])&&!Navigation.FlightContextNoSeedThrottle([0,.75f,0,-100])
285 &&!Navigation.FlightContextNoSeedThrottle([.75f,0,0,-100])&&!Navigation.FlightContextNoSeedThrottle([0,0,0,-1])
286 &&!Navigation.FlightContextNoSeedThrottle([0,0,0,float.NaN]))mask|=256;
287 return mask;
288 }
289 }
290 #endif
Added native/ManagedPlugin/SessionRecovery.ThrottleTrial.cs +89 -0
@@ -0,0 +1,89 @@
1 #if PLUGIN_SESSION_RECOVERY
2 namespace XFEEDToolBox.Plugin;
3
4 internal static unsafe partial class SessionRecoveryRuntime
5 {
6 private static bool PersistentThrottleTrialFrame(ulong frame,ulong previous,bool restored,float command,float[] values)
7 =>frame>previous&&restored&&command==0&&values.Length==4&&values.Take(3).All(value=>float.IsFinite(value)&&Math.Abs(value-.75f)<=.001f)
8 &&float.IsFinite(values[3])&&(Math.Abs(values[3])<=1.001f||values[3]==-100);
9 private static Navigation.RecoveryThrottlePulse BeginFlightThrottleTrial(nuint node,float dt)
10 {
11 FlightContextWork? wanted;lock(flightContextGate)wanted=flightContext;
12 if(wanted?.proof is null||!wanted.reproduction||!wanted.seedRequested||wanted.seedAttempted||wanted.proof.ship.node!=node)return default;
13 wanted.seedAttempted=true;
14 try
15 {
16 if(!enabled||GetCurrentThreadId()!=expectedThread||!float.IsFinite(dt)||dt<=0||dt>.25f||wanted.token==0||wanted.releaseRequested
17 ||wanted.failed||Utc()>=wanted.deadline||Utc()>=wanted.absoluteEnd||Utc()<wanted.bridgeAt||Utc()-wanted.bridgeAt>200
18 ||!Navigation.ReadRecoveryFlightContextLayers(wanted.proof,wanted.token).OnlyOwn)throw new InvalidOperationException("Original seed lease or exact layer changed");
19 var pulse=Navigation.BeginRecoveryThrottleTrial(wanted.proof);wanted.seedApplied=pulse.applied;wanted.seedOutstanding=pulse.applied;wanted.seedAt=Utc();
20 wanted.phase="original-absolute-command-once";return pulse;
21 }
22 catch(Exception error){wanted.failed=true;wanted.releaseRequested=true;wanted.issue=error.ToString();wanted.phase="seed-refused-no-replay";return default;}
23 }
24 private static void EndFlightThrottleTrial(in Navigation.RecoveryThrottlePulse pulse)
25 {
26 if(!pulse.applied)return;FlightContextWork? wanted;lock(flightContextGate)wanted=flightContext;
27 if(wanted?.proof is null||wanted.proof.ship.node!=pulse.node)return;
28 wanted.seedRestored=Navigation.EndRecoveryThrottleTrial(pulse);
29 if(!wanted.seedRestored){wanted.failed=true;wanted.releaseRequested=true;wanted.issue="Original one-frame absolute command was not restored";}
30 }
31 private static void ObserveFlightThrottleTrial(nuint node)
32 {
33 FlightContextWork? wanted;lock(flightContextGate)wanted=flightContext;
34 if(wanted?.proof is null||!wanted.reproduction||!wanted.seedApplied||wanted.registryDestroyed||wanted.proof.ship.node!=node||GetCurrentThreadId()!=expectedThread)return;
35 var values=Navigation.ReadRecoveryFlightContextThrottle(wanted.proof);var command=Navigation.RecoveryThrottleTrialCommand(wanted.proof);
36 if(values.Take(3).All(value=>Math.Abs(value)<=.001f))
37 {wanted.seedZeroObserved=true;wanted.seedOutstanding=false;return;}
38 Root rootFrame;lock(gate)rootFrame=root;
39 if(rootFrame.sequence==wanted.lastTrialFrame||!wanted.seedRestored||command!=0)return;
40 var previous=wanted.lastTrialFrame;wanted.lastTrialFrame=rootFrame.sequence;
41 if(PersistentThrottleTrialFrame(rootFrame.sequence,previous,wanted.seedRestored,command,values))
42 {
43 wanted.unseededFrames++;
44 if(wanted.trialSamples.Length<5)wanted.trialSamples=[..wanted.trialSamples,Obj(("frameSequence",rootFrame.sequence),("observedAtUnixMilliseconds",Utc()),
45 ("accumulator",values[0]),("source",values[1]),("actualThrottle",values[2]),("controllerOverride",values[3]),("absoluteCommand",command))];
46 if(wanted.unseededFrames>=3)wanted.phase="persistent075-original-graph";
47 }
48 else if(wanted.unseededFrames>0&&wanted.token!=0)
49 {wanted.failed=true;wanted.releaseRequested=true;wanted.issue="Original graph did not retain the fixed .75 trial";}
50 }
51 private static void CleanupFlightThrottleTrial(FlightContextWork wanted)
52 {
53 if(!wanted.seedOutstanding||wanted.seedCleanupCalled||wanted.proof is null||wanted.registryDestroyed)return;
54 Root context;lock(gate)context=root;
55 if(context.state!="InGame"||context.next!=context.state)return;
56 var due=wanted.failed||Utc()>=wanted.deadline||Utc()>=wanted.absoluteEnd
57 ||wanted.releaseRequested&&(!wanted.preserveSeed||Utc()>=wanted.seedCleanupDeadline);
58 if(!due)return;
59 try
60 {
61 var layers=Navigation.ReadRecoveryFlightContextLayers(wanted.proof,wanted.token);
62 if(wanted.token!=0?!layers.OnlyOwn:!layers.Empty)
63 {wanted.failed=true;wanted.issue="Original UI or foreign layer owns input; owned seed cleanup will not compete";return;}
64 wanted.seedCleanupCalled=Navigation.CleanupRecoveryThrottleTrial(wanted.proof);
65 wanted.failed=true;wanted.issue=wanted.seedCleanupCalled?"Owned .75 trial expired or was cancelled; original cleanup dispatched":
66 "Owned .75 cleanup refused changed identity or original input; no user input was overwritten";
67 }
68 catch(Exception error){wanted.failed=true;wanted.issue="Owned seed cleanup failed: "+error;}
69 }
70 internal static uint ThrottleTrialSelfTest()
71 {
72 if(started||ImageHash(ModulePath())==GameHash)return 0;uint mask=0;float[] actual=[.75f,.75f,.75f,-100];
73 if(PersistentThrottleTrialFrame(2,1,true,0,actual))mask|=1;
74 if(!PersistentThrottleTrialFrame(1,1,true,0,actual)&&!PersistentThrottleTrialFrame(0,1,true,0,actual))mask|=2;
75 if(!PersistentThrottleTrialFrame(2,1,false,0,actual)&&!PersistentThrottleTrialFrame(2,1,true,.75f,actual))mask|=4;
76 if(!PersistentThrottleTrialFrame(2,1,true,0,[0,.75f,.75f,-100]))mask|=8;
77 if(!PersistentThrottleTrialFrame(2,1,true,0,[.75f,0,.75f,-100]))mask|=16;
78 if(!PersistentThrottleTrialFrame(2,1,true,0,[.75f,.75f,0,-100]))mask|=32;
79 if(!PersistentThrottleTrialFrame(2,1,true,0,[.75f,.75f,float.NaN,-100])&&!PersistentThrottleTrialFrame(2,1,true,0,[.75f,.75f,.75f,float.NaN]))mask|=64;
80 if(!PersistentThrottleTrialFrame(2,1,true,0,[.75f,.75f,.75f,-1.5f])&&PersistentThrottleTrialFrame(2,1,true,0,[.75f,.75f,.75f,0]))mask|=128;
81 return mask;
82 }
83 }
84 internal static class FlightThrottleTrialExports
85 {
86 [UnmanagedCallersOnly(EntryPoint="XFEFlightThrottleTrialSelfTest")]
87 internal static uint Test(){try{return SessionRecoveryRuntime.ThrottleTrialSelfTest();}catch{return 0;}}
88 }
89 #endif
Modified native/ManagedPlugin/SessionRecoveryRuntime.cs +22 -4
@@ -8,7 +8,7 @@ namespace XFEEDToolBox.Plugin;
8 8 internal static unsafe partial class SessionRecoveryRuntime
9 9 {
10 10 private const nuint ContextUpdate=0x21697F0,MenuPoll=0x488AC60,MapperThunk=0x8D60D0,FlowAction=0x1D65500,PauseSelect=0x4838B50;
11 private const string Service="SessionRecovery0514g";
11 private const string Service="SessionRecovery0514k";
12 12 private static nuint image,end,originalContext,originalMenu,originalMapper;
13 13 private static uint expectedThread;
14 14 private static bool started,enabled;
@@ -115,6 +115,10 @@ internal static unsafe partial class SessionRecoveryRuntime
115 115 }
116 116 [UnmanagedCallersOnly]private static ulong ContextHook(nuint context)
117 117 {
118 // Release an expired scope or one about to leave InGame before the
119 // original context callback can begin scene teardown. The registry's
120 // real destructor additionally owns an independent pre-destroy hook.
121 try{CaptureRoot(context);PollFlightContextBeforeOriginal();}catch{}
118 122 ulong result;try{result=Call(originalContext,(ulong)context);}catch{return 0;}
119 123 try
120 124 {
@@ -147,9 +151,14 @@ internal static unsafe partial class SessionRecoveryRuntime
147 151 {
148 152 ResetWork? resetting=null;
149 153 try{resetting=BeginOriginalReset(node,dt);}catch(Exception error){FailOriginalReset(error.Message);}
154 Navigation.RecoveryThrottlePulse trial=default;
155 try{trial=BeginFlightThrottleTrial(node,dt);}catch{}
150 156 float? before=null;try{if(enabled)before=Read<float>(node+0xD0);}catch{}
151 157 ulong result;try{result=Call(originalMapper,(ulong)node,F32(dt));}catch{FailOriginalReset("Original mapper call failed");return 0;}
158 finally{try{EndFlightThrottleTrial(trial);}catch{}}
152 159 try{FinishOriginalReset(resetting,node);}catch(Exception error){FailOriginalReset(error.Message);}
160 try{ObserveFlightThrottleTrial(node);}catch{}
161 try{ObserveFlightContextAfterMapper(node);}catch{}
153 162 try
154 163 {
155 164 if(!enabled)return result;mapperCalls++;
@@ -178,6 +187,7 @@ internal static unsafe partial class SessionRecoveryRuntime
178 187 var actions=new JsonArray();
179 188 for(var i=0;i<m.names.Length;i++)actions.Add((JsonNode)Obj(("name",m.names[i]),("enabled",m.allowed[i]),("returnToMenu",ReturnToMenu(m.names[i])),("recoveryAction",AllowedAction(m,m.names[i]))));
180 189 return Obj(("name","XFEEDToolBox.SessionRecovery"),("schema",1),("processId",ProcessId),("gameSha256",GameHash),("service",Service),("nativeDockingReset",true),("nativeDockingStart",true),("dockingReset",ResetDescription()),
190 ("nativeFlightContextProbe",true),("flightContext",FlightContextDescription()),
181 191 ("enabled",enabled),("owningThread",expectedThread),("contextCalls",contextCalls),("menuCalls",menuCalls),("menuMatches",menuMatches),
182 192 ("menuReadErrors",menuReadErrors),("lastMenuIssue",lastMenuIssue),("mapperCalls",mapperCalls),("mapperNodes",nodes),
183 193 ("sequence",r.sequence),("observedAtUnixMilliseconds",r.utc),("flow",r.flow.ToString("x")),("csm",r.csm.ToString("x")),
@@ -190,18 +200,21 @@ internal static unsafe partial class SessionRecoveryRuntime
190 200 private static JsonNode Handle(JsonElement request)
191 201 {
192 202 var op=request.Str("op");
203 if(op is "flight.context.arm" or "flight.context.renew" or "flight.context.release" or "flight.context.status" or "flight.context.seed")return FlightContextRequest(op,request);
193 204 if(op=="docking.reset.probe")return ResetBridgeProbe();
194 205 if(op is "docking.reset.arm" or "docking.reset.status" or "docking.reset.cancel")return ResetRequest(op,request);
195 206 if(op is "describe" or "read")return Description();
196 207 if(op=="deactivate")
197 208 {
209 if(!PrepareFlightContextDeactivate())return Description();
198 210 lock(gate){enabled=false;work.pending=false;}
199 211 CancelOriginalReset();
200 HookDisable(image+MapperThunk);HookDisable(image+MenuPoll);HookDisable(image+ContextUpdate);
212 HookDisable(image+MapperThunk);HookDisable(image+MenuPoll);HookDisable(image+ContextUpdate);HookDisable(image+RegistryDestroy);
201 213 // Keep pinned code and trampolines alive for in-flight callbacks.
202 214 return Description();
203 215 }
204 216 if(op!="session.recovery.leave")throw new InvalidOperationException("Unsupported purpose-built session recovery operation");
217 if(!PrepareFlightContextDeactivate())throw new InvalidOperationException("Original Flight scope must be released before a menu action is queued");
205 218 var id=request.Str("requestId");var action=request.Str("action");var ttl=request.Int("ttlMilliseconds");var expected=request.U64("menuSequence");
206 219 if(id.Length!=32||id.Any(c=>!char.IsAsciiHexDigit(c))||action is not("ExitOptions" or "Confirmed Quit To Main Menu")||ttl is <1 or >250)throw new InvalidOperationException("Bounded original return-to-menu request required");
207 220 lock(gate)
@@ -224,12 +237,16 @@ internal static unsafe partial class SessionRecoveryRuntime
224 237 Navigation.ConfigureRecoveryReadOnlyImage();
225 238 if(!Prefix(ContextUpdate,[0x40,0x55,0x53,0x56,0x57,0x41,0x54,0x41,0x56,0x41,0x57]))return 0;
226 239 if(!Prefix(MenuPoll,[0x4C,0x8B,0xDC,0x49,0x89,0x4B,0x08,0x55,0x41,0x55])||!Prefix(MapperThunk,[0xE9,0x2B,0xD4,0x01,0x00]))return 0;
240 if(!Prefix(RegistryDestroy,[0x48,0x89,0x5C,0x24,0x08,0x48,0x89,0x6C,0x24,0x10,0x48,0x89,0x74,0x24,0x18,0x57]))return 0;
241 if(!Prefix(0x25E9360,[0xC7,0x81,0x80,0x06,0,0,0,0,0xC8,0xC2,0xC3]))return 0;
227 242 var status=HookInitialize();if(status is not(0 or 1))return 0;
228 243 if(HookCreate(image+ContextUpdate,(nuint)(delegate* unmanaged<nuint,ulong>)&ContextHook,out originalContext)!=0)return 0;
229 244 if(HookCreate(image+MenuPoll,(nuint)(delegate* unmanaged<nuint,ulong>)&MenuHook,out originalMenu)!=0){HookRemove(image+ContextUpdate);return 0;}
230 245 if(HookCreate(image+MapperThunk,(nuint)(delegate* unmanaged<nuint,float,ulong>)&MapperHook,out originalMapper)!=0){HookRemove(image+MenuPoll);HookRemove(image+ContextUpdate);return 0;}
231 if(!Pin((nuint)(delegate* unmanaged<nuint,ulong>)&ContextHook)||HookQueueEnable(image+ContextUpdate)!=0||HookQueueEnable(image+MenuPoll)!=0||HookQueueEnable(image+MapperThunk)!=0||HookApplyQueued()!=0)
232 {HookDisable(image+MapperThunk);HookDisable(image+MenuPoll);HookDisable(image+ContextUpdate);return 0;}
246 if(HookCreate(image+RegistryDestroy,(nuint)(delegate* unmanaged<nuint,void>)&RegistryDestroyHook,out originalRegistryDestroy)!=0)
247 {HookRemove(image+MapperThunk);HookRemove(image+MenuPoll);HookRemove(image+ContextUpdate);return 0;}
248 if(!Pin((nuint)(delegate* unmanaged<nuint,ulong>)&ContextHook)||HookQueueEnable(image+RegistryDestroy)!=0||HookQueueEnable(image+ContextUpdate)!=0||HookQueueEnable(image+MenuPoll)!=0||HookQueueEnable(image+MapperThunk)!=0||HookApplyQueued()!=0)
249 {HookDisable(image+MapperThunk);HookDisable(image+MenuPoll);HookDisable(image+ContextUpdate);HookDisable(image+RegistryDestroy);return 0;}
233 250 enabled=true;started=true;PipeServer.StartJson(Service,32768,Handle);return 1;
234 251 }
235 252 }
@@ -263,5 +280,6 @@ internal static class SessionRecoveryExports
263 280 [UnmanagedCallersOnly(EntryPoint="XFEDockingResetSelfTest")]internal static uint ResetSelfTest(){try{return SessionRecoveryRuntime.ResetSelfTest();}catch{return 0;}}
264 281 [UnmanagedCallersOnly(EntryPoint="XFEDockingResetTelemetrySelfTest")]internal static uint ResetTelemetrySelfTest(){try{return SessionRecoveryRuntime.ResetTelemetrySelfTest();}catch{return 0;}}
265 282 [UnmanagedCallersOnly(EntryPoint="XFEDockingStartSelfTest")]internal static uint DockingStartSelfTest(){try{return Navigation.RecoveryDockingStartSelfTest();}catch{return 0;}}
283 [UnmanagedCallersOnly(EntryPoint="XFEFlightContextSelfTest")]internal static uint FlightContextSelfTest(){try{return SessionRecoveryRuntime.FlightContextSelfTest();}catch{return 0;}}
266 284 }
267 285 #endif
Added research/NativeAcceptance/FlightContextAdmissionAcceptance.cs +120 -0
@@ -0,0 +1,120 @@
1 using System.Diagnostics;
2 using System.Text.Json;
3 using XFEEDToolBox.Core.Models;
4 using XFEEDToolBox.Core.Native;
5 using XFEEDToolBox.Core.Navigation;
6
7 internal static class FlightContextAdmissionAcceptance
8 {
9 private const string Service=NativeSystemFlightControlDriver.OriginalDockingService;
10 private static bool Flag(JsonElement value,string name)=>value.TryGetProperty(name,out var item)&&item.ValueKind==JsonValueKind.True;
11 internal static async Task RunAsync(int pid,string workRoot,string report)
12 {
13 workRoot=Path.GetFullPath(workRoot);report=Path.GetFullPath(report);
14 if(File.Exists(report))throw new IOException("Preserve the previous original Flight admission evidence.");
15 Directory.CreateDirectory(Path.GetDirectoryName(report)!);
16 using var exclusive=new FileStream(Path.Combine(workRoot,"docking-fixture-wait.lock"),FileMode.OpenOrCreate,FileAccess.ReadWrite,FileShare.None);
17 using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(12));var token=timeout.Token;
18 using var process=Process.GetProcessById(pid);var started=process.StartTime.ToUniversalTime().Ticks;
19 using var guardian=await PersistentProtectionProcess.StartOrReuseAsync(pid,workRoot,token,allowStart:false);
20 var nav=new NavigationPipeClient(pid);var scope=new NavigationPipeClient(pid,Service);
21 var owner=Guid.NewGuid().ToString("N");var requestId=Guid.NewGuid().ToString("N");
22 var observations=new List<JsonElement>();JsonElement? admission=null,finalDock=null;
23 var armed=false;var releaseSent=false;var passed=false;Exception? failure=null;var phase="preflight";
24 async Task Save()=>await File.WriteAllTextAsync(report,JsonSerializer.Serialize(new{at=DateTimeOffset.UtcNow,pid,started,phase,passed,
25 owner,requestId,admission,observations,finalDock,failure=failure?.ToString(),inputsSent=0,flightStarted=false,tradeOrdersSent=0,
26 originalContextOnly=true,protectionPid=guardian.ObserverPid},ToolboxSettings.JsonOptions));
27 void CheckProcess(){process.Refresh();if(process.HasExited||process.StartTime.ToUniversalTime().Ticks!=started)throw new IOException("Game process identity changed.");guardian.RequireReady(pid);}
28 async Task<JsonElement> Status()
29 {
30 CheckProcess();var value=await scope.CallAsync(new{op="flight.context.status",owner,requestId},token);
31 observations.Add(value.Clone());return value;
32 }
33 async Task Release()
34 {
35 if(!armed||releaseSent)return;CheckProcess();releaseSent=true;
36 using var cleanup=new CancellationTokenSource(TimeSpan.FromSeconds(2));
37 observations.Add((await scope.CallAsync(new{op="flight.context.release",owner,requestId},cleanup.Token)).Clone());
38 }
39 try
40 {
41 CheckProcess();var hello=await scope.CallAsync(new{op="describe"},token);
42 if(hello.GetProperty("processId").GetInt32()!=pid||hello.GetProperty("service").GetString()!=Service
43 ||hello.GetProperty("gameSha256").GetString()!=NativeSystemLocationReader.GameSha256||!Flag(hello,"enabled")
44 ||hello.GetProperty("state").GetString()!="InGame"||hello.GetProperty("nextState").GetString()!="InGame")
45 throw new InvalidDataException("Pinned original Flight context extension is unavailable.");
46 var local=await nav.CallAsync(new{op="local.read"},token);var target=local.GetProperty("target").Clone();
47 var market=target.GetProperty("marketId").GetUInt32();
48 var dock=await nav.CallAsync(new{op="commerce.docked.status",marketId=market},token);
49 if(!Flag(dock,"ready")||!Flag(dock,"serviceReady")||!Flag(dock,"healthy")||!Flag(dock,"fresh")||dock.GetProperty("rawDockingState").GetInt32()!=5)
50 throw new InvalidDataException("No-seed admission must remain actually docked and service ready.");
51 var selected=await nav.CallAsync(new{op="local.select",owner,key=target.GetProperty("key").GetInt32(),bodyAddress=target.GetProperty("bodyAddress").GetString(),
52 kind=target.GetProperty("kind").GetUInt32(),marketId=market,name=target.GetProperty("name").GetString()},token);
53 var generation=selected.GetProperty("generation").GetUInt64();var until=DateTimeOffset.UtcNow.AddSeconds(2);
54 while(!Flag(selected,"selected"))
55 {
56 if(!Flag(selected,"pending")||DateTimeOffset.UtcNow>=until)throw new InvalidDataException("Original station selection was not confirmed.");
57 await Task.Delay(10,token);selected=await nav.CallAsync(new{op="local.status",owner,generation},token);
58 }
59 await Save();
60 var frame=await nav.CallAsync(new{op="read"},token);var deadline=DateTimeOffset.UtcNow.AddMilliseconds(250);
61 phase="original-context-arm";armed=true;
62 admission=await scope.CallAsync(new{op="flight.context.arm",owner,requestId,purpose="stationary-no-seed",selectionGeneration=generation,
63 sequence=frame.GetProperty("sequence").GetUInt64(),ttlMilliseconds=250,expiresAtUnixMilliseconds=deadline.ToUnixTimeMilliseconds()},token);
64 observations.Add(admission.Value.Clone());until=DateTimeOffset.UtcNow.AddSeconds(2);
65 while(true)
66 {
67 var state=await Status();
68 if(Flag(state,"failed"))throw new InvalidDataException("Original Flight admission refused: "+state.GetProperty("issue").GetString());
69 if(Flag(state,"activeObserved"))
70 {
71 if(!Flag(state,"created")||!Flag(state,"owned")||!Flag(state,"ownTokenMember")||state.GetProperty("foreignTokens").GetUInt64()!=0
72 ||state.GetProperty("activeFlag").GetInt32()!=1||state.GetProperty("fixedPriority").GetInt32()!=0)
73 throw new InvalidDataException("No exact original owned Flight layer was observed.");
74 break;
75 }
76 if(DateTimeOffset.UtcNow>=until)throw new TimeoutException("Original Flight activation was not observed.");
77 if(deadline-DateTimeOffset.UtcNow<TimeSpan.FromMilliseconds(100))
78 {
79 frame=await nav.CallAsync(new{op="read"},token);deadline=DateTimeOffset.UtcNow.AddMilliseconds(250);
80 observations.Add((await scope.CallAsync(new{op="flight.context.renew",owner,requestId,selectionGeneration=generation,
81 sequence=frame.GetProperty("sequence").GetUInt64(),ttlMilliseconds=250,expiresAtUnixMilliseconds=deadline.ToUnixTimeMilliseconds()},token)).Clone());
82 }
83 await Task.Delay(10,token);
84 }
85 phase="original-context-release";await Release();until=DateTimeOffset.UtcNow.AddSeconds(2);
86 while(true)
87 {
88 var state=await Status();
89 if(Flag(state,"failed"))throw new InvalidDataException("Original Flight release failed: "+state.GetProperty("issue").GetString());
90 if(Flag(state,"inactiveObserved"))
91 {
92 if(Flag(state,"owned")||!Flag(state,"originalReleaseCalled")||!Flag(state,"explicitRelease")||!Flag(state,"stationaryContextQualified")
93 ||state.GetProperty("activeFlag").GetInt32()!=0||state.GetProperty("originalVm").GetString()!="0"
94 ||state.GetProperty("layerOccupancies").EnumerateArray().Any(v=>v.GetUInt64()!=0))
95 throw new InvalidDataException("Original Flight release did not restore the original inactive graph.");
96 foreach(var key in new[]{"accumulator","sourceValue","actualThrottle"})
97 if(!state.TryGetProperty(key,out var zero)||zero.ValueKind!=JsonValueKind.Number||Math.Abs(zero.GetDouble())>.001)
98 throw new InvalidDataException("No-seed admission changed a real throttle source.");
99 var controller=state.GetProperty("controllerThrottle").GetDouble();
100 if(!double.IsFinite(controller)||Math.Abs(controller)>.001&&controller!=-100)
101 throw new InvalidDataException("No-seed admission left an unexpected controller override.");
102 break;
103 }
104 if(DateTimeOffset.UtcNow>=until)throw new TimeoutException("Original inactive graph was not observed after release.");
105 await Task.Delay(10,token);
106 }
107 finalDock=await nav.CallAsync(new{op="commerce.docked.status",marketId=market},token);
108 passed=Flag(finalDock.Value,"ready")&&Flag(finalDock.Value,"serviceReady")&&Flag(finalDock.Value,"fresh")&&Flag(finalDock.Value,"healthy");
109 if(!passed)throw new InvalidDataException("No-seed probe did not finish at the original ready dock.");phase="passed-original-active-release-inactive";
110 }
111 catch(Exception error){failure=error;phase="failed-original-admission";}
112 finally
113 {
114 try{await Release();}catch(Exception cleanup){failure=failure is null?cleanup:new AggregateException(failure,cleanup);passed=false;}
115 await Save();
116 }
117 Console.WriteLine(JsonSerializer.Serialize(new{passed,phase,report,inputsSent=0,flightStarted=false}));
118 if(!passed)throw new InvalidOperationException("Original Flight admission did not pass; no flight or seed was attempted.",failure);
119 }
120 }
Modified research/NativeAcceptance/OriginalDockingResetAcceptance.cs +50 -10
Added research/NativeAcceptance/PersistentDockingThrottleAcceptance.cs +175 -0
Modified research/NativeAcceptance/Program.cs +5 -0
Modified research/NativeAcceptance/SessionRecoveryAcceptance.cs +9 -4
Added scripts/Build-DockingControl.ps1 +12 -0
Modified scripts/Build-ManagedPlugin.ps1 +1 -1
Modified scripts/Build-Plugin.ps1 +1 -0