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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using XFEEDToolBox.Core.Models;

namespace XFEEDToolBox.Core.Execution;

/// <summary>One durable round per confirmed outbound sale, completed at home for return routes.</summary>
public static class AutomaticTradeRounds
{
    private sealed record Completion(int Schema,long SourceMarketId,long DestinationMarketId,bool ReturnTrade,string Commander,long ShipId,
        string OutboundPurchaseKey,string OutboundSaleKey,string OutboundJourney,DateTimeOffset OutboundSoldAt,
        DateTimeOffset CompletedAt,string Evidence,string? ReturnOperationKey=null);
    private sealed record Journey(string Directory,TradeLeg Leg,TradeJourneyCheckpoint Checkpoint);
    private sealed record Arrival(DateTimeOffset At,string Evidence,string? OperationKey);
    private static bool SameReceipt(ConfirmedTradeReceipt? first,ConfirmedTradeReceipt? second)
        =>first is null?second is null:second is not null&&first.OperationKey==second.OperationKey&&first.MarketId==second.MarketId
            &&first.Commodity==second.Commodity&&first.QuantityDelta==second.QuantityDelta&&first.UnitPrice==second.UnitPrice
            &&first.CreditsBefore==second.CreditsBefore&&first.CreditsAfter==second.CreditsAfter&&first.CargoBefore==second.CargoBefore&&first.CargoAfter==second.CargoAfter
            &&first.CommanderId==second.CommanderId&&first.ShipId==second.ShipId&&first.SessionStartedAt==second.SessionStartedAt&&first.ConfirmedAt==second.ConfirmedAt
            &&first.ServerConfirmed==second.ServerConfirmed&&first.ClientApplied==second.ClientApplied;
    private static bool SameCheckpoint(TradeJourneyCheckpoint first,TradeJourneyCheckpoint second)
        =>first.Commander==second.Commander&&first.ShipId==second.ShipId&&first.StartedAt==second.StartedAt
            &&SameReceipt(first.Bought,second.Bought)&&SameReceipt(first.Sold,second.Sold);

