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.Models;

namespace XFEEDToolBox.Core.Execution;

public sealed record NativeTradeJourneyResult(bool Passed,TradeJourneyCheckpoint Checkpoint,TradeProfitSnapshot Profit,long OpeningCredits,long? ClosingCredits,string? Failure,DateTimeOffset Finished);
public sealed class NativeTradeJourneyIncompleteException(NativeTradeJourneyResult result,Exception? cause):InvalidOperationException(
    cause is null?"跑商批次未完成,原始货物和回执已保留。":$"跑商批次未完成:{cause.Message};原始货物和回执已保留。",cause)
{public NativeTradeJourneyResult Result{get;}=result;}

/// <summary>A resumable production cargo journey, driven entirely by the toolbox.</summary>
public sealed class NativeTradeJourney(NativeTradeCommandDriver driver,Func<PlayerState> player,Func<CancellationToken,Task> check,string directory)
{
    public string? JournalDirectory{get;init;}
    public event Action<string>? Status;
    public async Task<NativeTradeJourneyResult> RunAsync(TradeLeg leg,CancellationToken token)
    {
        if(leg.Orders.Count!=1)throw new ArgumentException("可恢复批次需要唯一的原始货物订单。");
        Directory.CreateDirectory(directory);using var lease=new FileStream(Path.Combine(directory,"runner.lock"),FileMode.OpenOrCreate,FileAccess.ReadWrite,FileShare.None);
        var checkpointPath=Path.Combine(directory,"journey.json");var resultPath=Path.Combine(directory,"result.json");var planPath=Path.Combine(directory,"leg.json");
        var initial=player();var order=leg.Orders[0];
        var checkpoint=File.Exists(checkpointPath)?TradeJourneyCheckpoints.Load(checkpointPath)
            :new(initial.CommanderId,initial.CurrentShipId??throw new InvalidDataException("原始船舶身份缺失。"),DateTimeOffset.UtcNow);
        if(checkpoint.Commander!=initial.CommanderId||checkpoint.ShipId!=initial.CurrentShipId)throw new InvalidDataException("恢复批次属于另一艘船或指挥官。");
        if(File.Exists(planPath))
        {
            var saved=JsonSerializer.Deserialize<TradeLeg>(await File.ReadAllTextAsync(planPath,token),ToolboxSettings.JsonOptions)!;
            if(saved.From.MarketId!=leg.From.MarketId||saved.To.MarketId!=leg.To.MarketId||saved.Orders.Count!=1||saved.Orders[0]!=order)
                throw new InvalidDataException("恢复批次的站点或订单与原始计划不一致。");
        }
        else await WriteAsync(planPath,leg,token);
        if(File.Exists(resultPath)&&checkpoint.Sold is not null)
        {
            var completed=JsonSerializer.Deserialize<NativeTradeJourneyResult>(await File.ReadAllTextAsync(resultPath,token),ToolboxSettings.JsonOptions);
            if(completed?.Passed==true)return completed;
        }
        await driver.PreflightAsync(token);await check(token);
        using var ledger=new TradeProfitLedger(Path.Combine(directory,"ledger"));var sync=new object();
        var openingPath=Path.Combine(directory,"opening-wallet.json");long opening;
        if(File.Exists(openingPath))
        {using var saved=JsonDocument.Parse(await File.ReadAllTextAsync(openingPath,token));opening=saved.RootElement.GetProperty("credits").GetInt64();}
        else
        {
            if(checkpoint.Bought is not null)throw new InvalidDataException("恢复批次缺少原始起始钱包,拒绝推算余额。");
            opening=(await driver.RefreshWalletAsync(token)).Credits;await WriteAsync(openingPath,new{credits=opening},token);
        }
        void RecordTrade(ConfirmedTradeReceipt receipt)
        {
            lock(sync)
            {
                if(receipt.CommanderId!=checkpoint.Commander||receipt.ShipId!=checkpoint.ShipId||receipt.Commodity!=order.Commodity
                    ||receipt.QuantityDelta!=(receipt.QuantityDelta>0?order.Quantity:-order.Quantity)
                    ||receipt.MarketId!=(receipt.QuantityDelta>0?leg.From.MarketId:leg.To.MarketId))throw new InvalidDataException("原始交易回执与本批次不符。");
                checkpoint=receipt.QuantityDelta>0?checkpoint with{Bought=receipt}:checkpoint with{Sold=receipt};
                TradeJourneyCheckpoints.Save(checkpointPath,checkpoint);ledger.RecordTrade(receipt);
            }
        }
        void RecordService(ConfirmedDockedServiceReceipt receipt)=>ledger.Record(receipt.ToCashEntry());
        driver.TradeConfirmed+=RecordTrade;driver.ServiceConfirmed+=RecordService;
        using var stop=CancellationTokenSource.CreateLinkedTokenSource(token);Task? services=null;long? closing=null;string? failure=null;Exception? cause=null;
        var saleWasAlreadyConfirmed=checkpoint.Sold is not null;
        try
        {
            if(checkpoint.Bought is {} bought)
            {
                ledger.RecordTrade(bought);
                if(player().Docked&&player().MarketId==leg.From.MarketId)
                    await driver.ServiceDockedAsync(new(leg.From.MarketId,order.Quantity),check,stop.Token);
            }
            else
            {
                if(!player().Docked||player().MarketId!=leg.From.MarketId||player().CargoCount!=0)throw new InvalidDataException("采购前未确认原站停靠及空货舱。");
                TradeJourneyCheckpoints.Save(checkpointPath,checkpoint);Status?.Invoke("正在采购并完成站内维护");
                services=driver.ServiceDockedAsync(new(leg.From.MarketId,order.Quantity),check,stop.Token);
                var action=new TradeAction(TradeActionKind.Buy,leg,order);var at=DateTimeOffset.UtcNow;
                var receipt=await driver.ExecuteTradeAsync(action,check,stop.Token);receipt.Validate(action,player(),at);
                await services;services=null;
            }
            if(checkpoint.Sold is {} sold)ledger.RecordTrade(sold);
            else
            {
                if(!player().Docked||player().MarketId!=leg.To.MarketId)
                {
                    Status?.Invoke("正在运送本批货物");await driver.ExecuteAsync(new(TradeActionKind.Travel,leg,null),check,stop.Token);
                }
                Status?.Invoke("正在卖出并完成站内维护");services=driver.ServiceDockedAsync(new(leg.To.MarketId,order.Quantity),check,stop.Token);
                var action=new TradeAction(TradeActionKind.Sell,leg,order);var at=DateTimeOffset.UtcNow;
                var receipt=await driver.ExecuteTradeAsync(action,check,stop.Token);receipt.Validate(action,player(),at);
                await services;services=null;
            }
            // A prior worker may have persisted the sale before its services
            // finished; service verification remains mandatory on resume.
            if(saleWasAlreadyConfirmed)await driver.ServiceDockedAsync(new(leg.To.MarketId,order.Quantity),check,stop.Token);
            closing=(await driver.RefreshWalletAsync(stop.Token)).Credits;
            if(JournalDirectory is {} journals&&opening+ledger.Snapshot.CashChange!=closing)
                await ExternalServiceCosts.ReconcileAsync(journals,directory,checkpoint,leg,ledger,opening,closing.Value,stop.Token);
            if(!player().Docked||player().MarketId!=leg.To.MarketId||player().CargoCount!=0||checkpoint.Sold is null||opening+ledger.Snapshot.CashChange!=closing)
                throw new InvalidDataException("本批次原始余额、空货舱或实际停靠尚未完整核对。");
        }
        catch(Exception error){cause=error;failure=error.ToString();stop.Cancel();}
        finally
        {
            if(services is not null)try{await services;}catch(Exception error){failure??=error.ToString();}
            driver.TradeConfirmed-=RecordTrade;driver.ServiceConfirmed-=RecordService;await driver.ReleaseAllAsync();
        }
        var result=new NativeTradeJourneyResult(failure is null,checkpoint,ledger.Snapshot,opening,closing,failure,DateTimeOffset.UtcNow);
        var attempts=Path.Combine(directory,"attempts");Directory.CreateDirectory(attempts);
        await WriteAsync(Path.Combine(attempts,Guid.NewGuid().ToString("N")+".json"),result,CancellationToken.None);
        await WriteAsync(resultPath,result,CancellationToken.None);
        if(!result.Passed)throw new NativeTradeJourneyIncompleteException(result,cause);
        return result;
    }
    private static async Task WriteAsync(string path,object value,CancellationToken token)
    {
        var temporary=path+"."+Guid.NewGuid().ToString("N")+".tmp";
        try{await File.WriteAllTextAsync(temporary,JsonSerializer.Serialize(value,ToolboxSettings.JsonOptions),token);File.Move(temporary,path,true);}
        finally{if(File.Exists(temporary))File.Delete(temporary);}
    }
}