using System.Diagnostics;
using System.Security.Cryptography;
using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Plugins;
using XFEEDToolBox.Core.Session;
using XFEEDToolBox.NativeSupport;
using XFEEDToolBox.Services;
/// <summary>Explicitly requested recovery of the current non-playing client; never trades or flies.</summary>
internal static class RecoverCurrentSession
{
private sealed record Stage(string Name,DateTimeOffset At,string? Detail=null);
private sealed record StationarySample(string Window,DateTimeOffset At,int ProcessId,long ProcessStartedUtcTicks,long Sequence,long ObservedAtMilliseconds,
string EntityId,string ReferenceWorld,long JournalShipId,double Hull,double? Shield,double Speed,bool OriginalEmergency,JsonElement Raw);
internal static async Task RunAsync(string executable,string report,bool updateStationaryShip=false,bool updateDockedShip=false)
{
executable=Path.GetFullPath(executable);report=Path.GetFullPath(report);
if(File.Exists(report)||File.Exists(report+".entry.json"))throw new IOException("Use a new current-session recovery report.");
Directory.CreateDirectory(Path.GetDirectoryName(report)!);
var stages=new List<Stage>();
GameSessionTarget? initialTarget=null,replacement=null;
PlayerState? initialPlayer=null,finalPlayer=null;
JsonElement? initialNativeStatus=null,beforeTerminationStatus=null,finalNativeStatus=null;
JsonElement? initialInventory=null,beforeTerminationInventory=null,finalInventory=null;
var stationarySamples=new List<StationarySample>();
PluginPackage? bundle=null;PluginStatus? previousPlugin=null,installedPlugin=null;
PluginHandshake? finalPlugin=null;
string? packageDirectory=null;Exception? failure=null;
var terminated=false;var oldProcessExited=false;var installed=false;var entered=false;var passed=false;
bool? cargoUnchanged=null;
bool? walletUnchanged=null;
DockedPluginMaintenanceProof? maintenanceProof=null;DockedMaintenanceJournalWatch? maintenanceWatch=null;
object? Snapshot(PlayerState? value)=>value is null?null:new
{
value.CommanderId,value.GameMode,value.LoadGameAt,value.CurrentShipId,value.Ship,
value.CargoCount,value.CargoReliable,value.CargoAt,value.Inventory,value.InGame,
value.Docked,value.SystemName,value.StationName,value.MarketId,value.CargoCapacity,value.Credits,value.CreditsReliable
};
async Task Save(string phase)
{
var temporary=report+".tmp";
await File.WriteAllTextAsync(temporary,JsonSerializer.Serialize(new
{
schema=1,at=DateTimeOffset.UtcNow,phase,passed,executable,packageDirectory,
initialTarget,replacement,initial=Snapshot(initialPlayer),final=Snapshot(finalPlayer),
initialNativeStatus,beforeTerminationStatus,finalNativeStatus,previousPlugin,installedPlugin,
package=bundle,finalPlugin,terminated,oldProcessExited,installed,entered,cargoUnchanged,walletUnchanged,
entryReport=report+".entry.json",stages,failure=failure?.ToString(),updateDockedShip,updateStationaryShip,
dockedMaintenanceSamples=maintenanceProof?.Samples,stationarySamples,initialInventory,beforeTerminationInventory,finalInventory,legacyEmergencyCleared=false,
flightInputsSent=0,tradeOrdersSent=0,screenUse=false,osInputsSent=0
},ToolboxSettings.JsonOptions));
File.Move(temporary,report,true);
}
async Task Record(string phase,string? detail=null)
{stages.Add(new(phase,DateTimeOffset.UtcNow,detail));await Save(phase);Console.WriteLine("恢复当前会话:"+phase);}
static async Task<string> Hash(string path,CancellationToken token)
{
await using var stream=new FileStream(path,FileMode.Open,FileAccess.Read,FileShare.ReadWrite|FileShare.Delete);
return Convert.ToHexStringLower(await SHA256.HashDataAsync(stream,token));
}
static void VerifyNative(JsonElement hello,JsonElement status,GameSessionTarget target,string hash,bool expectInGame)
{
var now=DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
if(hello.GetProperty("processId").GetInt32()!=target.ProcessId||hello.GetProperty("gameSha256").GetString()!=hash
||status.GetProperty("schema").GetInt32()!=1||status.GetProperty("name").GetString()!="XFEEDToolBox.Session"
||status.GetProperty("processId").GetInt32()!=target.ProcessId||status.GetProperty("gameSha256").GetString()!=hash
||status.GetProperty("sequence").GetInt64()<=0)
throw new InvalidDataException("Original session identity is not the selected game process.");
var observed=status.GetProperty("observedAtUnixMilliseconds").GetInt64();
var state=status.GetProperty("state").GetString();var next=status.GetProperty("nextState").GetString();
if(now-observed>350||observed>now+100||string.IsNullOrEmpty(state)||string.IsNullOrEmpty(next))
throw new IOException("Original session state is unavailable or stale; recovery did not infer the current screen.");
if(expectInGame)
{
if(!hello.GetProperty("inGame").GetBoolean()||state!="InGame"||next!="InGame")
throw new IOException("The replacement client has not confirmed a stable original InGame state.");
}
else if(hello.GetProperty("inGame").GetBoolean()||state=="InGame"||next=="InGame")
throw new InvalidOperationException("The client has reached a playing state; current-session recovery was not restarted.");
}
try
{
await Record("requested");
if(updateDockedShip&&updateStationaryShip)throw new ArgumentException("Use exactly one independently verified maintenance mode.");
using var timeout=new CancellationTokenSource(TimeSpan.FromMinutes(10));var token=timeout.Token;
var settings=ToolboxSettings.Load(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"XFEstudio","XFEEDToolBox","settings.json"));
await using var journal=new JournalMonitor(settings.JournalDirectory);await journal.StartAsync(token);
if(updateDockedShip)maintenanceProof=new();
if(updateDockedShip||updateStationaryShip)maintenanceWatch=new(journal);
initialPlayer=journal.State;
if(!NativeRelogWorkflow.SupportsMode(initialPlayer.GameMode)||string.IsNullOrWhiteSpace(initialPlayer.CommanderId)||initialPlayer.LoadGameAt is null)
throw new InvalidOperationException("A previously observed commander and Solo/Open session are required for original mode restoration.");
var driver=new WindowsGameSessionDriver(()=>journal.State,()=>executable);
initialTarget=await driver.FindLaunchTargetAsync(token);
if(initialTarget.ProcessId<=0)throw new InvalidOperationException("There is no unique current game process to recover.");
await driver.ValidateRestartAsync(initialTarget,token);
var gameHash=await Hash(executable,token);
if(gameHash!=NativeRelogChannel.SupportedGameSha256)throw new InvalidDataException("The current game build has no verified original session interface.");
var pipe=new NavigationPipeClient(initialTarget.ProcessId);
async Task<JsonElement> ReadInventoryAsync(NavigationPipeClient currentPipe,PlayerState current)
{
var raw=await currentPipe.CallAsync(new{op="read"},token);
var inventory=await currentPipe.CallAsync(new{op="commerce.inventory"},token);
var after=await currentPipe.CallAsync(new{op="read"},token);
if(!inventory.GetProperty("verified").GetBoolean()||inventory.GetProperty("entityId").GetString()!=raw.GetProperty("entityId").GetString()
||after.GetProperty("entityId").GetString()!=raw.GetProperty("entityId").GetString()
||after.GetProperty("referenceWorldAddress").GetString()!=raw.GetProperty("referenceWorldAddress").GetString()
||!current.CargoReliable||inventory.GetProperty("count").GetInt32()!=current.CargoCount||inventory.GetProperty("capacity").GetInt32()!=current.CargoCapacity
||!inventory.GetProperty("credits").TryGetInt64(out var credits)||credits<0)
throw new InvalidDataException("Stationary maintenance cannot verify the actual main-ship cargo and wallet.");
return inventory.Clone();
}
async Task VerifyStationaryWindowAsync(string window,bool requireStableWindow=true)
{
if(maintenanceWatch is null)throw new InvalidOperationException("Stationary maintenance was not explicitly requested.");
await VerifyStationaryShip(pipe,initialTarget,initialPlayer,()=>journal.State,stationarySamples,
driver.FindLaunchTargetAsync,maintenanceWatch.RequireUnchanged,window,token,requireStableWindow);
}
async Task VerifyDockedSampleAsync()
{
if(maintenanceProof is null||maintenanceWatch is null)throw new InvalidOperationException("Docked maintenance proof was not explicitly requested.");
token.ThrowIfCancellationRequested();maintenanceWatch.RequireUnchanged();
if(await driver.FindLaunchTargetAsync(token)!=initialTarget)throw new IOException("Docked maintenance process identity changed.");
var currentHello=await pipe.CallAsync(new{op="describe"},token);
var session=await GameSessionAcceptance.StatusAsync(initialTarget.ProcessId,token);
VerifyNative(currentHello,session,initialTarget,gameHash,expectInGame:true);
var before=await pipe.CallAsync(new{op="read"},token);var current=journal.State;
if(current.MarketId is not >0 or >=uint.MaxValue)throw new InvalidOperationException("Docked maintenance requires the actual current station market.");
var docked=await pipe.CallAsync(new{op="commerce.docked.status",marketId=(uint)current.MarketId.Value},token);
var raw=await pipe.CallAsync(new{op="read"},token);
if(before.GetProperty("entityId").GetString()!=raw.GetProperty("entityId").GetString()
||before.GetProperty("referenceWorldAddress").GetString()!=raw.GetProperty("referenceWorldAddress").GetString()
||before.GetProperty("sequence").GetInt64()>raw.GetProperty("sequence").GetInt64()
||before.GetProperty("observedAtUnixMilliseconds").GetInt64()>raw.GetProperty("observedAtUnixMilliseconds").GetInt64())
throw new IOException("The main ship changed around the original docking read.");
maintenanceWatch.RequireUnchanged();
maintenanceProof.Accept(journal.State,session,raw,docked,DateTimeOffset.UtcNow);
}
async Task VerifyDockedWindowAsync(bool initial)
{
var window=Stopwatch.StartNew();var beforeCount=maintenanceProof!.Samples.Count;
do
{
await VerifyDockedSampleAsync();
if(initial?maintenanceProof.Ready:maintenanceProof.Samples.Count-beforeCount>=3&&window.Elapsed>=TimeSpan.FromMilliseconds(250))break;
if(window.Elapsed>TimeSpan.FromSeconds(5))throw new TimeoutException("No bounded sequence of fresh stationary docked samples was confirmed.");
await Task.Delay(120,token);
}while(true);
await Record(initial?"independent-docked-maintenance-baseline-verified":"independent-docked-maintenance-rechecked",
"Original emergency values retained; no native health flag, flight input, menu or trade state was changed.");
}
var hello=await pipe.CallAsync(new{op="describe"},token);
initialNativeStatus=await GameSessionAcceptance.StatusAsync(initialTarget.ProcessId,token);
var initialPlaying=(updateStationaryShip||updateDockedShip)&&initialNativeStatus.Value.GetProperty("state").GetString()=="InGame"&&initialNativeStatus.Value.GetProperty("nextState").GetString()=="InGame";
if(updateDockedShip&&!initialPlaying)throw new InvalidOperationException("Docked maintenance requires stable InGame; Pause/ExitOptions cannot use non-playing recovery.");
if(updateStationaryShip&&!initialPlaying)throw new InvalidOperationException("Stationary maintenance requires stable InGame; Pause/ExitOptions cannot use non-playing recovery.");
VerifyNative(hello,initialNativeStatus.Value,initialTarget,gameHash,expectInGame:initialPlaying);
if(updateDockedShip)await VerifyDockedWindowAsync(initial:true);
else if(initialPlaying)
{
initialInventory=await ReadInventoryAsync(pipe,initialPlayer);
await VerifyStationaryWindowAsync("initial");
}
await Record(updateDockedShip?"current-process-and-docked-maintenance-verified":initialPlaying?"current-process-and-stationary-maintenance-verified":"current-process-and-nonplaying-session-verified");
packageDirectory=NativeSupportPackage.Directory;
var manager=new GamePluginManager(packageDirectory);bundle=manager.Package();
previousPlugin=await manager.InspectAsync(executable,token);
if(!previousPlugin.Installed||previousPlugin.PendingRemoval||previousPlugin.Installation is null)
throw new InvalidOperationException("Recovery updates only the existing toolbox-owned active plugin installation.");
if(bundle.SupportedGameSha256!=gameHash||await Hash(Path.Combine(packageDirectory,bundle.Loader),token)!=bundle.Sha256)
throw new InvalidDataException("The embedded replacement package failed validation; the game was left running.");
await Record("embedded-package-and-owned-installation-verified");
if(await driver.FindLaunchTargetAsync(token)!=initialTarget)throw new IOException("The current game process changed before recovery.");
await driver.ValidateRestartAsync(initialTarget,token);
hello=await pipe.CallAsync(new{op="describe"},token);
beforeTerminationStatus=await GameSessionAcceptance.StatusAsync(initialTarget.ProcessId,token);
var playingBefore=(updateStationaryShip||updateDockedShip)&&beforeTerminationStatus.Value.GetProperty("state").GetString()=="InGame"&&beforeTerminationStatus.Value.GetProperty("nextState").GetString()=="InGame";
if(updateDockedShip&&!playingBefore)throw new InvalidOperationException("Docked maintenance cannot fall back to a non-playing termination path.");
if(updateStationaryShip&&!playingBefore)throw new InvalidOperationException("Stationary maintenance cannot fall back to a non-playing termination path.");
VerifyNative(hello,beforeTerminationStatus.Value,initialTarget,gameHash,expectInGame:playingBefore);
if(updateDockedShip)await VerifyDockedWindowAsync(initial:false);
else if(playingBefore)
{
beforeTerminationInventory=await ReadInventoryAsync(pipe,journal.State);
if(initialInventory is null||!SameInventory(initialInventory.Value,beforeTerminationInventory.Value))
throw new IOException("The actual cargo or wallet changed before stationary maintenance termination.");
await VerifyStationaryWindowAsync("before-termination");
}
await Record("before-verified-process-termination");
if(updateDockedShip)
{
// Disk serialization must not create an unchecked gap before
// termination. Obtain one last different original frame.
await Task.Delay(30,token);await VerifyDockedSampleAsync();
}
else if(playingBefore)
{
await VerifyStationaryWindowAsync("final-before-termination",requireStableWindow:false);
beforeTerminationInventory=await ReadInventoryAsync(pipe,journal.State);
if(initialInventory is null||!SameInventory(initialInventory.Value,beforeTerminationInventory.Value))
throw new IOException("Actual cargo or wallet changed immediately before stationary maintenance termination.");
}
maintenanceWatch?.RequireUnchanged();
await driver.TerminateAsync(initialTarget,token);terminated=true;
await Record("termination-requested");
var exitWatch=Stopwatch.StartNew();
while(!await driver.HasExitedAsync(initialTarget,token))
{
if(exitWatch.Elapsed>TimeSpan.FromSeconds(20))throw new TimeoutException("The verified previous client did not exit.");
await Task.Delay(100,token);
}
oldProcessExited=true;await Record("previous-process-exit-confirmed");
installedPlugin=await manager.InstallAsync(executable,token);
if(!installedPlugin.Installed||installedPlugin.PendingRemoval||installedPlugin.Installation?.LoaderSha256!=bundle.Sha256)
throw new InvalidDataException("The replacement plugin installation has not matched its embedded package.");
installed=true;await Record("embedded-plugin-installed");
await Record("official-launch-and-original-mode-entry-requested",initialPlayer.GameMode);
await GameSessionAcceptance.StartAsync(executable,report+".entry.json");
entered=true;
replacement=await driver.FindLaunchTargetAsync(token);
if(replacement.ProcessId<=0||replacement.StartedUtcTicks<=initialTarget.StartedUtcTicks
||!string.Equals(replacement.Executable,initialTarget.Executable,StringComparison.OrdinalIgnoreCase))
throw new IOException("A new same-installation game process has not been confirmed.");
await driver.ValidateRestartAsync(replacement,token);
var replacementPipe=new NavigationPipeClient(replacement.ProcessId);
hello=await replacementPipe.CallAsync(new{op="describe"},token);
finalNativeStatus=await GameSessionAcceptance.StatusAsync(replacement.ProcessId,token);
VerifyNative(hello,finalNativeStatus.Value,replacement,gameHash,expectInGame:true);
finalPlugin=await GamePluginManager.ProbeAsync(replacement.ProcessId,token:token);
if(!finalPlugin.Loaded||!finalPlugin.Active||!finalPlugin.ObservationStarted||finalPlugin.Version!=bundle.Version)
throw new IOException("The new client has not confirmed the installed plugin and original observation service.");
var journalUntil=DateTimeOffset.UtcNow.AddSeconds(5);
do
{
finalPlayer=journal.State;
if(finalPlayer.InGame&&finalPlayer.LoadGameAt>initialPlayer.LoadGameAt&&(!(updateDockedShip||updateStationaryShip)
||finalPlayer.CargoReliable&&finalPlayer.CreditsReliable&&(!updateDockedShip||finalPlayer.Docked&&finalPlayer.MarketId==initialPlayer.MarketId)))break;
await Task.Delay(100,token);
}while(DateTimeOffset.UtcNow<journalUntil);
if(finalPlayer is null||!finalPlayer.InGame||finalPlayer.LoadGameAt<=initialPlayer.LoadGameAt
||finalPlayer.CommanderId!=initialPlayer.CommanderId||finalPlayer.GameMode!=initialPlayer.GameMode
||initialPlayer.CurrentShipId is {} ship&&finalPlayer.CurrentShipId!=ship)
throw new IOException("The new LoadGame has not confirmed the same commander, ship and original mode.");
static InventoryItem[] Cargo(PlayerState value)=>value.Inventory.OrderBy(row=>row.Key,StringComparer.Ordinal).ThenBy(row=>row.MissionId).ThenBy(row=>row.Stolen).ToArray();
if(initialPlayer.CargoReliable&&finalPlayer.CargoReliable)
cargoUnchanged=initialPlayer.CargoCount==finalPlayer.CargoCount&&Cargo(initialPlayer).SequenceEqual(Cargo(finalPlayer));
if(cargoUnchanged==false)throw new IOException("The session re-entered, but cargo differs from the recorded starting snapshot; no trade or cargo operation was submitted.");
if(updateDockedShip&&(!finalPlayer.CargoReliable||cargoUnchanged!=true||!finalPlayer.Docked||finalPlayer.MarketId!=initialPlayer.MarketId
||finalPlayer.SystemName!=initialPlayer.SystemName||finalPlayer.StationName!=initialPlayer.StationName
||finalPlayer.CargoCapacity!=initialPlayer.CargoCapacity||finalPlayer.Credits!=initialPlayer.Credits))
throw new IOException("The docked maintenance return has not confirmed unchanged station, cargo, ship capacity and recorded wallet.");
if(updateStationaryShip)
{
finalInventory=await ReadInventoryAsync(replacementPipe,finalPlayer);
walletUnchanged=initialInventory is {} original&&finalInventory.Value.GetProperty("credits").GetInt64()==original.GetProperty("credits").GetInt64();
if(initialInventory is null||walletUnchanged!=true||cargoUnchanged!=true||!SameInventory(initialInventory.Value,finalInventory.Value)
||finalPlayer.CargoCapacity!=initialPlayer.CargoCapacity||finalPlayer.Docked!=initialPlayer.Docked
||finalPlayer.SystemName!=initialPlayer.SystemName||initialPlayer.Docked&&finalPlayer.MarketId!=initialPlayer.MarketId)
throw new IOException("The stationary maintenance return has not confirmed unchanged actual cargo, wallet, ship capacity and location.");
using var returnedWatch=new DockedMaintenanceJournalWatch(journal);
await VerifyStationaryShip(replacementPipe,replacement,finalPlayer,()=>journal.State,stationarySamples,
driver.FindLaunchTargetAsync,returnedWatch.RequireUnchanged,"after-restart",token);
}
passed=true;await Record("same-commander-original-mode-and-plugin-confirmed");
}
catch(Exception error){failure=error;}
finally{maintenanceWatch?.Dispose();await Save(failure is null?"finished":"failed");}
Console.WriteLine(JsonSerializer.Serialize(new{passed,report,terminated,installed,entered,cargoUnchanged,replacement,failure=failure?.Message},ToolboxSettings.JsonOptions));
if(failure is not null)throw new InvalidOperationException("Current-session recovery did not fully verify completion; inspect "+report,failure);
}
private static bool SameInventory(JsonElement before,JsonElement after)
=>before.GetProperty("verified").GetBoolean()&&after.GetProperty("verified").GetBoolean()
&&before.GetProperty("count").GetInt32()==after.GetProperty("count").GetInt32()
&&before.GetProperty("capacity").GetInt32()==after.GetProperty("capacity").GetInt32()
&&before.GetProperty("credits").GetInt64()==after.GetProperty("credits").GetInt64();
private static async Task VerifyStationaryShip(NavigationPipeClient pipe,GameSessionTarget target,PlayerState baseline,Func<PlayerState> player,
List<StationarySample> samples,Func<CancellationToken,Task<GameSessionTarget>> findTarget,Action requireJournalUnchanged,string window,
CancellationToken token,bool requireStableWindow=true)
{
static InventoryItem[] Cargo(PlayerState value)=>value.Inventory.OrderBy(row=>row.Key,StringComparer.Ordinal).ThenBy(row=>row.MissionId).ThenBy(row=>row.Stolen).ToArray();
var clock=Stopwatch.StartNew();var start=samples.Count;long? windowAt=null;
do
{
token.ThrowIfCancellationRequested();requireJournalUnchanged();
if(await findTarget(token)!=target)throw new IOException("The verified stationary maintenance process changed.");
var current=player();
if(!baseline.InGame||baseline.CurrentShipId is null||baseline.LoadGameAt is null||!baseline.CargoReliable||!baseline.CreditsReliable
||!current.InGame||current.CommanderId!=baseline.CommanderId||current.CurrentShipId!=baseline.CurrentShipId||current.LoadGameAt!=baseline.LoadGameAt
||current.GameMode!=baseline.GameMode||current.CargoCapacity!=baseline.CargoCapacity||!current.CargoReliable||current.CargoCount!=baseline.CargoCount
||!Cargo(current).SequenceEqual(Cargo(baseline))||!current.CreditsReliable||current.Credits!=baseline.Credits
||current.Docked!=baseline.Docked||current.SystemName!=baseline.SystemName||current.MarketId!=baseline.MarketId)
throw new InvalidOperationException("The stationary main ship, session, cargo, wallet or recorded location changed.");
var raw=await pipe.CallAsync(new{op="read"},token);var health=raw.GetProperty("shipHealth");
var observed=raw.GetProperty("observedAtUnixMilliseconds").GetInt64();var now=DateTimeOffset.UtcNow;
var sequence=raw.GetProperty("sequence").GetInt64();var entity=raw.GetProperty("entityId").GetString()??"";var world=raw.GetProperty("referenceWorldAddress").GetString()??"";
var hull=health.GetProperty("hullFraction").GetDouble();var speed=raw.GetProperty("speedMetresPerSecond").GetDouble();
double? shield=health.TryGetProperty("shieldFraction",out var shieldValue)&&shieldValue.ValueKind==JsonValueKind.Number?shieldValue.GetDouble():null;
if(!raw.GetProperty("inGame").GetBoolean()||!raw.GetProperty("localPlayerShipVerified").GetBoolean()||raw.GetProperty("playerOverride").GetBoolean()
||raw.GetProperty("controlApplied").GetBoolean()||raw.GetProperty("fsd").GetProperty("ownedCharging").GetBoolean()
||!ulong.TryParse(entity,out var entityId)||entityId==0||!ulong.TryParse(world,out var worldId)||worldId==0||sequence<=0
||!health.GetProperty("verified").GetBoolean()||health.GetProperty("entityId").GetString()!=entity||health.GetProperty("sequence").GetInt64()!=sequence
||health.GetProperty("observedAtUnixMilliseconds").GetInt64()!=observed||health.GetProperty("damageObserved").GetBoolean()
||!double.IsFinite(hull)||hull is <=0 or >1||shield is {} s&&(!double.IsFinite(s)||s is <0 or >1)
||!double.IsFinite(speed)||speed is <0 or >1||observed>now.ToUnixTimeMilliseconds()+100||now.ToUnixTimeMilliseconds()-observed>300)
throw new InvalidOperationException("Authorized maintenance requires fresh stationary main-ship samples without new damage, control or owned FSD charging.");
// Keep the old emergency bit as evidence. It grants no input or
// reset authority and does not override these independent samples.
var emergency=health.GetProperty("emergency").GetBoolean();
var previous=samples.LastOrDefault(sample=>sample.ProcessId==target.ProcessId&&sample.ProcessStartedUtcTicks==target.StartedUtcTicks);
if(previous is not null&&(entity!=previous.EntityId||world!=previous.ReferenceWorld||sequence<previous.Sequence||observed<previous.ObservedAtMilliseconds
||hull+.000001<previous.Hull||previous.Shield is {} oldShield&&shield is {} nextShield&&nextShield+.000001<oldShield
||samples.Count>start&&observed-previous.ObservedAtMilliseconds>500))
throw new InvalidOperationException("The stationary ship identity, health or continuous observation changed.");
if(previous is not null&&sequence==previous.Sequence)
{
if(observed!=previous.ObservedAtMilliseconds||hull!=previous.Hull||shield!=previous.Shield)
throw new InvalidDataException("The original stationary health changed without a new sample identity.");
if(clock.Elapsed>TimeSpan.FromSeconds(6))throw new TimeoutException("No newer stationary main-ship sample was observed.");
await Task.Delay(120,token);continue;
}
samples.Add(new(window,now,target.ProcessId,target.StartedUtcTicks,sequence,observed,entity,world,current.CurrentShipId.Value,hull,shield,speed,emergency,raw.Clone()));
windowAt??=observed;requireJournalUnchanged();
if(!requireStableWindow||samples.Count-start>=12&&observed-windowAt.Value>=3000&&clock.Elapsed>=TimeSpan.FromSeconds(3))return;
if(clock.Elapsed>TimeSpan.FromSeconds(6))throw new TimeoutException("A continuous three-second stationary maintenance window was not established.");
await Task.Delay(120,token);
}while(true);
}
}
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Plugins;
using XFEEDToolBox.Core.Session;
using XFEEDToolBox.NativeSupport;
using XFEEDToolBox.Services;
/// <summary>Explicitly requested recovery of the current non-playing client; never trades or flies.</summary>
internal static class RecoverCurrentSession
{
private sealed record Stage(string Name,DateTimeOffset At,string? Detail=null);
private sealed record StationarySample(string Window,DateTimeOffset At,int ProcessId,long ProcessStartedUtcTicks,long Sequence,long ObservedAtMilliseconds,
string EntityId,string ReferenceWorld,long JournalShipId,double Hull,double? Shield,double Speed,bool OriginalEmergency,JsonElement Raw);
internal static async Task RunAsync(string executable,string report,bool updateStationaryShip=false,bool updateDockedShip=false)
{
executable=Path.GetFullPath(executable);report=Path.GetFullPath(report);
if(File.Exists(report)||File.Exists(report+".entry.json"))throw new IOException("Use a new current-session recovery report.");
Directory.CreateDirectory(Path.GetDirectoryName(report)!);
var stages=new List<Stage>();
GameSessionTarget? initialTarget=null,replacement=null;
PlayerState? initialPlayer=null,finalPlayer=null;
JsonElement? initialNativeStatus=null,beforeTerminationStatus=null,finalNativeStatus=null;
JsonElement? initialInventory=null,beforeTerminationInventory=null,finalInventory=null;
var stationarySamples=new List<StationarySample>();
PluginPackage? bundle=null;PluginStatus? previousPlugin=null,installedPlugin=null;
PluginHandshake? finalPlugin=null;
string? packageDirectory=null;Exception? failure=null;
var terminated=false;var oldProcessExited=false;var installed=false;var entered=false;var passed=false;
bool? cargoUnchanged=null;
bool? walletUnchanged=null;
DockedPluginMaintenanceProof? maintenanceProof=null;DockedMaintenanceJournalWatch? maintenanceWatch=null;
object? Snapshot(PlayerState? value)=>value is null?null:new
{
value.CommanderId,value.GameMode,value.LoadGameAt,value.CurrentShipId,value.Ship,
value.CargoCount,value.CargoReliable,value.CargoAt,value.Inventory,value.InGame,
value.Docked,value.SystemName,value.StationName,value.MarketId,value.CargoCapacity,value.Credits,value.CreditsReliable
};
async Task Save(string phase)
{
var temporary=report+".tmp";
await File.WriteAllTextAsync(temporary,JsonSerializer.Serialize(new
{
schema=1,at=DateTimeOffset.UtcNow,phase,passed,executable,packageDirectory,
initialTarget,replacement,initial=Snapshot(initialPlayer),final=Snapshot(finalPlayer),
initialNativeStatus,beforeTerminationStatus,finalNativeStatus,previousPlugin,installedPlugin,
package=bundle,finalPlugin,terminated,oldProcessExited,installed,entered,cargoUnchanged,walletUnchanged,
entryReport=report+".entry.json",stages,failure=failure?.ToString(),updateDockedShip,updateStationaryShip,
dockedMaintenanceSamples=maintenanceProof?.Samples,stationarySamples,initialInventory,beforeTerminationInventory,finalInventory,legacyEmergencyCleared=false,
flightInputsSent=0,tradeOrdersSent=0,screenUse=false,osInputsSent=0
},ToolboxSettings.JsonOptions));
File.Move(temporary,report,true);
}
async Task Record(string phase,string? detail=null)
{stages.Add(new(phase,DateTimeOffset.UtcNow,detail));await Save(phase);Console.WriteLine("恢复当前会话:"+phase);}
static async Task<string> Hash(string path,CancellationToken token)
{
await using var stream=new FileStream(path,FileMode.Open,FileAccess.Read,FileShare.ReadWrite|FileShare.Delete);
return Convert.ToHexStringLower(await SHA256.HashDataAsync(stream,token));
}
static void VerifyNative(JsonElement hello,JsonElement status,GameSessionTarget target,string hash,bool expectInGame)
{
var now=DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
if(hello.GetProperty("processId").GetInt32()!=target.ProcessId||hello.GetProperty("gameSha256").GetString()!=hash
||status.GetProperty("schema").GetInt32()!=1||status.GetProperty("name").GetString()!="XFEEDToolBox.Session"
||status.GetProperty("processId").GetInt32()!=target.ProcessId||status.GetProperty("gameSha256").GetString()!=hash
||status.GetProperty("sequence").GetInt64()<=0)
throw new InvalidDataException("Original session identity is not the selected game process.");
var observed=status.GetProperty("observedAtUnixMilliseconds").GetInt64();
var state=status.GetProperty("state").GetString();var next=status.GetProperty("nextState").GetString();
if(now-observed>350||observed>now+100||string.IsNullOrEmpty(state)||string.IsNullOrEmpty(next))
throw new IOException("Original session state is unavailable or stale; recovery did not infer the current screen.");
if(expectInGame)
{
if(!hello.GetProperty("inGame").GetBoolean()||state!="InGame"||next!="InGame")
throw new IOException("The replacement client has not confirmed a stable original InGame state.");
}
else if(hello.GetProperty("inGame").GetBoolean()||state=="InGame"||next=="InGame")
throw new InvalidOperationException("The client has reached a playing state; current-session recovery was not restarted.");
}
try
{
await Record("requested");
if(updateDockedShip&&updateStationaryShip)throw new ArgumentException("Use exactly one independently verified maintenance mode.");
using var timeout=new CancellationTokenSource(TimeSpan.FromMinutes(10));var token=timeout.Token;
var settings=ToolboxSettings.Load(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"XFEstudio","XFEEDToolBox","settings.json"));
await using var journal=new JournalMonitor(settings.JournalDirectory);await journal.StartAsync(token);
if(updateDockedShip)maintenanceProof=new();
if(updateDockedShip||updateStationaryShip)maintenanceWatch=new(journal);
initialPlayer=journal.State;
if(!NativeRelogWorkflow.SupportsMode(initialPlayer.GameMode)||string.IsNullOrWhiteSpace(initialPlayer.CommanderId)||initialPlayer.LoadGameAt is null)
throw new InvalidOperationException("A previously observed commander and Solo/Open session are required for original mode restoration.");
var driver=new WindowsGameSessionDriver(()=>journal.State,()=>executable);
initialTarget=await driver.FindLaunchTargetAsync(token);
if(initialTarget.ProcessId<=0)throw new InvalidOperationException("There is no unique current game process to recover.");
await driver.ValidateRestartAsync(initialTarget,token);
var gameHash=await Hash(executable,token);
if(gameHash!=NativeRelogChannel.SupportedGameSha256)throw new InvalidDataException("The current game build has no verified original session interface.");
var pipe=new NavigationPipeClient(initialTarget.ProcessId);
async Task<JsonElement> ReadInventoryAsync(NavigationPipeClient currentPipe,PlayerState current)
{
var raw=await currentPipe.CallAsync(new{op="read"},token);
var inventory=await currentPipe.CallAsync(new{op="commerce.inventory"},token);
var after=await currentPipe.CallAsync(new{op="read"},token);
if(!inventory.GetProperty("verified").GetBoolean()||inventory.GetProperty("entityId").GetString()!=raw.GetProperty("entityId").GetString()
||after.GetProperty("entityId").GetString()!=raw.GetProperty("entityId").GetString()
||after.GetProperty("referenceWorldAddress").GetString()!=raw.GetProperty("referenceWorldAddress").GetString()
||!current.CargoReliable||inventory.GetProperty("count").GetInt32()!=current.CargoCount||inventory.GetProperty("capacity").GetInt32()!=current.CargoCapacity
||!inventory.GetProperty("credits").TryGetInt64(out var credits)||credits<0)
throw new InvalidDataException("Stationary maintenance cannot verify the actual main-ship cargo and wallet.");
return inventory.Clone();
}
async Task VerifyStationaryWindowAsync(string window,bool requireStableWindow=true)
{
if(maintenanceWatch is null)throw new InvalidOperationException("Stationary maintenance was not explicitly requested.");
await VerifyStationaryShip(pipe,initialTarget,initialPlayer,()=>journal.State,stationarySamples,
driver.FindLaunchTargetAsync,maintenanceWatch.RequireUnchanged,window,token,requireStableWindow);
}
async Task VerifyDockedSampleAsync()
{
if(maintenanceProof is null||maintenanceWatch is null)throw new InvalidOperationException("Docked maintenance proof was not explicitly requested.");
token.ThrowIfCancellationRequested();maintenanceWatch.RequireUnchanged();
if(await driver.FindLaunchTargetAsync(token)!=initialTarget)throw new IOException("Docked maintenance process identity changed.");
var currentHello=await pipe.CallAsync(new{op="describe"},token);
var session=await GameSessionAcceptance.StatusAsync(initialTarget.ProcessId,token);
VerifyNative(currentHello,session,initialTarget,gameHash,expectInGame:true);
var before=await pipe.CallAsync(new{op="read"},token);var current=journal.State;
if(current.MarketId is not >0 or >=uint.MaxValue)throw new InvalidOperationException("Docked maintenance requires the actual current station market.");
var docked=await pipe.CallAsync(new{op="commerce.docked.status",marketId=(uint)current.MarketId.Value},token);
var raw=await pipe.CallAsync(new{op="read"},token);
if(before.GetProperty("entityId").GetString()!=raw.GetProperty("entityId").GetString()
||before.GetProperty("referenceWorldAddress").GetString()!=raw.GetProperty("referenceWorldAddress").GetString()
||before.GetProperty("sequence").GetInt64()>raw.GetProperty("sequence").GetInt64()
||before.GetProperty("observedAtUnixMilliseconds").GetInt64()>raw.GetProperty("observedAtUnixMilliseconds").GetInt64())
throw new IOException("The main ship changed around the original docking read.");
maintenanceWatch.RequireUnchanged();
maintenanceProof.Accept(journal.State,session,raw,docked,DateTimeOffset.UtcNow);
}
async Task VerifyDockedWindowAsync(bool initial)
{
var window=Stopwatch.StartNew();var beforeCount=maintenanceProof!.Samples.Count;
do
{
await VerifyDockedSampleAsync();
if(initial?maintenanceProof.Ready:maintenanceProof.Samples.Count-beforeCount>=3&&window.Elapsed>=TimeSpan.FromMilliseconds(250))break;
if(window.Elapsed>TimeSpan.FromSeconds(5))throw new TimeoutException("No bounded sequence of fresh stationary docked samples was confirmed.");
await Task.Delay(120,token);
}while(true);
await Record(initial?"independent-docked-maintenance-baseline-verified":"independent-docked-maintenance-rechecked",
"Original emergency values retained; no native health flag, flight input, menu or trade state was changed.");
}
var hello=await pipe.CallAsync(new{op="describe"},token);
initialNativeStatus=await GameSessionAcceptance.StatusAsync(initialTarget.ProcessId,token);
var initialPlaying=(updateStationaryShip||updateDockedShip)&&initialNativeStatus.Value.GetProperty("state").GetString()=="InGame"&&initialNativeStatus.Value.GetProperty("nextState").GetString()=="InGame";
if(updateDockedShip&&!initialPlaying)throw new InvalidOperationException("Docked maintenance requires stable InGame; Pause/ExitOptions cannot use non-playing recovery.");
if(updateStationaryShip&&!initialPlaying)throw new InvalidOperationException("Stationary maintenance requires stable InGame; Pause/ExitOptions cannot use non-playing recovery.");
VerifyNative(hello,initialNativeStatus.Value,initialTarget,gameHash,expectInGame:initialPlaying);
if(updateDockedShip)await VerifyDockedWindowAsync(initial:true);
else if(initialPlaying)
{
initialInventory=await ReadInventoryAsync(pipe,initialPlayer);
await VerifyStationaryWindowAsync("initial");
}
await Record(updateDockedShip?"current-process-and-docked-maintenance-verified":initialPlaying?"current-process-and-stationary-maintenance-verified":"current-process-and-nonplaying-session-verified");
packageDirectory=NativeSupportPackage.Directory;
var manager=new GamePluginManager(packageDirectory);bundle=manager.Package();
previousPlugin=await manager.InspectAsync(executable,token);
if(!previousPlugin.Installed||previousPlugin.PendingRemoval||previousPlugin.Installation is null)
throw new InvalidOperationException("Recovery updates only the existing toolbox-owned active plugin installation.");
if(bundle.SupportedGameSha256!=gameHash||await Hash(Path.Combine(packageDirectory,bundle.Loader),token)!=bundle.Sha256)
throw new InvalidDataException("The embedded replacement package failed validation; the game was left running.");
await Record("embedded-package-and-owned-installation-verified");
if(await driver.FindLaunchTargetAsync(token)!=initialTarget)throw new IOException("The current game process changed before recovery.");
await driver.ValidateRestartAsync(initialTarget,token);
hello=await pipe.CallAsync(new{op="describe"},token);
beforeTerminationStatus=await GameSessionAcceptance.StatusAsync(initialTarget.ProcessId,token);
var playingBefore=(updateStationaryShip||updateDockedShip)&&beforeTerminationStatus.Value.GetProperty("state").GetString()=="InGame"&&beforeTerminationStatus.Value.GetProperty("nextState").GetString()=="InGame";
if(updateDockedShip&&!playingBefore)throw new InvalidOperationException("Docked maintenance cannot fall back to a non-playing termination path.");
if(updateStationaryShip&&!playingBefore)throw new InvalidOperationException("Stationary maintenance cannot fall back to a non-playing termination path.");
VerifyNative(hello,beforeTerminationStatus.Value,initialTarget,gameHash,expectInGame:playingBefore);
if(updateDockedShip)await VerifyDockedWindowAsync(initial:false);
else if(playingBefore)
{
beforeTerminationInventory=await ReadInventoryAsync(pipe,journal.State);
if(initialInventory is null||!SameInventory(initialInventory.Value,beforeTerminationInventory.Value))
throw new IOException("The actual cargo or wallet changed before stationary maintenance termination.");
await VerifyStationaryWindowAsync("before-termination");
}
await Record("before-verified-process-termination");
if(updateDockedShip)
{
// Disk serialization must not create an unchecked gap before
// termination. Obtain one last different original frame.
await Task.Delay(30,token);await VerifyDockedSampleAsync();
}
else if(playingBefore)
{
await VerifyStationaryWindowAsync("final-before-termination",requireStableWindow:false);
beforeTerminationInventory=await ReadInventoryAsync(pipe,journal.State);
if(initialInventory is null||!SameInventory(initialInventory.Value,beforeTerminationInventory.Value))
throw new IOException("Actual cargo or wallet changed immediately before stationary maintenance termination.");
}
maintenanceWatch?.RequireUnchanged();
await driver.TerminateAsync(initialTarget,token);terminated=true;
await Record("termination-requested");
var exitWatch=Stopwatch.StartNew();
while(!await driver.HasExitedAsync(initialTarget,token))
{
if(exitWatch.Elapsed>TimeSpan.FromSeconds(20))throw new TimeoutException("The verified previous client did not exit.");
await Task.Delay(100,token);
}
oldProcessExited=true;await Record("previous-process-exit-confirmed");
installedPlugin=await manager.InstallAsync(executable,token);
if(!installedPlugin.Installed||installedPlugin.PendingRemoval||installedPlugin.Installation?.LoaderSha256!=bundle.Sha256)
throw new InvalidDataException("The replacement plugin installation has not matched its embedded package.");
installed=true;await Record("embedded-plugin-installed");
await Record("official-launch-and-original-mode-entry-requested",initialPlayer.GameMode);
await GameSessionAcceptance.StartAsync(executable,report+".entry.json");
entered=true;
replacement=await driver.FindLaunchTargetAsync(token);
if(replacement.ProcessId<=0||replacement.StartedUtcTicks<=initialTarget.StartedUtcTicks
||!string.Equals(replacement.Executable,initialTarget.Executable,StringComparison.OrdinalIgnoreCase))
throw new IOException("A new same-installation game process has not been confirmed.");
await driver.ValidateRestartAsync(replacement,token);
var replacementPipe=new NavigationPipeClient(replacement.ProcessId);
hello=await replacementPipe.CallAsync(new{op="describe"},token);
finalNativeStatus=await GameSessionAcceptance.StatusAsync(replacement.ProcessId,token);
VerifyNative(hello,finalNativeStatus.Value,replacement,gameHash,expectInGame:true);
finalPlugin=await GamePluginManager.ProbeAsync(replacement.ProcessId,token:token);
if(!finalPlugin.Loaded||!finalPlugin.Active||!finalPlugin.ObservationStarted||finalPlugin.Version!=bundle.Version)
throw new IOException("The new client has not confirmed the installed plugin and original observation service.");
var journalUntil=DateTimeOffset.UtcNow.AddSeconds(5);
do
{
finalPlayer=journal.State;
if(finalPlayer.InGame&&finalPlayer.LoadGameAt>initialPlayer.LoadGameAt&&(!(updateDockedShip||updateStationaryShip)
||finalPlayer.CargoReliable&&finalPlayer.CreditsReliable&&(!updateDockedShip||finalPlayer.Docked&&finalPlayer.MarketId==initialPlayer.MarketId)))break;
await Task.Delay(100,token);
}while(DateTimeOffset.UtcNow<journalUntil);
if(finalPlayer is null||!finalPlayer.InGame||finalPlayer.LoadGameAt<=initialPlayer.LoadGameAt
||finalPlayer.CommanderId!=initialPlayer.CommanderId||finalPlayer.GameMode!=initialPlayer.GameMode
||initialPlayer.CurrentShipId is {} ship&&finalPlayer.CurrentShipId!=ship)
throw new IOException("The new LoadGame has not confirmed the same commander, ship and original mode.");
static InventoryItem[] Cargo(PlayerState value)=>value.Inventory.OrderBy(row=>row.Key,StringComparer.Ordinal).ThenBy(row=>row.MissionId).ThenBy(row=>row.Stolen).ToArray();
if(initialPlayer.CargoReliable&&finalPlayer.CargoReliable)
cargoUnchanged=initialPlayer.CargoCount==finalPlayer.CargoCount&&Cargo(initialPlayer).SequenceEqual(Cargo(finalPlayer));
if(cargoUnchanged==false)throw new IOException("The session re-entered, but cargo differs from the recorded starting snapshot; no trade or cargo operation was submitted.");
if(updateDockedShip&&(!finalPlayer.CargoReliable||cargoUnchanged!=true||!finalPlayer.Docked||finalPlayer.MarketId!=initialPlayer.MarketId
||finalPlayer.SystemName!=initialPlayer.SystemName||finalPlayer.StationName!=initialPlayer.StationName
||finalPlayer.CargoCapacity!=initialPlayer.CargoCapacity||finalPlayer.Credits!=initialPlayer.Credits))
throw new IOException("The docked maintenance return has not confirmed unchanged station, cargo, ship capacity and recorded wallet.");
if(updateStationaryShip)
{
finalInventory=await ReadInventoryAsync(replacementPipe,finalPlayer);
walletUnchanged=initialInventory is {} original&&finalInventory.Value.GetProperty("credits").GetInt64()==original.GetProperty("credits").GetInt64();
if(initialInventory is null||walletUnchanged!=true||cargoUnchanged!=true||!SameInventory(initialInventory.Value,finalInventory.Value)
||finalPlayer.CargoCapacity!=initialPlayer.CargoCapacity||finalPlayer.Docked!=initialPlayer.Docked
||finalPlayer.SystemName!=initialPlayer.SystemName||initialPlayer.Docked&&finalPlayer.MarketId!=initialPlayer.MarketId)
throw new IOException("The stationary maintenance return has not confirmed unchanged actual cargo, wallet, ship capacity and location.");
using var returnedWatch=new DockedMaintenanceJournalWatch(journal);
await VerifyStationaryShip(replacementPipe,replacement,finalPlayer,()=>journal.State,stationarySamples,
driver.FindLaunchTargetAsync,returnedWatch.RequireUnchanged,"after-restart",token);
}
passed=true;await Record("same-commander-original-mode-and-plugin-confirmed");
}
catch(Exception error){failure=error;}
finally{maintenanceWatch?.Dispose();await Save(failure is null?"finished":"failed");}
Console.WriteLine(JsonSerializer.Serialize(new{passed,report,terminated,installed,entered,cargoUnchanged,replacement,failure=failure?.Message},ToolboxSettings.JsonOptions));
if(failure is not null)throw new InvalidOperationException("Current-session recovery did not fully verify completion; inspect "+report,failure);
}
private static bool SameInventory(JsonElement before,JsonElement after)
=>before.GetProperty("verified").GetBoolean()&&after.GetProperty("verified").GetBoolean()
&&before.GetProperty("count").GetInt32()==after.GetProperty("count").GetInt32()
&&before.GetProperty("capacity").GetInt32()==after.GetProperty("capacity").GetInt32()
&&before.GetProperty("credits").GetInt64()==after.GetProperty("credits").GetInt64();
private static async Task VerifyStationaryShip(NavigationPipeClient pipe,GameSessionTarget target,PlayerState baseline,Func<PlayerState> player,
List<StationarySample> samples,Func<CancellationToken,Task<GameSessionTarget>> findTarget,Action requireJournalUnchanged,string window,
CancellationToken token,bool requireStableWindow=true)
{
static InventoryItem[] Cargo(PlayerState value)=>value.Inventory.OrderBy(row=>row.Key,StringComparer.Ordinal).ThenBy(row=>row.MissionId).ThenBy(row=>row.Stolen).ToArray();
var clock=Stopwatch.StartNew();var start=samples.Count;long? windowAt=null;
do
{
token.ThrowIfCancellationRequested();requireJournalUnchanged();
if(await findTarget(token)!=target)throw new IOException("The verified stationary maintenance process changed.");
var current=player();
if(!baseline.InGame||baseline.CurrentShipId is null||baseline.LoadGameAt is null||!baseline.CargoReliable||!baseline.CreditsReliable
||!current.InGame||current.CommanderId!=baseline.CommanderId||current.CurrentShipId!=baseline.CurrentShipId||current.LoadGameAt!=baseline.LoadGameAt
||current.GameMode!=baseline.GameMode||current.CargoCapacity!=baseline.CargoCapacity||!current.CargoReliable||current.CargoCount!=baseline.CargoCount
||!Cargo(current).SequenceEqual(Cargo(baseline))||!current.CreditsReliable||current.Credits!=baseline.Credits
||current.Docked!=baseline.Docked||current.SystemName!=baseline.SystemName||current.MarketId!=baseline.MarketId)
throw new InvalidOperationException("The stationary main ship, session, cargo, wallet or recorded location changed.");
var raw=await pipe.CallAsync(new{op="read"},token);var health=raw.GetProperty("shipHealth");
var observed=raw.GetProperty("observedAtUnixMilliseconds").GetInt64();var now=DateTimeOffset.UtcNow;
var sequence=raw.GetProperty("sequence").GetInt64();var entity=raw.GetProperty("entityId").GetString()??"";var world=raw.GetProperty("referenceWorldAddress").GetString()??"";
var hull=health.GetProperty("hullFraction").GetDouble();var speed=raw.GetProperty("speedMetresPerSecond").GetDouble();
double? shield=health.TryGetProperty("shieldFraction",out var shieldValue)&&shieldValue.ValueKind==JsonValueKind.Number?shieldValue.GetDouble():null;
if(!raw.GetProperty("inGame").GetBoolean()||!raw.GetProperty("localPlayerShipVerified").GetBoolean()||raw.GetProperty("playerOverride").GetBoolean()
||raw.GetProperty("controlApplied").GetBoolean()||raw.GetProperty("fsd").GetProperty("ownedCharging").GetBoolean()
||!ulong.TryParse(entity,out var entityId)||entityId==0||!ulong.TryParse(world,out var worldId)||worldId==0||sequence<=0
||!health.GetProperty("verified").GetBoolean()||health.GetProperty("entityId").GetString()!=entity||health.GetProperty("sequence").GetInt64()!=sequence
||health.GetProperty("observedAtUnixMilliseconds").GetInt64()!=observed||health.GetProperty("damageObserved").GetBoolean()
||!double.IsFinite(hull)||hull is <=0 or >1||shield is {} s&&(!double.IsFinite(s)||s is <0 or >1)
||!double.IsFinite(speed)||speed is <0 or >1||observed>now.ToUnixTimeMilliseconds()+100||now.ToUnixTimeMilliseconds()-observed>300)
throw new InvalidOperationException("Authorized maintenance requires fresh stationary main-ship samples without new damage, control or owned FSD charging.");
// Keep the old emergency bit as evidence. It grants no input or
// reset authority and does not override these independent samples.
var emergency=health.GetProperty("emergency").GetBoolean();
var previous=samples.LastOrDefault(sample=>sample.ProcessId==target.ProcessId&&sample.ProcessStartedUtcTicks==target.StartedUtcTicks);
if(previous is not null&&(entity!=previous.EntityId||world!=previous.ReferenceWorld||sequence<previous.Sequence||observed<previous.ObservedAtMilliseconds
||hull+.000001<previous.Hull||previous.Shield is {} oldShield&&shield is {} nextShield&&nextShield+.000001<oldShield
||samples.Count>start&&observed-previous.ObservedAtMilliseconds>500))
throw new InvalidOperationException("The stationary ship identity, health or continuous observation changed.");
if(previous is not null&&sequence==previous.Sequence)
{
if(observed!=previous.ObservedAtMilliseconds||hull!=previous.Hull||shield!=previous.Shield)
throw new InvalidDataException("The original stationary health changed without a new sample identity.");
if(clock.Elapsed>TimeSpan.FromSeconds(6))throw new TimeoutException("No newer stationary main-ship sample was observed.");
await Task.Delay(120,token);continue;
}
samples.Add(new(window,now,target.ProcessId,target.StartedUtcTicks,sequence,observed,entity,world,current.CurrentShipId.Value,hull,shield,speed,emergency,raw.Clone()));
windowAt??=observed;requireJournalUnchanged();
if(!requireStableWindow||samples.Count-start>=12&&observed-windowAt.Value>=3000&&clock.Elapsed>=TimeSpan.FromSeconds(3))return;
if(clock.Elapsed>TimeSpan.FromSeconds(6))throw new TimeoutException("A continuous three-second stationary maintenance window was not established.");
await Task.Delay(120,token);
}while(true);
}
}