XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
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();}
    }
}