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

namespace XFEEDToolBox.Core.Execution;

public sealed class NativeOrderReconciliationRequiredException(JsonElement receipt):IOException("原始服务器请求未确认,正在恢复官方会话并核对原单。")
{
    public JsonElement Receipt{get;}=receipt.Clone();
    public static bool Present(Exception error)=>error is NativeOrderReconciliationRequiredException||error.InnerException is {} inner&&Present(inner);
}

/// <summary>Never resubmits an uncertain key. A fresh official client plus original server inventory must prove non-application.</summary>
public static class NativeOrderReconciliation
{
    private static string Marker(string pending)=>pending.Replace(".pending.json",".not-applied-after-reload.json",StringComparison.Ordinal);
    private static string Hash(byte[] bytes)=>Convert.ToHexStringLower(SHA256.HashData(bytes));
    public static bool IsServerFailure(JsonElement value)=>value.TryGetProperty("state",out var state)&&state.GetInt32()==4
        &&value.TryGetProperty("error",out var error)&&error.TryGetInt32(out var code)&&code is >=500 and <=599
        &&value.TryGetProperty("mutationsSent",out var sent)&&sent.GetInt32()==1
        &&value.TryGetProperty("serviceKind",out var kind)&&kind.GetInt32()==1
        &&value.TryGetProperty("serverConfirmed",out var server)&&server.ValueKind==JsonValueKind.False
        &&value.TryGetProperty("clientApplied",out var applied)&&applied.ValueKind==JsonValueKind.False;
    public static bool IsResolved(string pending,PlayerState current)
    {
        var marker=Marker(pending);var status=pending.Replace(".pending.json",".status.json",StringComparison.Ordinal);
        if(!File.Exists(marker)||!File.Exists(status))return false;
        using var proof=JsonDocument.Parse(File.ReadAllText(marker));var p=proof.RootElement;
        return p.GetProperty("commanderId").GetString()==current.CommanderId&&p.GetProperty("shipId").GetInt64()==current.CurrentShipId
            &&p.GetProperty("status").GetString()=="official-reload-proved-not-applied"
            &&p.GetProperty("pendingSha256").GetString()==Hash(File.ReadAllBytes(pending))&&p.GetProperty("statusSha256").GetString()==Hash(File.ReadAllBytes(status));
    }
    public static async Task WaitForResolutionAsync(string pending,IOriginalCommerceChannel channel,Func<PlayerState> player,string? journals,Action<string>? progress,CancellationToken token)
    {
        var statusPath=pending.Replace(".pending.json",".status.json",StringComparison.Ordinal);
        if(!File.Exists(statusPath))return;
        var originalStatus=await File.ReadAllBytesAsync(statusPath,token);using var status=JsonDocument.Parse(originalStatus);
        if(!IsServerFailure(status.RootElement))return;
        var bytes=await File.ReadAllBytesAsync(pending,token);using var original=JsonDocument.Parse(bytes);var row=original.RootElement;
        if(row.GetProperty("channel").GetString()==channel.Identity)throw new NativeOrderReconciliationRequiredException(status.RootElement);
        while(!IsResolved(pending,player()))
        {
            token.ThrowIfCancellationRequested();progress?.Invoke("正在只读核对原订单、服务器货舱和官方会话余额,不重复提交。");
            try{if(await TryResolveAsync(pending,bytes,originalStatus,row,channel,player(),journals,token))return;}
            catch(Exception error)when(NavigationRecovery.CanRetry(error,token)){}
            await Task.Delay(1000,token);
        }
    }
    private static async Task<bool> TryResolveAsync(string pending,byte[] originalBytes,byte[] originalStatus,JsonElement row,
        IOriginalCommerceChannel channel,PlayerState player,string? journals,CancellationToken token)
    {
        var created=row.GetProperty("created").GetDateTimeOffset();var intent=row.GetProperty("intent");var before=intent.GetProperty("before");var market=intent.GetProperty("market").GetInt64();
        var parts=channel.Identity.Split(':');var old=row.GetProperty("channel").GetString()!.Split(':');
        if(parts.Length!=2||old.Length!=2||parts[0]==old[0]||!long.TryParse(parts[1],out var processStarted)||processStarted<=created.UtcTicks
            ||!player.InGame||!player.Docked||player.MarketId!=market||!player.CargoReliable||!player.CreditsReliable||player.LoadGameAt is null||player.LoadGameAt<=created
            ||player.CommanderId!=row.GetProperty("CommanderId").GetString()||player.CurrentShipId!=row.GetProperty("CurrentShipId").GetInt64()
            ||player.CargoCapacity!=before.GetProperty("capacity").GetInt32())return false;
        var fees=journals is null?[]:await ExternalServiceCosts.ReadAsync(journals,player.CommanderId,player.CurrentShipId!.Value,created,[market],token);
        var expected=before.GetProperty("credits").GetInt64()-fees.Sum(f=>f.Entry.Amount);
        var current=await channel.CallAsync(new{op="commerce.inventory"},token);
        if(!current.GetProperty("verified").GetBoolean()||!before.GetProperty("verified").GetBoolean()||current.GetProperty("credits").GetInt64()!=expected
            ||player.Credits!=expected||current.GetProperty("count").GetInt32()!=before.GetProperty("count").GetInt32()||player.CargoCount!=before.GetProperty("count").GetInt32())return false;
        var queued=await channel.CallAsync(new{op="commerce.market.read",marketId=market,refreshClient=false},token);var generation=queued.GetProperty("generation").GetUInt64();
        var actual=await NavigationRecovery.ReadWithRecoveryAsync(async cancellation=>
        {
            var result=await channel.CallAsync(new{op="commerce.market.status",generation,maximum=50},cancellation);
            if(result.GetProperty("state").GetInt32()<3)throw new FlightTelemetryUnavailableException("等待原始服务器货舱快照。");
            if(result.GetProperty("state").GetInt32()!=3)throw new FlightTelemetryUnavailableException("服务器只读货舱查询暂未成功。");return result;
        },null,token);
        if(!actual.TryGetProperty("cargo",out var cargo)||cargo.ValueKind!=JsonValueKind.Object)return false;
        var commodity=CommodityCatalog.Key(intent.GetProperty("Commodity").GetString()!);long total=0,quantity=0;
        foreach(var item in cargo.EnumerateObject())
        {if(!item.Value.TryGetInt32(out var count)||count<0)return false;total+=count;if(CommodityCatalog.Key(item.Name)==commodity)quantity+=count;}
        var delta=intent.GetProperty("delta").GetInt32();var wanted=delta<0?Math.Abs((long)delta):0;
        if(total!=before.GetProperty("count").GetInt32()||quantity!=wanted||total!=wanted
            ||actual.TryGetProperty("serverCredits",out var serverCredits)&&serverCredits.GetInt64()!=expected)return false;
        var proof=new{status="official-reload-proved-not-applied",commanderId=player.CommanderId,shipId=player.CurrentShipId,
            pendingSha256=Hash(originalBytes),statusSha256=Hash(originalStatus),channel=channel.Identity,player.LoadGameAt,
            actualClientInventory=current,originalServerCargo=cargo,knownExternalExpenses=fees,expectedCredits=expected,originalRequestReplayed=false,at=DateTimeOffset.UtcNow};
        await File.WriteAllTextAsync(Marker(pending)+".tmp",JsonSerializer.Serialize(proof,ToolboxSettings.JsonOptions),token);File.Move(Marker(pending)+".tmp",Marker(pending),true);return true;
    }
}