using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Execution;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Native;
using XFEEDToolBox.Core.Navigation;
// This is the actual sequential trading process. It has no Codex automation,
// OS scheduled task or overlapping trade jobs. A stop file cancels its active
// workflow; original accepted financial orders still finish reconciliation.
internal static class CgTradeLoop
{
private static readonly Station Source=new(){MarketId=3228792064,Name="Polyakov Station",SystemName="Duamta"};
private static readonly Station Homer=new(){MarketId=4210924803,Name="Homer Platform",SystemName="AD Leonis"};
internal static async Task RunAsync(int pid,string workRoot)
{
workRoot=Path.GetFullPath(workRoot);var directory=Path.Combine(workRoot,"automatic-trading");Directory.CreateDirectory(directory);
using var runLease=new FileStream(Path.Combine(directory,"runner.lock"),FileMode.OpenOrCreate,FileAccess.ReadWrite,FileShare.None);
using var stop=new CancellationTokenSource();
using var sentinel=new Timer(_=>{if(File.Exists(Path.Combine(directory,"stop")))stop.Cancel();},null,100,100);
ConsoleCancelEventHandler cancel=(_,e)=>{e.Cancel=true;stop.Cancel();};Console.CancelKeyPress+=cancel;
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();var initial=journal.State;
var lossHold=Path.Combine(workRoot,"ship-loss-hold.json");
if(File.Exists(lossHold)){using var h=JsonDocument.Parse(await File.ReadAllTextAsync(lossHold));if(h.RootElement.GetProperty("active").GetBoolean())throw new InvalidOperationException("Ship loss hold is active; automatic flight remains disabled.");}
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.");
using var guardian=await PersistentProtectionProcess.StartOrReuseAsync(pid,workRoot,stop.Token);
var previousGuardian=PersistentProtectionProcess.Current;PersistentProtectionProcess.Current=guardian;
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.13-dev")
throw new InvalidOperationException("This continuous workflow requires the verified 0.5.13 docking handoff implementation.");
decimal initialWallet;
using(var first=JsonDocument.Parse(await File.ReadAllTextAsync(Path.Combine(workRoot,"one-ton-journey","result.json"),stop.Token)))
{
var r=first.RootElement;if(!r.GetProperty("passed").GetBoolean())throw new InvalidDataException("Original completed low-cost journey required.");
var original=TradeJourneyCheckpoints.Load(Path.Combine(workRoot,"one-ton-journey","journey.json"));
if(original.Bought is not {ServerConfirmed:true,ClientApplied:true,Commodity:"biowaste",QuantityDelta:1} bought||original.Sold is null)
throw new InvalidDataException("Original confirmed opening purchase receipt required.");
initialWallet=bought.CreditsBefore;
}
string phase="starting";string? activeJourney=null;var started=DateTimeOffset.UtcNow;
var statePath=Path.Combine(directory,"state.json");
async Task State(string value,string? issue=null)
{
phase=value;var payload=new{pid,started,at=DateTimeOffset.UtcNow,phase,activeJourney,system=journal.State.SystemName,
station=journal.State.StationName,journal.State.Docked,journal.State.CargoCount,issue,scheduledTask=false};
var text=JsonSerializer.Serialize(payload,ToolboxSettings.JsonOptions);var temporary=statePath+".tmp";
await File.WriteAllTextAsync(temporary,text);File.Move(temporary,statePath,true);Console.WriteLine(text);
}
async Task Check(CancellationToken token)
{
token.ThrowIfCancellationRequested();var current=journal.State;
if(!current.InGame||current.CommanderId!=initial.CommanderId||current.CurrentShipId!=initial.CurrentShipId||current.LoadGameAt!=initial.LoadGameAt
||NavigationSafety.HasExternalThreat(current)||NavigationSafety.UnhealthyHull(current))
throw new InvalidOperationException("Automatic trade session, ship or safety state changed.");
var hold=Path.Combine(workRoot,"maintenance-hold.json");
if(File.Exists(hold))
{using var d=JsonDocument.Parse(await File.ReadAllTextAsync(hold,token));if(d.RootElement.GetProperty("active").GetBoolean())throw new InvalidOperationException("Maintenance hold is active.");}
var until=DateTimeOffset.UtcNow.AddSeconds(15);
while(true)
{
try
{
guardian.RequireReady(pid);
var health=await channel.CallAsync(new{op="health.read"},token);var ship=health.GetProperty("shipHealth");
if(!health.GetProperty("localPlayerShipVerified").GetBoolean()||!ship.GetProperty("verified").GetBoolean()
||ship.GetProperty("hullFraction").GetDouble()<.5)throw new InvalidDataException("Fresh healthy main ship required.");
return;
}
catch(Exception error)when((NavigationRecovery.CanRetry(error,token)
||error is InvalidOperationException&&error.Message=="Native main-ship health unavailable or stale")&&DateTimeOffset.UtcNow<until)
{await Task.Delay(80,token);}
}
}
async Task Travel(Station target,string prefix)
{
await Check(stop.Token);if(journal.State.Docked&&journal.State.MarketId==target.MarketId)return;
if(journal.State.Docked)
{
using var source=new NativeCurrentSystemSource(pid,NativeSystemLocationReader.GameSha256,()=>journal.State);
var catalogue=await NavigationRecovery.ReadWithRecoveryAsync(source.ReadLocationsAsync,null,stop.Token);
var alternate=catalogue.Locations.FirstOrDefault(x=>x.Category==SystemLocationCategory.Station&&x.MarketId is >0&&x.MarketId!=journal.State.MarketId)
??throw new InvalidOperationException("A verified alternate departure target is required.");
await State("departing");await LocalDepartureAcceptance.RunAsync(pid,alternate.Name,prefix+"-departure.json",stop.Token,stopAfterDeparture:false);
}
await Check(stop.Token);
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,keepHandoffScene:true);
// Each workflow has its own journal reader. The arrival observer
// can see Docked one poll before this outer reader does.
var dockObservedUntil=DateTimeOffset.UtcNow.AddSeconds(5);
while(!journal.State.Docked||journal.State.MarketId!=target.MarketId)
{
stop.Token.ThrowIfCancellationRequested();guardian.RequireReady(pid);
if(journal.State.Docked||DateTimeOffset.UtcNow>=dockObservedUntil)break;
await Task.Delay(50,stop.Token);
}
if(!journal.State.Docked||journal.State.MarketId!=target.MarketId)throw new InvalidOperationException("Actual docking differs from the trade route.");
}
async Task Membership()
{
await Check(stop.Token);var member=await new NativeCommunityGoalClient(channel).ReadAsync(true,stop.Token);
var goal=CommunityGoalCatalogue.RequireTrade(member.Goals,DateTimeOffset.UtcNow,true);
if(goal.Id!=864)throw new InvalidDataException("The active trade CG differs.");
var path=Path.Combine(workRoot,"cg-membership-864","membership-latest.json");
await File.WriteAllTextAsync(path+".tmp",JsonSerializer.Serialize(member),stop.Token);File.Move(path+".tmp",path,true);
}
async Task<List<JsonElement>> Market()
{
await Check(stop.Token);var market=journal.State.MarketId??throw new InvalidOperationException("Actual docking market required.");
var dockReadyUntil=DateTimeOffset.UtcNow.AddSeconds(10);
while(true)
{
var dock=await channel.CallAsync(new{op="commerce.docked.status",marketId=market},stop.Token);
if(dock.GetProperty("ready").GetBoolean())break;
if(DateTimeOffset.UtcNow>=dockReadyUntil)throw new TimeoutException("Original docking market has not settled.");
await Task.Delay(80,stop.Token);
}
var queued=await channel.CallAsync(new{op="commerce.market.read",marketId=market},stop.Token);var generation=queued.GetProperty("generation").GetUInt64();
var until=DateTimeOffset.UtcNow.AddSeconds(35);JsonElement result;
while(true)
{
result=await channel.CallAsync(new{op="commerce.market.status",generation,maximum=50},stop.Token);
if(result.GetProperty("state").GetInt32()>=3)break;
if(DateTimeOffset.UtcNow>=until)throw new TimeoutException("Original market read did not complete.");await Task.Delay(80,stop.Token);
}
if(result.GetProperty("state").GetInt32()!=3||result.GetProperty("marketId").GetInt64()!=market)throw new InvalidDataException("Original market identity or response differs.");
var rows=new List<JsonElement>();rows.AddRange(result.GetProperty("commodities").EnumerateArray().Select(x=>x.Clone()));
for(var offset=50;offset<result.GetProperty("commodityCount").GetInt32();offset+=50)
{var page=await channel.CallAsync(new{op="commerce.market.status",generation,offset,maximum=50},stop.Token);rows.AddRange(page.GetProperty("commodities").EnumerateArray().Select(x=>x.Clone()));}
return rows;
}
var commodities=new[]{(Id:128049172u,Key:"uranium",Label:"Uranium"),(Id:128049189u,Key:"coffee",Label:"Coffee"),
(Id:128049197u,Key:"polymers",Label:"Polymers"),(Id:128673851u,Key:"liquidoxygen",Label:"Liquid oxygen")};
var destinationQuotes=Path.Combine(directory,"destination-quotes.json");
async Task CaptureDestinationQuotes()
{
if(!journal.State.Docked||journal.State.MarketId!=Homer.MarketId)throw new InvalidOperationException("Homer original quotes require actual docking.");
var rows=await Market();var prices=commodities.Select(c=>new{commodity=c.Key,sellPrice=rows.Single(x=>x.GetProperty("id").GetUInt32()==c.Id).GetProperty("sellPrice").GetInt64()}).ToArray();
await File.WriteAllTextAsync(destinationQuotes+".tmp",JsonSerializer.Serialize(new{at=DateTimeOffset.UtcNow,marketId=Homer.MarketId,prices}),stop.Token);
File.Move(destinationQuotes+".tmp",destinationQuotes,true);
}
async Task<TradeOrder> SelectBatch()
{
using var saved=JsonDocument.Parse(await File.ReadAllTextAsync(destinationQuotes,stop.Token));var d=saved.RootElement;
if(d.GetProperty("marketId").GetInt64()!=Homer.MarketId||DateTimeOffset.UtcNow-d.GetProperty("at").GetDateTimeOffset()>TimeSpan.FromMinutes(30))
throw new InvalidDataException("Destination prices must be refreshed before purchasing.");
var prices=d.GetProperty("prices").EnumerateArray().ToDictionary(x=>x.GetProperty("commodity").GetString()!,x=>x.GetProperty("sellPrice").GetInt64());
var sourceRows=await Market();var quotes=new List<CgBatchQuote>();
foreach(var c in commodities)
{
var row=sourceRows.SingleOrDefault(x=>x.GetProperty("id").GetUInt32()==c.Id);if(row.ValueKind==JsonValueKind.Undefined)continue;
quotes.Add(new(c.Key,c.Label,row.TryGetProperty("stock",out var stock)?stock.GetInt64():0,row.GetProperty("buyPrice").GetInt64(),prices[c.Key]));
}
var inventory=await channel.CallAsync(new{op="commerce.inventory"},stop.Token);
if(!inventory.GetProperty("verified").GetBoolean()||inventory.GetProperty("count").GetInt32()!=0||inventory.GetProperty("capacity").GetInt32()!=192)
throw new InvalidDataException("Original empty hold differs before selecting a batch.");
return CgBatchPlanner.Select(quotes,192,inventory.GetProperty("credits").GetDecimal(),Math.Max(journal.State.Rebuy,4427098));
}
async Task Maintain(string tag)
{await State("docked-maintenance");await DockedMaintenance.RunAsync(pid,Path.Combine(directory,"maintenance-"+tag),stop.Token);}
string? Unfinished()
{
var candidates=Directory.EnumerateDirectories(workRoot,"cg192-*").Where(path=>File.Exists(Path.Combine(path,"journey.json")))
.Where(path=>{var result=Path.Combine(path,"result.json");if(!File.Exists(result))return true;using var d=JsonDocument.Parse(File.ReadAllText(result));return !d.RootElement.GetProperty("passed").GetBoolean();}).ToArray();
return candidates.Length switch{0=>null,1=>candidates[0],_=>throw new InvalidDataException("Multiple unfinished cargo journeys require reconciliation.")};
}
async Task FinancialSummary()
{
var paths=new List<string>{Path.Combine(workRoot,"one-ton-journey"),Path.Combine(workRoot,"cg-empty-second-maintenance")};
paths.AddRange(Directory.EnumerateDirectories(workRoot,"cg192-*"));paths.AddRange(Directory.EnumerateDirectories(directory,"maintenance-*"));
var recoveryCosts=Path.Combine(workRoot,"recovery-costs");if(Directory.Exists(recoveryCosts))paths.AddRange(Directory.EnumerateDirectories(recoveryCosts));
var components=new List<object>();decimal total=0,cash=0;
foreach(var path in paths.Distinct(StringComparer.OrdinalIgnoreCase))
{
var result=Path.Combine(path,"result.json");if(!File.Exists(result))continue;
using var d=JsonDocument.Parse(await File.ReadAllTextAsync(result,stop.Token));var r=d.RootElement;
if(!r.GetProperty("passed").GetBoolean())continue;var p=r.GetProperty("profit");
var net=p.GetProperty("netProfit").GetDecimal();total+=net;cash+=p.GetProperty("cashChange").GetDecimal();
components.Add(new{directory=Path.GetRelativePath(workRoot,path).Replace('\\','/'),netProfit=net});
}
var inventory=await channel.CallAsync(new{op="commerce.inventory"},stop.Token);var credits=inventory.GetProperty("credits").GetInt64();
// The initial wallet is established by the original completed 1t
// and first CG receipts. External or missing movements stop trading.
if(initialWallet+cash!=credits||inventory.GetProperty("count").GetInt32()!=0)throw new InvalidDataException("Continuous trade wallet does not reconcile with all completed ledgers.");
var pathSummary=Path.Combine(workRoot,"current-financial-summary.json");
using var membership=JsonDocument.Parse(await File.ReadAllTextAsync(Path.Combine(workRoot,"cg-membership-864","membership-latest.json"),stop.Token));
var goal=CommunityGoalCatalogue.RequireTrade(CommunityGoalCatalogue.Parse(membership.RootElement.GetProperty("Body").GetString()!),DateTimeOffset.UtcNow,true);
var payload=JsonSerializer.Serialize(new{at=DateTimeOffset.UtcNow,confirmedNetProfit=total,credits,cargoCount=0,cargoCapacity=192,
system=journal.State.SystemName,station=journal.State.StationName,components,cgContribution=goal.Contribution,collectedRewards=0,automaticProcess=true,scheduledTask=false},ToolboxSettings.JsonOptions);
await File.WriteAllTextAsync(pathSummary+".tmp",payload,stop.Token);File.Move(pathSummary+".tmp",pathSummary,true);
}
try
{
await State("preflight");await Check(stop.Token);activeJourney=Unfinished();
if(activeJourney is not null)
{await State("resuming-cargo");await CgCargoJourney.RunAsync(pid,activeJourney,stop.Token);await FinancialSummary();activeJourney=null;}
var proof=Path.Combine(directory,"continuous-empty-validation.json");
if(!File.Exists(proof))
{
if(journal.State.CargoCount!=0)throw new InvalidOperationException("Empty validation requires the original empty hold.");
if(journal.State.Docked)await Maintain("validation-before-"+Guid.NewGuid().ToString("N"));
if(journal.State.Docked&&journal.State.MarketId==Homer.MarketId)
{await Membership();await CaptureDestinationQuotes();}
var planPath=Path.Combine(directory,"validation-plan.json");Station target;
if(File.Exists(planPath))
{
using var plan=JsonDocument.Parse(await File.ReadAllTextAsync(planPath,stop.Token));
target=plan.RootElement.GetProperty("marketId").GetInt64() switch{3228792064=>Source,4210924803=>Homer,_=>throw new InvalidDataException("Validation target differs from the CG route.")};
}
else
{
target=journal.State.Docked?(journal.State.MarketId==Homer.MarketId?Source:Homer)
:journal.State.SystemName==Homer.SystemName?Homer:Source;
}
// A normal-space recovery may already have docked at the
// failed attempt's destination. That is not continuous proof;
// 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.13-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.13-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.13-dev")throw new InvalidDataException("Continuous proof belongs to a different implementation.");}
while(true)
{
await Check(stop.Token);activeJourney=Unfinished();
if(activeJourney is null)
{
if(journal.State.Docked&&journal.State.MarketId==Homer.MarketId)
{await Membership();await CaptureDestinationQuotes();await Maintain("homer-"+Guid.NewGuid().ToString("N"));}
else if(!File.Exists(destinationQuotes))
{await Travel(Homer,Path.Combine(directory,"price-refresh-"+Guid.NewGuid().ToString("N")));await Membership();await CaptureDestinationQuotes();await Maintain("homer-"+Guid.NewGuid().ToString("N"));}
await Travel(Source,Path.Combine(directory,"source-"+Guid.NewGuid().ToString("N")));
var number=Directory.EnumerateDirectories(workRoot,"cg192-*").Select(path=>int.TryParse(Path.GetFileName(path).AsSpan(6),out var n)?n:0).DefaultIfEmpty().Max()+1;
activeJourney=Path.Combine(workRoot,"cg192-"+number.ToString("000"));
}
var selected=File.Exists(Path.Combine(activeJourney,"journey.json"))?null:await SelectBatch();
await State("cargo-journey");await CgCargoJourney.RunAsync(pid,activeJourney,stop.Token,selected);activeJourney=null;await FinancialSummary();await State("cycle-completed");
}
}
catch(OperationCanceledException)when(stop.IsCancellationRequested){await State("stopped");}
catch(Exception error){await State("faulted",error.ToString());throw;}
finally{Console.CancelKeyPress-=cancel;PersistentProtectionProcess.Current=previousGuardian;if(File.Exists(Path.Combine(directory,"stop")))guardian.StopForUser();}
}
}
using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Execution;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Native;
using XFEEDToolBox.Core.Navigation;
// This is the actual sequential trading process. It has no Codex automation,
// OS scheduled task or overlapping trade jobs. A stop file cancels its active
// workflow; original accepted financial orders still finish reconciliation.
internal static class CgTradeLoop
{
private static readonly Station Source=new(){MarketId=3228792064,Name="Polyakov Station",SystemName="Duamta"};
private static readonly Station Homer=new(){MarketId=4210924803,Name="Homer Platform",SystemName="AD Leonis"};
internal static async Task RunAsync(int pid,string workRoot)
{
workRoot=Path.GetFullPath(workRoot);var directory=Path.Combine(workRoot,"automatic-trading");Directory.CreateDirectory(directory);
using var runLease=new FileStream(Path.Combine(directory,"runner.lock"),FileMode.OpenOrCreate,FileAccess.ReadWrite,FileShare.None);
using var stop=new CancellationTokenSource();
using var sentinel=new Timer(_=>{if(File.Exists(Path.Combine(directory,"stop")))stop.Cancel();},null,100,100);
ConsoleCancelEventHandler cancel=(_,e)=>{e.Cancel=true;stop.Cancel();};Console.CancelKeyPress+=cancel;
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();var initial=journal.State;
var lossHold=Path.Combine(workRoot,"ship-loss-hold.json");
if(File.Exists(lossHold)){using var h=JsonDocument.Parse(await File.ReadAllTextAsync(lossHold));if(h.RootElement.GetProperty("active").GetBoolean())throw new InvalidOperationException("Ship loss hold is active; automatic flight remains disabled.");}
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.");
using var guardian=await PersistentProtectionProcess.StartOrReuseAsync(pid,workRoot,stop.Token);
var previousGuardian=PersistentProtectionProcess.Current;PersistentProtectionProcess.Current=guardian;
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.13-dev")
throw new InvalidOperationException("This continuous workflow requires the verified 0.5.13 docking handoff implementation.");
decimal initialWallet;
using(var first=JsonDocument.Parse(await File.ReadAllTextAsync(Path.Combine(workRoot,"one-ton-journey","result.json"),stop.Token)))
{
var r=first.RootElement;if(!r.GetProperty("passed").GetBoolean())throw new InvalidDataException("Original completed low-cost journey required.");
var original=TradeJourneyCheckpoints.Load(Path.Combine(workRoot,"one-ton-journey","journey.json"));
if(original.Bought is not {ServerConfirmed:true,ClientApplied:true,Commodity:"biowaste",QuantityDelta:1} bought||original.Sold is null)
throw new InvalidDataException("Original confirmed opening purchase receipt required.");
initialWallet=bought.CreditsBefore;
}
string phase="starting";string? activeJourney=null;var started=DateTimeOffset.UtcNow;
var statePath=Path.Combine(directory,"state.json");
async Task State(string value,string? issue=null)
{
phase=value;var payload=new{pid,started,at=DateTimeOffset.UtcNow,phase,activeJourney,system=journal.State.SystemName,
station=journal.State.StationName,journal.State.Docked,journal.State.CargoCount,issue,scheduledTask=false};
var text=JsonSerializer.Serialize(payload,ToolboxSettings.JsonOptions);var temporary=statePath+".tmp";
await File.WriteAllTextAsync(temporary,text);File.Move(temporary,statePath,true);Console.WriteLine(text);
}
async Task Check(CancellationToken token)
{
token.ThrowIfCancellationRequested();var current=journal.State;
if(!current.InGame||current.CommanderId!=initial.CommanderId||current.CurrentShipId!=initial.CurrentShipId||current.LoadGameAt!=initial.LoadGameAt
||NavigationSafety.HasExternalThreat(current)||NavigationSafety.UnhealthyHull(current))
throw new InvalidOperationException("Automatic trade session, ship or safety state changed.");
var hold=Path.Combine(workRoot,"maintenance-hold.json");
if(File.Exists(hold))
{using var d=JsonDocument.Parse(await File.ReadAllTextAsync(hold,token));if(d.RootElement.GetProperty("active").GetBoolean())throw new InvalidOperationException("Maintenance hold is active.");}
var until=DateTimeOffset.UtcNow.AddSeconds(15);
while(true)
{
try
{
guardian.RequireReady(pid);
var health=await channel.CallAsync(new{op="health.read"},token);var ship=health.GetProperty("shipHealth");
if(!health.GetProperty("localPlayerShipVerified").GetBoolean()||!ship.GetProperty("verified").GetBoolean()
||ship.GetProperty("hullFraction").GetDouble()<.5)throw new InvalidDataException("Fresh healthy main ship required.");
return;
}
catch(Exception error)when((NavigationRecovery.CanRetry(error,token)
||error is InvalidOperationException&&error.Message=="Native main-ship health unavailable or stale")&&DateTimeOffset.UtcNow<until)
{await Task.Delay(80,token);}
}
}
async Task Travel(Station target,string prefix)
{
await Check(stop.Token);if(journal.State.Docked&&journal.State.MarketId==target.MarketId)return;
if(journal.State.Docked)
{
using var source=new NativeCurrentSystemSource(pid,NativeSystemLocationReader.GameSha256,()=>journal.State);
var catalogue=await NavigationRecovery.ReadWithRecoveryAsync(source.ReadLocationsAsync,null,stop.Token);
var alternate=catalogue.Locations.FirstOrDefault(x=>x.Category==SystemLocationCategory.Station&&x.MarketId is >0&&x.MarketId!=journal.State.MarketId)
??throw new InvalidOperationException("A verified alternate departure target is required.");
await State("departing");await LocalDepartureAcceptance.RunAsync(pid,alternate.Name,prefix+"-departure.json",stop.Token,stopAfterDeparture:false);
}
await Check(stop.Token);
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,keepHandoffScene:true);
// Each workflow has its own journal reader. The arrival observer
// can see Docked one poll before this outer reader does.
var dockObservedUntil=DateTimeOffset.UtcNow.AddSeconds(5);
while(!journal.State.Docked||journal.State.MarketId!=target.MarketId)
{
stop.Token.ThrowIfCancellationRequested();guardian.RequireReady(pid);
if(journal.State.Docked||DateTimeOffset.UtcNow>=dockObservedUntil)break;
await Task.Delay(50,stop.Token);
}
if(!journal.State.Docked||journal.State.MarketId!=target.MarketId)throw new InvalidOperationException("Actual docking differs from the trade route.");
}
async Task Membership()
{
await Check(stop.Token);var member=await new NativeCommunityGoalClient(channel).ReadAsync(true,stop.Token);
var goal=CommunityGoalCatalogue.RequireTrade(member.Goals,DateTimeOffset.UtcNow,true);
if(goal.Id!=864)throw new InvalidDataException("The active trade CG differs.");
var path=Path.Combine(workRoot,"cg-membership-864","membership-latest.json");
await File.WriteAllTextAsync(path+".tmp",JsonSerializer.Serialize(member),stop.Token);File.Move(path+".tmp",path,true);
}
async Task<List<JsonElement>> Market()
{
await Check(stop.Token);var market=journal.State.MarketId??throw new InvalidOperationException("Actual docking market required.");
var dockReadyUntil=DateTimeOffset.UtcNow.AddSeconds(10);
while(true)
{
var dock=await channel.CallAsync(new{op="commerce.docked.status",marketId=market},stop.Token);
if(dock.GetProperty("ready").GetBoolean())break;
if(DateTimeOffset.UtcNow>=dockReadyUntil)throw new TimeoutException("Original docking market has not settled.");
await Task.Delay(80,stop.Token);
}
var queued=await channel.CallAsync(new{op="commerce.market.read",marketId=market},stop.Token);var generation=queued.GetProperty("generation").GetUInt64();
var until=DateTimeOffset.UtcNow.AddSeconds(35);JsonElement result;
while(true)
{
result=await channel.CallAsync(new{op="commerce.market.status",generation,maximum=50},stop.Token);
if(result.GetProperty("state").GetInt32()>=3)break;
if(DateTimeOffset.UtcNow>=until)throw new TimeoutException("Original market read did not complete.");await Task.Delay(80,stop.Token);
}
if(result.GetProperty("state").GetInt32()!=3||result.GetProperty("marketId").GetInt64()!=market)throw new InvalidDataException("Original market identity or response differs.");
var rows=new List<JsonElement>();rows.AddRange(result.GetProperty("commodities").EnumerateArray().Select(x=>x.Clone()));
for(var offset=50;offset<result.GetProperty("commodityCount").GetInt32();offset+=50)
{var page=await channel.CallAsync(new{op="commerce.market.status",generation,offset,maximum=50},stop.Token);rows.AddRange(page.GetProperty("commodities").EnumerateArray().Select(x=>x.Clone()));}
return rows;
}
var commodities=new[]{(Id:128049172u,Key:"uranium",Label:"Uranium"),(Id:128049189u,Key:"coffee",Label:"Coffee"),
(Id:128049197u,Key:"polymers",Label:"Polymers"),(Id:128673851u,Key:"liquidoxygen",Label:"Liquid oxygen")};
var destinationQuotes=Path.Combine(directory,"destination-quotes.json");
async Task CaptureDestinationQuotes()
{
if(!journal.State.Docked||journal.State.MarketId!=Homer.MarketId)throw new InvalidOperationException("Homer original quotes require actual docking.");
var rows=await Market();var prices=commodities.Select(c=>new{commodity=c.Key,sellPrice=rows.Single(x=>x.GetProperty("id").GetUInt32()==c.Id).GetProperty("sellPrice").GetInt64()}).ToArray();
await File.WriteAllTextAsync(destinationQuotes+".tmp",JsonSerializer.Serialize(new{at=DateTimeOffset.UtcNow,marketId=Homer.MarketId,prices}),stop.Token);
File.Move(destinationQuotes+".tmp",destinationQuotes,true);
}
async Task<TradeOrder> SelectBatch()
{
using var saved=JsonDocument.Parse(await File.ReadAllTextAsync(destinationQuotes,stop.Token));var d=saved.RootElement;
if(d.GetProperty("marketId").GetInt64()!=Homer.MarketId||DateTimeOffset.UtcNow-d.GetProperty("at").GetDateTimeOffset()>TimeSpan.FromMinutes(30))
throw new InvalidDataException("Destination prices must be refreshed before purchasing.");
var prices=d.GetProperty("prices").EnumerateArray().ToDictionary(x=>x.GetProperty("commodity").GetString()!,x=>x.GetProperty("sellPrice").GetInt64());
var sourceRows=await Market();var quotes=new List<CgBatchQuote>();
foreach(var c in commodities)
{
var row=sourceRows.SingleOrDefault(x=>x.GetProperty("id").GetUInt32()==c.Id);if(row.ValueKind==JsonValueKind.Undefined)continue;
quotes.Add(new(c.Key,c.Label,row.TryGetProperty("stock",out var stock)?stock.GetInt64():0,row.GetProperty("buyPrice").GetInt64(),prices[c.Key]));
}
var inventory=await channel.CallAsync(new{op="commerce.inventory"},stop.Token);
if(!inventory.GetProperty("verified").GetBoolean()||inventory.GetProperty("count").GetInt32()!=0||inventory.GetProperty("capacity").GetInt32()!=192)
throw new InvalidDataException("Original empty hold differs before selecting a batch.");
return CgBatchPlanner.Select(quotes,192,inventory.GetProperty("credits").GetDecimal(),Math.Max(journal.State.Rebuy,4427098));
}
async Task Maintain(string tag)
{await State("docked-maintenance");await DockedMaintenance.RunAsync(pid,Path.Combine(directory,"maintenance-"+tag),stop.Token);}
string? Unfinished()
{
var candidates=Directory.EnumerateDirectories(workRoot,"cg192-*").Where(path=>File.Exists(Path.Combine(path,"journey.json")))
.Where(path=>{var result=Path.Combine(path,"result.json");if(!File.Exists(result))return true;using var d=JsonDocument.Parse(File.ReadAllText(result));return !d.RootElement.GetProperty("passed").GetBoolean();}).ToArray();
return candidates.Length switch{0=>null,1=>candidates[0],_=>throw new InvalidDataException("Multiple unfinished cargo journeys require reconciliation.")};
}
async Task FinancialSummary()
{
var paths=new List<string>{Path.Combine(workRoot,"one-ton-journey"),Path.Combine(workRoot,"cg-empty-second-maintenance")};
paths.AddRange(Directory.EnumerateDirectories(workRoot,"cg192-*"));paths.AddRange(Directory.EnumerateDirectories(directory,"maintenance-*"));
var recoveryCosts=Path.Combine(workRoot,"recovery-costs");if(Directory.Exists(recoveryCosts))paths.AddRange(Directory.EnumerateDirectories(recoveryCosts));
var components=new List<object>();decimal total=0,cash=0;
foreach(var path in paths.Distinct(StringComparer.OrdinalIgnoreCase))
{
var result=Path.Combine(path,"result.json");if(!File.Exists(result))continue;
using var d=JsonDocument.Parse(await File.ReadAllTextAsync(result,stop.Token));var r=d.RootElement;
if(!r.GetProperty("passed").GetBoolean())continue;var p=r.GetProperty("profit");
var net=p.GetProperty("netProfit").GetDecimal();total+=net;cash+=p.GetProperty("cashChange").GetDecimal();
components.Add(new{directory=Path.GetRelativePath(workRoot,path).Replace('\\','/'),netProfit=net});
}
var inventory=await channel.CallAsync(new{op="commerce.inventory"},stop.Token);var credits=inventory.GetProperty("credits").GetInt64();
// The initial wallet is established by the original completed 1t
// and first CG receipts. External or missing movements stop trading.
if(initialWallet+cash!=credits||inventory.GetProperty("count").GetInt32()!=0)throw new InvalidDataException("Continuous trade wallet does not reconcile with all completed ledgers.");
var pathSummary=Path.Combine(workRoot,"current-financial-summary.json");
using var membership=JsonDocument.Parse(await File.ReadAllTextAsync(Path.Combine(workRoot,"cg-membership-864","membership-latest.json"),stop.Token));
var goal=CommunityGoalCatalogue.RequireTrade(CommunityGoalCatalogue.Parse(membership.RootElement.GetProperty("Body").GetString()!),DateTimeOffset.UtcNow,true);
var payload=JsonSerializer.Serialize(new{at=DateTimeOffset.UtcNow,confirmedNetProfit=total,credits,cargoCount=0,cargoCapacity=192,
system=journal.State.SystemName,station=journal.State.StationName,components,cgContribution=goal.Contribution,collectedRewards=0,automaticProcess=true,scheduledTask=false},ToolboxSettings.JsonOptions);
await File.WriteAllTextAsync(pathSummary+".tmp",payload,stop.Token);File.Move(pathSummary+".tmp",pathSummary,true);
}
try
{
await State("preflight");await Check(stop.Token);activeJourney=Unfinished();
if(activeJourney is not null)
{await State("resuming-cargo");await CgCargoJourney.RunAsync(pid,activeJourney,stop.Token);await FinancialSummary();activeJourney=null;}
var proof=Path.Combine(directory,"continuous-empty-validation.json");
if(!File.Exists(proof))
{
if(journal.State.CargoCount!=0)throw new InvalidOperationException("Empty validation requires the original empty hold.");
if(journal.State.Docked)await Maintain("validation-before-"+Guid.NewGuid().ToString("N"));
if(journal.State.Docked&&journal.State.MarketId==Homer.MarketId)
{await Membership();await CaptureDestinationQuotes();}
var planPath=Path.Combine(directory,"validation-plan.json");Station target;
if(File.Exists(planPath))
{
using var plan=JsonDocument.Parse(await File.ReadAllTextAsync(planPath,stop.Token));
target=plan.RootElement.GetProperty("marketId").GetInt64() switch{3228792064=>Source,4210924803=>Homer,_=>throw new InvalidDataException("Validation target differs from the CG route.")};
}
else
{
target=journal.State.Docked?(journal.State.MarketId==Homer.MarketId?Source:Homer)
:journal.State.SystemName==Homer.SystemName?Homer:Source;
}
// A normal-space recovery may already have docked at the
// failed attempt's destination. That is not continuous proof;
// 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.13-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.13-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.13-dev")throw new InvalidDataException("Continuous proof belongs to a different implementation.");}
while(true)
{
await Check(stop.Token);activeJourney=Unfinished();
if(activeJourney is null)
{
if(journal.State.Docked&&journal.State.MarketId==Homer.MarketId)
{await Membership();await CaptureDestinationQuotes();await Maintain("homer-"+Guid.NewGuid().ToString("N"));}
else if(!File.Exists(destinationQuotes))
{await Travel(Homer,Path.Combine(directory,"price-refresh-"+Guid.NewGuid().ToString("N")));await Membership();await CaptureDestinationQuotes();await Maintain("homer-"+Guid.NewGuid().ToString("N"));}
await Travel(Source,Path.Combine(directory,"source-"+Guid.NewGuid().ToString("N")));
var number=Directory.EnumerateDirectories(workRoot,"cg192-*").Select(path=>int.TryParse(Path.GetFileName(path).AsSpan(6),out var n)?n:0).DefaultIfEmpty().Max()+1;
activeJourney=Path.Combine(workRoot,"cg192-"+number.ToString("000"));
}
var selected=File.Exists(Path.Combine(activeJourney,"journey.json"))?null:await SelectBatch();
await State("cargo-journey");await CgCargoJourney.RunAsync(pid,activeJourney,stop.Token,selected);activeJourney=null;await FinancialSummary();await State("cycle-completed");
}
}
catch(OperationCanceledException)when(stop.IsCancellationRequested){await State("stopped");}
catch(Exception error){await State("faulted",error.ToString());throw;}
finally{Console.CancelKeyPress-=cancel;PersistentProtectionProcess.Current=previousGuardian;if(File.Exists(Path.Combine(directory,"stop")))guardian.StopForUser();}
}
}