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.UtcNowjournal.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> 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();rows.AddRange(result.GetProperty("commodities").EnumerateArray().Select(x=>x.Clone())); for(var offset=50;offsetx.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 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(); 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{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();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();} } }