    public static int Refresh(AutomaticTradeConfiguration configuration,PlayerState current)
    {
        var root=configuration.WorkingDirectory;
        bool Outbound(TradeLeg leg)=>leg.To.MarketId==configuration.Destination.MarketId
            &&(configuration.UsesDynamicSources?configuration.Commodities.Length>0&&leg.Orders.Count>0
                &&leg.From.MarketId!=configuration.Destination.MarketId&&leg.Orders.All(o=>configuration.Commodities.Contains(o.Commodity,StringComparer.Ordinal))
                :leg.From.MarketId==configuration.Source.MarketId);
        bool SameGoal(Journey journey)
        {
            if(configuration.Mode!=AutomaticTradeMode.CommunityGoal||configuration.CommunityGoalId is null)return true;
            var file=Path.Combine(journey.Directory,"membership-before.json");if(!File.Exists(file))return false;
            using var proof=JsonDocument.Parse(File.ReadAllText(file));
            return proof.RootElement.GetProperty("goalId").GetUInt64()==configuration.CommunityGoalId;
        }
        if(!Directory.Exists(root)||string.IsNullOrWhiteSpace(current.CommanderId)||current.CurrentShipId is not {} ship)return 0;
        var completed=new List<Journey>();var purchases=new List<ConfirmedTradeReceipt>();
        foreach(var directory in Directory.EnumerateDirectories(root,"journey-*"))
        {
            var checkpointPath=Path.Combine(directory,"journey.json");
            if(!File.Exists(checkpointPath))continue;
            var checkpoint=TradeJourneyCheckpoints.Load(checkpointPath);
            if(checkpoint.Commander!=current.CommanderId||checkpoint.ShipId!=ship)continue;
            if(checkpoint.Bought is {} purchase)purchases.Add(purchase);
            var resultPath=Path.Combine(directory,"result.json");var legPath=Path.Combine(directory,"leg.json");
            if(!File.Exists(resultPath)||!File.Exists(legPath))continue;
            var result=JsonSerializer.Deserialize<NativeTradeJourneyResult>(File.ReadAllText(resultPath),ToolboxSettings.JsonOptions);
            if(result is null||result.Checkpoint is null||!SameCheckpoint(result.Checkpoint,checkpoint)||checkpoint.Bought is not {} bought||checkpoint.Sold is not {} sold)continue;
            var leg=JsonSerializer.Deserialize<TradeLeg>(File.ReadAllText(legPath),ToolboxSettings.JsonOptions);
            if(leg is null||!result.Passed&&!HistoricalTradeCompletion.IsRecovered(directory,leg,current))continue;
            if(leg is null||leg.Orders.Count!=1||bought.MarketId!=leg.From.MarketId||sold.MarketId!=leg.To.MarketId
                ||bought.Commodity!=leg.Orders[0].Commodity||bought.QuantityDelta!=leg.Orders[0].Quantity||sold.ConfirmedAt>result.Finished)continue;
            completed.Add(new(directory,leg,checkpoint));
        }
        var candidates=completed.Where(journey=>Outbound(journey.Leg)&&SameGoal(journey))
            .DistinctBy(journey=>journey.Checkpoint.Sold!.OperationKey).OrderBy(journey=>journey.Checkpoint.Bought!.ConfirmedAt).ToArray();
        // Two distinct batches cannot both occupy this ship's cargo hold over
        // overlapping intervals. Ambiguous records must not inflate rounds.
        var outbound=candidates.Where(journey=>!candidates.Any(other=>other!=journey
            &&other.Checkpoint.Bought!.ConfirmedAt<journey.Checkpoint.Sold!.ConfirmedAt
            &&journey.Checkpoint.Bought!.ConfirmedAt<other.Checkpoint.Sold!.ConfirmedAt)).ToArray();
        if(outbound.Length==0)return 0;
        var scope=JsonSerializer.Serialize(new{source=configuration.UsesDynamicSources?0:configuration.Source.MarketId,destination=configuration.Destination.MarketId,configuration.ReturnTrade,
            goal=configuration.Mode==AutomaticTradeMode.CommunityGoal?configuration.CommunityGoalId:null,
            commander=current.CommanderId,ship},ToolboxSettings.JsonOptions);
        var records=Path.Combine(root,"rounds-v1",Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(scope))));
        var count=0;
        for(var index=0;index<outbound.Length;index++)
        {
            var journey=outbound[index];
            var bought=journey.Checkpoint.Bought!;var sold=journey.Checkpoint.Sold!;var path=Path.Combine(records,sold.OperationKey+".json");
            if(File.Exists(path))
            {
                var existing=JsonSerializer.Deserialize<Completion>(File.ReadAllText(path),ToolboxSettings.JsonOptions);
                if(existing is null||existing.Schema!=1||existing.SourceMarketId!=journey.Leg.From.MarketId||existing.DestinationMarketId!=configuration.Destination.MarketId
                    ||existing.ReturnTrade!=configuration.ReturnTrade||existing.Commander!=current.CommanderId||existing.ShipId!=ship
                    ||existing.OutboundPurchaseKey!=bought.OperationKey||existing.OutboundSaleKey!=sold.OperationKey||existing.OutboundSoldAt!=sold.ConfirmedAt
                    ||existing.CompletedAt<sold.ConfirmedAt)throw new InvalidDataException("已完成轮次记录与原始交易回执不一致。");
                count++;continue;
            }
            Arrival? arrival;
            if(!configuration.ReturnTrade)arrival=new(sold.ConfirmedAt,"confirmed-outbound-sale",sold.OperationKey);
            else
            {
                // A later purchase at the source also proves a return that
                // completed just before a restart or the next journey began.
                var home=journey.Leg.From.MarketId;
                var witnesses=purchases.Where(receipt=>receipt.MarketId==home&&receipt.ConfirmedAt>=sold.ConfirmedAt&&receipt.OperationKey!=bought.OperationKey)
                    .Select(receipt=>new Arrival(receipt.ConfirmedAt,"confirmed-purchase-at-source",receipt.OperationKey))
                    .Concat(completed.Where(other=>other.Leg.From.MarketId==configuration.Destination.MarketId&&other.Leg.To.MarketId==home
                        &&other.Checkpoint.Sold!.ConfirmedAt>=sold.ConfirmedAt)
                        .Select(other=>new Arrival(other.Checkpoint.Sold!.ConfirmedAt,"confirmed-return-sale",other.Checkpoint.Sold.OperationKey))).ToList();
                if(current.InGame&&current.Docked&&current.MarketId==home&&current.LocationAt is {} dockedAt
                    &&dockedAt>=sold.ConfirmedAt&&dockedAt<=DateTimeOffset.UtcNow.AddSeconds(1))
                    witnesses.Add(new(dockedAt,"journal-docked-at-source",null));
                // Home must have been reached before the next outbound buy;
                // one later docking cannot close multiple prior rounds.
                var nextPurchase=index+1<outbound.Length?outbound[index+1].Checkpoint.Bought!.ConfirmedAt:(DateTimeOffset?)null;
                arrival=witnesses.Where(witness=>nextPurchase is null||witness.At<=nextPurchase).OrderBy(witness=>witness.At).FirstOrDefault();
            }
            if(arrival is null)continue;
            var value=new Completion(1,journey.Leg.From.MarketId,configuration.Destination.MarketId,configuration.ReturnTrade,current.CommanderId,ship,
                bought.OperationKey,sold.OperationKey,Path.GetFileName(journey.Directory),sold.ConfirmedAt,arrival.At,arrival.Evidence,arrival.OperationKey);
            Directory.CreateDirectory(records);var temporary=path+"."+Guid.NewGuid().ToString("N")+".tmp";
            try
            {
                using(var file=new FileStream(temporary,FileMode.CreateNew,FileAccess.Write,FileShare.Read,4096,FileOptions.WriteThrough))
                {JsonSerializer.Serialize(file,value,ToolboxSettings.JsonOptions);file.Flush(true);}
                File.Move(temporary,path,false);
            }
            finally{if(File.Exists(temporary))File.Delete(temporary);}
            count++;
        }
        return count;
    }
}