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.Json;
using XFEEDToolBox.Core.Models;

namespace XFEEDToolBox.Core.Execution;

/// <summary>Completes accounting after an optional post-sale market read failed; original receipts and failed results remain immutable.</summary>
public static class HistoricalTradeCompletion
{
    public const string FileName="confirmed-sale-completion.json";
    public sealed record Candidate(NativeTradeJourneyResult Original,string ResultHash,string CheckpointHash,string LegHash,string LedgerHash);
    private sealed record Proof(int Schema,string Commander,long ShipId,long MarketId,DateTimeOffset VerifiedAt,long HistoricalClosingCredits,long CurrentObservedCredits,
        string ResultHash,string CheckpointHash,string LegHash,string LedgerHash,bool CurrentEmptyCargoVerified,bool MaintenanceVerified,bool NativeResultRewritten);
    private static string Hash(string path)=>Convert.ToHexStringLower(SHA256.HashData(File.ReadAllBytes(path)));
    public static Candidate? ReadCandidate(string directory,TradeLeg leg,PlayerState player)
    {
        var resultPath=Path.Combine(directory,"result.json");var checkpointPath=Path.Combine(directory,"journey.json");var legPath=Path.Combine(directory,"leg.json");
        var openingPath=Path.Combine(directory,"opening-wallet.json");var ledgerPath=Path.Combine(directory,"ledger");
        if(!File.Exists(resultPath)||!File.Exists(checkpointPath)||!File.Exists(legPath)||!File.Exists(openingPath)||!Directory.Exists(ledgerPath))return null;
        var checkpoint=TradeJourneyCheckpoints.Load(checkpointPath);
        if(checkpoint.Commander!=player.CommanderId||checkpoint.ShipId!=player.CurrentShipId||checkpoint.Bought is not {} bought||checkpoint.Sold is not {} sold)return null;
        var result=JsonSerializer.Deserialize<NativeTradeJourneyResult>(File.ReadAllText(resultPath),ToolboxSettings.JsonOptions);
        if(result is null||result.Passed||result.Checkpoint!=checkpoint||result.Failure is not {} failure
            ||!failure.Contains("MarketAsync",StringComparison.Ordinal)||!failure.Contains("原始错误码 1",StringComparison.Ordinal)
            ||leg.Orders.Count!=1||bought.MarketId!=leg.From.MarketId||sold.MarketId!=leg.To.MarketId||result.Finished<sold.ConfirmedAt)return null;
        var order=leg.Orders[0];
        if(bought.Commodity!=order.Commodity||sold.Commodity!=order.Commodity||bought.QuantityDelta!=order.Quantity||sold.QuantityDelta!=-order.Quantity
            ||bought.UnitPrice>order.BuyPrice||sold.UnitPrice<order.SellPrice||bought.CargoBefore!=0||sold.CargoAfter!=0
            ||bought.CargoAfter!=order.Quantity||sold.CargoBefore!=order.Quantity)return null;
        using var opening=JsonDocument.Parse(File.ReadAllText(openingPath));
        if(opening.RootElement.GetProperty("credits").GetInt64()!=result.OpeningCredits)return null;
        using(var ledger=new TradeProfitLedger(ledgerPath))
        {
            var profit=ledger.Snapshot;
            if(profit!=result.Profit||profit.CargoInvestment!=0||profit.Purchases!=(decimal)bought.UnitPrice*order.Quantity
                ||profit.Sales!=(decimal)sold.UnitPrice*order.Quantity||result.OpeningCredits+profit.CashChange!=sold.CreditsAfter)return null;
        }
        var entries=Directory.EnumerateFiles(ledgerPath,"*.entry.json").Order(StringComparer.Ordinal).Select(path=>new{file=Path.GetFileName(path),hash=Hash(path)}).ToArray();
        var ledgerHash=Convert.ToHexStringLower(SHA256.HashData(JsonSerializer.SerializeToUtf8Bytes(entries)));
        return new(result,Hash(resultPath),Hash(checkpointPath),Hash(legPath),ledgerHash);
    }
    public static async Task SealAsync(string directory,TradeLeg leg,PlayerState player,long currentCredits,CancellationToken token)
    {
        var candidate=ReadCandidate(directory,leg,player)??throw new InvalidDataException("原批次的成交回执与历史余额未形成完整闭环。");
        if(!player.InGame||!player.Docked||player.MarketId!=leg.To.MarketId||!player.CargoReliable||player.CargoCount!=0||!player.CreditsReliable||player.Credits!=currentCredits)
            throw new InvalidDataException("未核对当前目的站、空舱和实际钱包,原批次保持未结算。");
        var proof=new Proof(1,player.CommanderId,player.CurrentShipId!.Value,leg.To.MarketId,DateTimeOffset.UtcNow,candidate.Original.Checkpoint.Sold!.CreditsAfter,currentCredits,
            candidate.ResultHash,candidate.CheckpointHash,candidate.LegHash,candidate.LedgerHash,true,true,false);
        var path=Path.Combine(directory,FileName);await File.WriteAllTextAsync(path+".tmp",JsonSerializer.Serialize(proof,ToolboxSettings.JsonOptions),token);File.Move(path+".tmp",path,true);
    }
    public static bool IsRecovered(string directory,TradeLeg leg,PlayerState player)
    {
        var path=Path.Combine(directory,FileName);if(!File.Exists(path))return false;
        var proof=JsonSerializer.Deserialize<Proof>(File.ReadAllText(path),ToolboxSettings.JsonOptions);var candidate=ReadCandidate(directory,leg,player);
        return proof is not null&&candidate is not null&&proof.Schema==1&&!proof.NativeResultRewritten&&proof.CurrentEmptyCargoVerified&&proof.MaintenanceVerified
            &&proof.Commander==player.CommanderId&&proof.ShipId==player.CurrentShipId&&proof.MarketId==leg.To.MarketId
            &&proof.HistoricalClosingCredits==candidate.Original.Checkpoint.Sold!.CreditsAfter&&proof.CurrentObservedCredits>=0&&proof.VerifiedAt>=candidate.Original.Finished
            &&proof.ResultHash==candidate.ResultHash&&proof.CheckpointHash==candidate.CheckpointHash&&proof.LegHash==candidate.LegHash&&proof.LedgerHash==candidate.LedgerHash;
    }
}