XFEstudio/XFEEDToolBox
Preserve failed docking scenes while keeping damage exits armed
Separate the CG workflow's failed handoff exit policy from actual ship damage protection. A two-second handoff timeout still stops control and fails acceptance, while the game stays available for inspection. Add a recovery docking command with the same behavior and retain full read-only diagnostic failures. Advance the plugin and workflow pins to 0.5.12 for the docking action TTL fix. Record the background return-leg failure after the earlier visual success; require fresh continuous validation before another purchase. Validation: 12 protection regressions including real damage after a preserved handoff, 728 native self-tests with ABI/PE/export checks, 20 status and 3 lifecycle handshake replies, isolated CLI publish. New plugin installed; live recovery and repeat acceptance remain pending.
7ff9727
代码差异
@@ -17,6 +17,7 @@ public sealed class NativeFlightTestProtection:IAsyncDisposable
private readonly Action stopAutomation;
private readonly bool ownsMonitor;
private readonly bool automaticExit;
private readonly bool exitForFailedHandoff;
private readonly IGameSessionDriver sessionDriver;
private bool independentHealth;
private Task? polling;
@@ -27,9 +28,9 @@ public sealed class NativeFlightTestProtection:IAsyncDisposable
public string? LastIssue{get;private set;}
public bool Stopped=>Volatile.Read(ref unsafeState)!=0;
public bool Fresh=>!Stopped&&DateTimeOffset.UtcNow.UtcTicks-Interlocked.Read(ref lastHealthTicks)<=TimeSpan.FromMilliseconds(500).Ticks;
private NativeFlightTestProtection(int pid,GameSessionTarget target,IGameSessionDriver sessions,Func<PlayerState> player,Action stopAutomation,ShipProtectionMonitor? sharedMonitor,bool automaticExit)
private NativeFlightTestProtection(int pid,GameSessionTarget target,IGameSessionDriver sessions,Func<PlayerState> player,Action stopAutomation,ShipProtectionMonitor? sharedMonitor,bool automaticExit,bool exitForFailedHandoff)
{
pipe=new(pid);this.target=target;this.player=player;this.stopAutomation=stopAutomation;sessionDriver=sessions;this.automaticExit=automaticExit;
pipe=new(pid);this.target=target;this.player=player;this.stopAutomation=stopAutomation;sessionDriver=sessions;this.automaticExit=automaticExit;this.exitForFailedHandoff=exitForFailedHandoff;
ownsMonitor=sharedMonitor is null;
monitor=sharedMonitor??new(sessions,player,()=>{Interlocked.Exchange(ref unsafeState,1);StopAutomation();},automaticExit);
}
@@ -41,12 +42,12 @@ public sealed class NativeFlightTestProtection:IAsyncDisposable
Interlocked.Exchange(ref protection.unsafeState,1);
protection.LastIssue="自动降落未接管,飞船仍在移动;已停止操作并保护退出。";
protection.StopAutomation();
if(!protection.automaticExit){protection.LastIssue="人工观察模式:自动降落未接管,已停止自动输入,不退出游戏。";return;}
if(!protection.automaticExit||!protection.exitForFailedHandoff){protection.LastIssue="自动降落未接管,已停止自动输入并保留游戏现场。";return;}
// Do not restart into the same unresolved approach or let cancelling
// the navigation worker interrupt the already required protective exit.
await new GameSessionController(protection.sessionDriver,()=>false).EmergencyExitAsync(protection.target,CancellationToken.None);
}
public static async Task<NativeFlightTestProtection> ArmAsync(int pid,string hash,IGameSessionDriver sessions,Func<PlayerState> player,Action stopAutomation,CancellationToken token=default,ShipProtectionMonitor? sharedMonitor=null,bool automaticExit=true)
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)
{
if(Volatile.Read(ref active) is not null)throw new InvalidOperationException("已有飞行测试保护正在运行。");
var pipe=new NavigationPipeClient(pid);var hello=await pipe.CallAsync(new{op="describe"},token);
@@ -58,7 +59,7 @@ public sealed class NativeFlightTestProtection:IAsyncDisposable
var target=await sessions.FindTargetAsync(token);if(target.ProcessId!=pid)throw new InvalidOperationException("飞行测试与受保护游戏实例不一致。");
if(sharedMonitor is not null&&sharedMonitor.BoundTarget!=target)throw new InvalidOperationException("连续血量监测与受保护游戏实例不一致。");
if(sharedMonitor is not null&&sharedMonitor.AutomaticExitEnabled!=automaticExit)throw new InvalidOperationException("连续血量监测的退出模式不一致。");
var protection=new NativeFlightTestProtection(pid,target,sessions,player,stopAutomation,sharedMonitor,automaticExit);
var protection=new NativeFlightTestProtection(pid,target,sessions,player,stopAutomation,sharedMonitor,automaticExit,exitForFailedHandoff);
protection.independentHealth=hello.TryGetProperty("independentShipHealth",out var independent)&&independent.ValueKind==JsonValueKind.True;
try
{
@@ -147,6 +147,26 @@ public class NativeFlightTestProtectionTests
}
finally{stop.Cancel();await server;}
}
[Test] public async Task PreservedHandoffSceneStillExitsForSubsequentRealDamage()
{
var pid=Random.Shared.Next(300000000,310000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5));
var sessions=new Sessions();sessions.Target=sessions.Target with{ProcessId=pid};var hash=new string('a',64);var now=DateTimeOffset.UtcNow;var damaged=0;
var player=new PlayerState{CommanderId="F1",InGame=true,CurrentShipId=7,LoadGameAt=now.AddMinutes(-1),Ships=[new(){ShipId=7,HullHealth=1,HullHealthAt=now,ConfigurationAt=now}],
StatusAt=now,Status=new((1L<<24)|8,0,1,1,0,null,null,null,null,[])};
var server=ServeHealth(pid,hash,_=>99,stop.Token,_=>Volatile.Read(ref damaged)==0?1:.49,independent:true);
try
{
await using var protection=await NativeFlightTestProtection.ArmAsync(pid,hash,sessions,()=>player,()=>{},stop.Token,exitForFailedHandoff:false);
while(protection.LastHealth!.Sequence<3)await Task.Delay(20,stop.Token);
await NativeFlightTestProtection.ExitForFailedHandoffAsync(pid);
Check.That(protection.Stopped);Check.Throws<InvalidOperationException>(()=>NativeFlightTestProtection.CheckBeforeInput(pid));
Check.That(!sessions.Calls.Contains("exit")&&!sessions.Calls.Contains("launch"));
Interlocked.Exchange(ref damaged,1);
while(!sessions.Calls.Contains("exit"))await Task.Delay(20,stop.Token);
Check.Equal(1,sessions.Calls.Count(x=>x=="exit"));
}
finally{stop.Cancel();await server;}
}
[Test] public async Task ManualVisualDamageStopsInputsWithoutExitOrRestart()
{
var pid=Random.Shared.Next(290000000,300000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5));
@@ -20,6 +20,10 @@
此次加载0.5.11的新增停靠更新点 `dockingZeroUpdateMatched/Applied` 均为0,成功不能归因于它。离线发现该更新点误把选择确认的短TTL当成整个飞行目标的寿命;源码已改为保留目标身份、代次和控制所有权核验,以当前输入命令自己的有效期约束提交,回归覆盖过期确认、过期命令和变化目标。这项后续修正尚未更换到正在运行的游戏,仍需后续独立实测。故障根因与后台持续复现情况尚未确认。
恢复正式流程后的0.5.11返程再次出现实际油门.75、空管已授权而AI未接管,随后保护退出;没有新采购。单次人工观察通过不代表持续自动化稳定。0.5.12已包含上述有效期修正,728次原生自测、原ABI/导出及握手通过,已安装,实机复测仍在进行。
CG流程现在明确指定 `keepHandoffScene=true`:交接超时仍立即停止本程序输入,保留游戏现场用于诊断;与它分开的真实受损退出继续启用。新增回归证明交接保留现场后,后续真实受损仍触发退出;12项保护回归通过。通用保护接口的默认退出行为保持兼容,人工观察模式仍按用户要求同时关闭两类自动退出。
原始返回菜单路径未通过实机;只有插件明确声明 `nativeReturnToMenuVerified=true` 才允许该路径。从已就绪主菜单进入Solo使用独立入口能力。
后续空载连续航程再次完成起飞、跃迁、SCA和空管,但自动降落仍未接管并触发保护退出。实际飞行油门持续为.75,即便控制器零位及原始输入回执已确认。只读来源诊断确认FSD与自动降落使用同一个原始输入注册表,因此不能把来源差异当作已确认原因。可使用 `inspect-docking-input-sources <PID> <新报告>` 读取来源与原始零输入待消费/已消费状态;该命令仅使用QUERY_INFORMATION和VM_READ权限。
@@ -41,3 +45,5 @@
继续已有货物必须复用原目录。原始回执、钱包、会话身份及日志只存本机,不能提交到仓库。`continuousArrivalAccepted` 必须来自同一完整到站报告,不能用正常空间重试替代。未领取CG奖励不计入净利润。
空载诊断停靠可使用 `service-cg-docked <PID> <独立维护目录>`。它仅接受已核对路线站点、空舱及受保护的Corsair/5,不启动贸易或飞行;每笔原始服务费用记入独立账本,结尾必须与原始钱包核对。该路径已在恢复停靠后完成加油与完整性服务实测,诊断费用也应从总收益扣除。
`accept-local-docking-keep-scene <PID> <站名> <新报告路径>` 用于正常空间恢复停靠,保留真实受损保护,交接失败时停止输入并保留游戏。该报告要求 `requireCruise=false`,不能作为完整连续超巡交接的证明。
@@ -13,7 +13,7 @@ internal sealed class NativeAccessException : Exception
internal static unsafe class Platform
{
internal const string GameHash = "ed5a8f8cd10f9a80d3bda5badd7ba1d4fcfc15996c8c7e018852cb170e264b63";
internal const string PluginVersion = "0.5.11-dev";
internal const string PluginVersion = "0.5.12-dev";
internal static int ProcessId => Environment.ProcessId;
internal static nuint ImageBase => (nuint)GetModuleHandleW(null);
internal static nuint ImageEnd
@@ -125,7 +125,7 @@ internal static class CgCargoJourney
}
await Check(token);Console.WriteLine("原始超巡、空管与自动降落:"+target.Name);
await LocalNavigationAcceptance.RunAsync(pid,target.Name,Path.Combine(directory,"arrival-"+Guid.NewGuid().ToString("N")+".json"),
requireCruise:true,useNativeCruise:true,allowObscuration:true,externalToken:token);
requireCruise:true,useNativeCruise:true,allowObscuration:true,externalToken:token,keepHandoffScene:true);
if(!Player().Docked||Player().MarketId!=target.MarketId)throw new InvalidOperationException("Original docking differs from the CG route.");
}
var from=new Station{MarketId=3228792064,Name="Polyakov Station",SystemName="Duamta"};
@@ -24,8 +24,8 @@ internal static class CgTradeLoop
if(!initial.InGame||initial.CurrentShipId!=5||initial.CargoCapacity!=192||string.IsNullOrWhiteSpace(initial.CommanderId))
throw new InvalidOperationException("The verified CG ship and live session are required.");
var channel=new NativeCommerceChannel(pid);var hello=await channel.CallAsync(new{op="describe"},stop.Token);
if(hello.GetProperty("gameSha256").GetString()!=NativeSystemLocationReader.GameSha256||hello.GetProperty("version").GetString()!="0.5.11-dev")
throw new InvalidOperationException("This continuous workflow requires the verified 0.5.11 docking handoff implementation.");
if(hello.GetProperty("gameSha256").GetString()!=NativeSystemLocationReader.GameSha256||hello.GetProperty("version").GetString()!="0.5.12-dev")
throw new InvalidOperationException("This continuous workflow requires the verified 0.5.12 docking handoff implementation.");
decimal initialWallet;
using(var first=JsonDocument.Parse(await File.ReadAllTextAsync(Path.Combine(workRoot,"one-ton-journey","result.json"),stop.Token)))
{
@@ -83,7 +83,7 @@ internal static class CgTradeLoop
if(!journal.State.SystemName.Equals(target.SystemName,StringComparison.OrdinalIgnoreCase))
{await State("interstellar");await InterstellarTravelAcceptance.RunAsync(pid,target.SystemName,prefix+"-route.json",stop.Token);}
await Check(stop.Token);await State("cruise-and-docking");
await LocalNavigationAcceptance.RunAsync(pid,target.Name,prefix+"-arrival.json",true,true,true,stop.Token);
await LocalNavigationAcceptance.RunAsync(pid,target.Name,prefix+"-arrival.json",true,true,true,stop.Token,keepHandoffScene:true);
if(!journal.State.Docked||journal.State.MarketId!=target.MarketId)throw new InvalidOperationException("Actual docking differs from the trade route.");
}
async Task Membership()
@@ -206,17 +206,17 @@ internal static class CgTradeLoop
// validate a new complete leg to the opposite route station.
if(journal.State.Docked&&journal.State.MarketId==target.MarketId)
target=target.MarketId==Homer.MarketId?Source:Homer;
await File.WriteAllTextAsync(planPath+".tmp",JsonSerializer.Serialize(new{marketId=target.MarketId,version="0.5.11-dev"}),stop.Token);
await File.WriteAllTextAsync(planPath+".tmp",JsonSerializer.Serialize(new{marketId=target.MarketId,version="0.5.12-dev"}),stop.Token);
File.Move(planPath+".tmp",planPath,true);
var prefix=Path.Combine(directory,"validation-"+Guid.NewGuid().ToString("N"));
await State("empty-handoff-validation");await Travel(target,prefix);
using var report=JsonDocument.Parse(await File.ReadAllTextAsync(prefix+"-arrival.json",stop.Token));var r=report.RootElement;
if(!r.GetProperty("passed").GetBoolean()||!r.GetProperty("cruiseEngaged").GetBoolean()||!r.GetProperty("dockingGranted").GetBoolean()||!r.GetProperty("dockingEngaged").GetBoolean())
throw new InvalidDataException("Continuous original cruise/ATC/docking proof is required before new purchases.");
await File.WriteAllTextAsync(proof,JsonSerializer.Serialize(new{version="0.5.11-dev",report=prefix+"-arrival.json",passed=true}),stop.Token);
await File.WriteAllTextAsync(proof,JsonSerializer.Serialize(new{version="0.5.12-dev",report=prefix+"-arrival.json",passed=true}),stop.Token);
await Maintain("validation-after-"+Guid.NewGuid().ToString("N"));await FinancialSummary();
}
else{using var d=JsonDocument.Parse(await File.ReadAllTextAsync(proof,stop.Token));if(!d.RootElement.GetProperty("passed").GetBoolean()||d.RootElement.GetProperty("version").GetString()!="0.5.11-dev")throw new InvalidDataException("Continuous proof belongs to a different implementation.");}
else{using var d=JsonDocument.Parse(await File.ReadAllTextAsync(proof,stop.Token));if(!d.RootElement.GetProperty("passed").GetBoolean()||d.RootElement.GetProperty("version").GetString()!="0.5.12-dev")throw new InvalidDataException("Continuous proof belongs to a different implementation.");}
while(true)
{
await Check(stop.Token);activeJourney=Unfinished();
@@ -7,7 +7,7 @@ using XFEEDToolBox.Services;
internal sealed class FlightTestProtectionScope(JournalMonitor journal,NativeFlightTestProtection protection):IAsyncDisposable
{
public bool Stopped=>protection.Stopped;
public static async Task<FlightTestProtectionScope> ArmAsync(int pid,bool automaticExit=true)
public static async Task<FlightTestProtectionScope> ArmAsync(int pid,bool automaticExit=true,bool exitForFailedHandoff=true)
{
if(!OperatingSystem.IsWindows())throw new PlatformNotSupportedException("原生飞行验收仅支持 Windows。");
var settings=ToolboxSettings.Load(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"XFEstudio","XFEEDToolBox","settings.json"));
@@ -17,7 +17,7 @@ internal sealed class FlightTestProtectionScope(JournalMonitor journal,NativeFli
await journal.StartAsync();
var hello=await new NavigationPipeClient(pid).CallAsync(new{op="describe"});
var protection=await NativeFlightTestProtection.ArmAsync(pid,hello.GetProperty("gameSha256").GetString()!,
new WindowsGameSessionDriver(()=>journal.State,()=>settings.PluginGameExecutable),()=>journal.State,()=>{},automaticExit:automaticExit);
new WindowsGameSessionDriver(()=>journal.State,()=>settings.PluginGameExecutable),()=>journal.State,()=>{},automaticExit:automaticExit,exitForFailedHandoff:exitForFailedHandoff);
journal.JournalEvent+=protection.ObserveJournal;
return new(journal,protection);
}
@@ -6,7 +6,7 @@ using XFEEDToolBox.Core.Navigation;
internal static class LocalNavigationAcceptance
{
public static async Task RunAsync(int pid,string exactName,string reportPath,bool requireCruise=true,bool useNativeCruise=true,bool allowObscuration=false,CancellationToken externalToken=default,bool manualVisual=false)
public static async Task RunAsync(int pid,string exactName,string reportPath,bool requireCruise=true,bool useNativeCruise=true,bool allowObscuration=false,CancellationToken externalToken=default,bool manualVisual=false,bool keepHandoffScene=false)
{
if(File.Exists(reportPath)||File.Exists(reportPath+".jsonl"))throw new IOException("Use a new report path.");
var settings=ToolboxSettings.Load(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"XFEstudio","XFEEDToolBox","settings.json"));
@@ -28,7 +28,7 @@ internal static class LocalNavigationAcceptance
{
if(protection?.Stopped!=true)return;Session();var nav=journal.State.Navigation;
if(!(nav.SupercruiseEnteredAt>=start.AddSeconds(-1)||nav.SupercruiseExitedAt>=start.AddSeconds(-1)))throw new InvalidOperationException("Unexpected entity change during native-assist navigation.");
await protection.DisposeAsync();protection=await FlightTestProtectionScope.ArmAsync(pid,!manualVisual);
await protection.DisposeAsync();protection=await FlightTestProtectionScope.ArmAsync(pid,!manualVisual,!keepHandoffScene);
}
var driver=new NativeSystemFlightControlDriver(pid,hash,()=>journal.State,new NativeGameInputChannel(pid,hash,Path.GetFullPath("XFEEDToolBox/PluginPackage")),EnsureProtection);
var policy=new SystemFlightNavigator();var arrived=false;var released=false;var cruiseEngaged=false;var dockingGranted=false;var dockingEngaged=false;string? failure=null;
@@ -40,7 +40,7 @@ internal static class LocalNavigationAcceptance
async Task Record(object value){await log.WriteLineAsync(JsonSerializer.Serialize(value));await log.FlushAsync();}
try
{
protection=await FlightTestProtectionScope.ArmAsync(pid,!manualVisual);using var stop=CancellationTokenSource.CreateLinkedTokenSource(externalToken);stop.CancelAfter(TimeSpan.FromMinutes(10));
protection=await FlightTestProtectionScope.ArmAsync(pid,!manualVisual,!keepHandoffScene);using var stop=CancellationTokenSource.CreateLinkedTokenSource(externalToken);stop.CancelAfter(TimeSpan.FromMinutes(10));
await NavigationRecovery.ReadWithRecoveryAsync(async token=>{await driver.SelectAsync(target,token);return true;},null,stop.Token);
await driver.PrepareAsync(stop.Token);
while(true)
@@ -102,11 +102,11 @@ internal static class LocalNavigationAcceptance
{
if(decision.Actions.HasFlag(SystemNavigationAction.ExitForFailedHandoff))
{
await Record(new{at=DateTimeOffset.UtcNow,protectiveExit=!manualVisual,manualVisualHandoffStop=manualVisual,frame,decision});
await Record(new{at=DateTimeOffset.UtcNow,protectiveExit=!(manualVisual||keepHandoffScene),manualVisualHandoffStop=manualVisual,keepHandoffScene,frame,decision});
await driver.ApplySystemFlightAsync(decision,frame,target,frame.ObservedAt.AddMilliseconds(250),stop.Token);
}
throw new InvalidOperationException(manualVisual&&decision.Actions.HasFlag(SystemNavigationAction.ExitForFailedHandoff)
?"人工观察模式:交接未完成,已停止自动输入,游戏保持运行。":decision.Message);
throw new InvalidOperationException((manualVisual||keepHandoffScene)&&decision.Actions.HasFlag(SystemNavigationAction.ExitForFailedHandoff)
?"交接未完成,已停止自动输入,游戏保持运行。":decision.Message);
}
if(useNativeCruise&&frame.Mode==LocalFlightMode.Supercruise&&decision.Actions.HasFlag(SystemNavigationAction.CancelOwnedAssists)&&!(allowObscuration&&frame.TargetObscured))throw new InvalidOperationException("Native assist did not engage; software fallback is a separate acceptance case.");
if(lastPhase!=decision.Phase){Console.WriteLine(JsonSerializer.Serialize(new{at=DateTimeOffset.UtcNow,phase=decision.Phase.ToString(),decision.Message,frame.TargetDistance,frame.Speed}));lastPhase=decision.Phase;}
@@ -120,7 +120,7 @@ internal static class LocalNavigationAcceptance
{
object provenance;
try{provenance=await NativeDockingInputDiagnostics.ReadAsync(pid,diagnosticToken);}
catch(Exception error){provenance=new{failure=error.Message,processAccess="QUERY_INFORMATION | VM_READ",mutationsSent=0};}
catch(Exception error){provenance=new{failure=error.ToString(),processAccess="QUERY_INFORMATION | VM_READ",mutationsSent=0};}
await File.WriteAllTextAsync(reportPath+".input-provenance.json",JsonSerializer.Serialize(provenance,ToolboxSettings.JsonOptions));
});
}
@@ -146,7 +146,7 @@ internal static class LocalNavigationAcceptance
if(dockingDiagnostics is not null)try{await dockingDiagnostics.WaitAsync(TimeSpan.FromSeconds(3));}catch{}
}
var passed=arrived&&released&&failure is null;
await File.WriteAllTextAsync(reportPath,JsonSerializer.Serialize(new{pid,start,finished=DateTimeOffset.UtcNow,passed,arrived,released,manualVisual,automaticExitEnabled=!manualVisual,requireCruise,useNativeCruise,cruiseObserved,safeDropRequested,cruiseEngaged,dockingGranted,dockingEngaged,inputs,maxHeat,closest,
await File.WriteAllTextAsync(reportPath,JsonSerializer.Serialize(new{pid,start,finished=DateTimeOffset.UtcNow,passed,arrived,released,manualVisual,keepHandoffScene,automaticExitEnabled=!manualVisual,handoffExitEnabled=!(manualVisual||keepHandoffScene),requireCruise,useNativeCruise,cruiseObserved,safeDropRequested,cruiseEngaged,dockingGranted,dockingEngaged,inputs,maxHeat,closest,
fullThrottleApproachSamples,peakNormalSpeedFraction,firstAtcSpeed,firstNativeLandingSpeed,
allowObscuration,obscuredSamples,nativeObscuredSamples,bypassFullThrottleSamples,bypassCleared,clearThrottleZeroSamples,nativeCruiseAfterBypass,nativeCruiseEnabledSamples,cruiseActivationRequests,
finalMarketId=journal.State.MarketId,finalDocked=journal.State.Docked,failure,tradeOrdersSent=0,completeNavigationAccepted=false},ToolboxSettings.JsonOptions));
@@ -19,7 +19,7 @@ internal static class ManualVisualHandoff
if(!NavigationAssists.From(state).AutoDock||!NavigationAssists.From(state).Supercruise)
throw new InvalidOperationException("当前装备不满足原生辅助交接观察。");
var pipe=new NavigationPipeClient(pid);var hello=await pipe.CallAsync(new{op="describe"});
if(hello.GetProperty("gameSha256").GetString()!=NativeSystemLocationReader.GameSha256||hello.GetProperty("version").GetString()!="0.5.11-dev")
if(hello.GetProperty("gameSha256").GetString()!=NativeSystemLocationReader.GameSha256||hello.GetProperty("version").GetString()!="0.5.12-dev")
throw new InvalidDataException("人工观察所绑定的游戏构建或插件候选版本已改变。");
var health=await pipe.CallAsync(new{op="health.read"});var h=health.GetProperty("shipHealth");
var at=DateTimeOffset.FromUnixTimeMilliseconds(h.GetProperty("observedAtUnixMilliseconds").GetInt64());
@@ -176,6 +176,8 @@ if(args.Length==4&&args[0]=="accept-local-software-cruise")
{await LocalNavigationAcceptance.RunAsync(int.Parse(args[1]),args[2],args[3],true,false);return;}
if(args.Length==4&&args[0]=="accept-local-docking")
{await LocalNavigationAcceptance.RunAsync(int.Parse(args[1]),args[2],args[3],false);return;}
if(args.Length==4&&args[0]=="accept-local-docking-keep-scene")
{await LocalNavigationAcceptance.RunAsync(int.Parse(args[1]),args[2],args[3],requireCruise:false,keepHandoffScene:true);return;}
if(args.Length==4&&args[0]=="accept-local-launch")
{await LocalLaunchAcceptance.RunAsync(int.Parse(args[1]),args[2],args[3]);return;}
if(args.Length==4&&args[0]=="accept-local-launch-cancel")