using System.Diagnostics; using System.Text.Json; using XFEEDToolBox.Core.Data; using XFEEDToolBox.Core.Execution; using XFEEDToolBox.Core.Models; using XFEEDToolBox.Core.Native; using XFEEDToolBox.Core.Navigation; internal static class TradeJourneyAcceptance { public static async Task RunAsync(int pid,string directory) { directory=Path.GetFullPath(directory);Directory.CreateDirectory(directory); var statePath=Path.Combine(directory,"journey.json");var statusPath=Path.Combine(directory,"status.json"); 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; if(!initial.InGame||initial.CurrentShipId is null||!initial.CargoReliable)throw new InvalidOperationException("Reliable live ship required."); var state=File.Exists(statePath)?TradeJourneyCheckpoints.Load(statePath):new TradeJourneyCheckpoint(initial.CommanderId,initial.CurrentShipId.Value,DateTimeOffset.UtcNow); if(state.Commander!=initial.CommanderId||state.ShipId!=initial.CurrentShipId)throw new InvalidOperationException("Acceptance commander or ship changed."); using var stop=new CancellationTokenSource(TimeSpan.FromMinutes(35)); using var sentinel=new Timer(_=>{if(File.Exists(Path.Combine(directory,"stop")))stop.Cancel();},null,100,100); using var ledger=new TradeProfitLedger(Path.Combine(directory,"ledger")); var statusLock=new object();string phase="准备 1t 低价跑商验收"; void Publish(string message,bool? passed=null) { lock(statusLock) { phase=message; var temporary=statusPath+".tmp"; File.WriteAllText(temporary,JsonSerializer.Serialize(new{pid,phase,passed,at=DateTimeOffset.UtcNow,profit=ledger.Snapshot, state.StartedAt,shipId=state.ShipId,initial.CargoCapacity,destination="AD Leonis / Homer Platform", quantity=1,commodity="biowaste",screenUse=false},ToolboxSettings.JsonOptions)); File.Move(temporary,statusPath,true);Console.WriteLine(message); } } Task Save(){TradeJourneyCheckpoints.Save(statePath,state);return Task.CompletedTask;} var channel=new NativeCommerceChannel(pid);NativeWalletObservation? wallet=null; PlayerState Player()=>wallet?.Project(journal.State)??journal.State; async Task Check(CancellationToken token) { token.ThrowIfCancellationRequested();var current=Player(); var maintenance=Path.Combine(Directory.GetParent(directory)!.FullName,"maintenance-hold.json"); if(current.Docked&&File.Exists(maintenance)) { using var notice=JsonDocument.Parse(await File.ReadAllTextAsync(maintenance,token)); if(notice.RootElement.GetProperty("active").GetBoolean())throw new InvalidOperationException("服务器维护等待中,保持停靠;维护完成后自动继续同一笔 1t 货物验收。"); } if(!current.InGame||current.CurrentShipId!=state.ShipId||current.CommanderId!=state.Commander||NavigationSafety.UnhealthyHull(current)||NavigationSafety.HasExternalThreat(current)) throw new InvalidOperationException("Acceptance session or ship safety changed."); var health=await channel.CallAsync(new{op="health.read"},token); if(!health.GetProperty("localPlayerShipVerified").GetBoolean()||!health.GetProperty("shipHealth").GetProperty("verified").GetBoolean() ||health.GetProperty("shipHealth").GetProperty("hullFraction").GetDouble()<.5)throw new InvalidOperationException("Independent ship health unavailable."); } async Task Travel(Station station,Func beforeInput,CancellationToken token) { await beforeInput(token); if(Player().Docked) { if(Player().MarketId==station.MarketId)return; Publish("自动起飞并解除质量锁定"); await LocalDepartureAcceptance.RunAsync(pid,"Quimper Dock",Path.Combine(directory,"departure-"+Guid.NewGuid().ToString("N")+".json"),token); } await Check(token); if(!Player().SystemName.Equals(station.SystemName,StringComparison.OrdinalIgnoreCase)) { Publish("正在按游戏内航线前往 "+station.SystemName); await InterstellarTravelAcceptance.RunAsync(pid,station.SystemName,Path.Combine(directory,"interstellar-"+Guid.NewGuid().ToString("N")+".json"),token); } await Check(token);Publish("正在超巡、呼叫空管并自动停靠 "+station.Name); await LocalNavigationAcceptance.RunAsync(pid,station.Name,Path.Combine(directory,"arrival-"+Guid.NewGuid().ToString("N")+".json"),requireCruise:false,useNativeCruise:true,allowObscuration:true,externalToken:token); if(!Player().Docked||Player().MarketId!=station.MarketId)throw new InvalidOperationException("Original docking did not confirm the expected destination."); } var driver=new NativeTradeCommandDriver(channel,Player,Path.Combine(directory,"operations"),Travel,Check); driver.WalletObserved+=observation=>wallet=observation; driver.ServiceConfirmed+=receipt=>{ledger.Record(receipt.ToCashEntry());Publish(phase);}; driver.Status+=message=>Publish(message); var from=new Station{MarketId=128096504,Name="Leiber Installation",SystemName="Phanes"}; var to=new Station{MarketId=4210924803,Name="Homer Platform",SystemName="AD Leonis"}; // A small acceptance expense is allowed; this is not the production route. // Sale uses a floor of 1 Cr and records the actual server price and net loss/profit. var order=new TradeOrder("biowaste","Biowaste",1,40,1); var leg=new TradeLeg{Number=1,From=from,To=to,Orders=[order]}; var policy=new TradeAcceptancePolicy(); var healthLog=Path.Combine(directory,"continuous-health-"+Guid.NewGuid().ToString("N")+".json"); var start=new ProcessStartInfo(Environment.ProcessPath!){UseShellExecute=false,CreateNoWindow=true,WindowStyle=ProcessWindowStyle.Hidden, RedirectStandardOutput=true,RedirectStandardError=true}; foreach(var argument in new[]{"observe-health",pid.ToString(),healthLog,"2100"})start.ArgumentList.Add(argument); using var monitor=Process.Start(start)??throw new IOException("Cannot start independent continuous ship protection."); var monitorReady=new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); monitor.OutputDataReceived+=(_,e)=>{if(e.Data?.Contains("health protection armed",StringComparison.Ordinal)==true)monitorReady.TrySetResult();}; monitor.BeginOutputReadLine();monitor.BeginErrorReadLine(); string? failure=null;Task? dockedService=null; try { await monitorReady.Task.WaitAsync(TimeSpan.FromSeconds(12),stop.Token); await Save();await driver.PreflightAsync(stop.Token);await Check(stop.Token); if(state.Bought is null) { if(!Player().Docked||Player().MarketId!=from.MarketId||Player().CargoCount!=0)throw new InvalidOperationException("First acceptance purchase requires the confirmed empty hold at Leiber Installation."); Publish("加油维修补给与 1t 低价买入同时执行"); var buy=new TradeAction(TradeActionKind.Buy,leg,order);policy.ValidateAction(buy,Player()); dockedService=driver.ServiceDockedAsync(new(from.MarketId,1),Check,stop.Token); var purchase=driver.ExecuteTradeAsync(buy,Check,stop.Token); var receipt=await purchase;receipt.Validate(buy,Player(),state.StartedAt);ledger.RecordTrade(receipt); state=state with{Bought=receipt};await Save();Publish("已确认买入 1t 低价货物"); await dockedService;dockedService=null; } else ledger.RecordTrade(state.Bought); if(state.Sold is null) { var travel=new TradeAction(TradeActionKind.Travel,leg,null);policy.ValidateAction(travel,Player()); if(!Player().Docked||Player().MarketId!=to.MarketId) { // Driver travel starts at the original departure station. A // resumed in-flight acceptance continues its same destination. await Travel(to,Check,stop.Token); } var sale=new TradeAction(TradeActionKind.Sell,leg,order);policy.ValidateAction(sale,Player()); Publish("到站维护与实际卖出同时执行"); dockedService=driver.ServiceDockedAsync(new(to.MarketId,1),Check,stop.Token); var receipt=await driver.ExecuteTradeAsync(sale,Check,stop.Token); receipt.Validate(sale,Player(),state.StartedAt);ledger.RecordTrade(receipt);state=state with{Sold=receipt};await Save(); await dockedService;dockedService=null; } else ledger.RecordTrade(state.Sold); if(Player().CargoCount!=0||!Player().Docked||Player().MarketId!=to.MarketId||monitor.HasExited) throw new InvalidOperationException("Final cargo, docking or continuous health protection not confirmed."); Publish("1t 完整跑商实测完成",true); } catch(Exception error){failure=error.ToString();Publish("正在处理验收问题:"+error.Message,false);} finally { stop.Cancel(); if(dockedService is not null)try{await dockedService;}catch(Exception error){failure??=error.ToString();} await driver.ReleaseAllAsync(); await File.WriteAllTextAsync(healthLog+".stop","stop",CancellationToken.None); try{await monitor.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(4));}catch(TimeoutException){} await File.WriteAllTextAsync(Path.Combine(directory,"result.json"),JsonSerializer.Serialize(new{passed=failure is null,failure,state,profit=ledger.Snapshot, finalSystem=Player().SystemName,finalStation=Player().StationName,Player().Docked,Player().CargoCount,screenUse=false},ToolboxSettings.JsonOptions)); } if(failure is not null)throw new InvalidOperationException("1t journey is awaiting automatic repair; see its persisted receipts and result."); } }