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>Accounts for an actual official-game manual sale, preserving the failed automated receipt.</summary>
public static class ExternalNativeSaleSettlement
{
    public static async Task ReconcileAsync(string root,string journals,PlayerState player,JsonElement inventory,IOriginalCommerceChannel channel,CancellationToken token)
    {
        if(!player.InGame||!player.Docked||!player.CargoReliable||!player.CreditsReliable||player.CargoCount!=0||player.MarketId is null||!inventory.GetProperty("verified").GetBoolean()||inventory.GetProperty("count").GetInt32()!=0)return;
        foreach(var directory in Directory.EnumerateDirectories(root,"journey-*"))
        {
            var checkpointPath=Path.Combine(directory,"journey.json");var legPath=Path.Combine(directory,"leg.json");
            if(!File.Exists(checkpointPath)||!File.Exists(legPath)||File.Exists(Path.Combine(directory,"settled.json")))continue;
            var checkpoint=TradeJourneyCheckpoints.Load(checkpointPath);if(checkpoint.Bought is not{} bought||checkpoint.Sold is not null||checkpoint.Commander!=player.CommanderId||checkpoint.ShipId!=player.CurrentShipId)continue;
            var leg=JsonSerializer.Deserialize<TradeLeg>(await File.ReadAllTextAsync(legPath,token),ToolboxSettings.JsonOptions)!;
            var ops=Path.Combine(root,"operations");var pendingFiles=new List<string>();
            foreach(var file in Directory.EnumerateFiles(ops,"*.pending.json"))
            {
                if(File.Exists(file.Replace(".pending.json",".confirmed.json",StringComparison.Ordinal)))continue;
                var statusFile=file.Replace(".pending.json",".status.json",StringComparison.Ordinal);if(!File.Exists(statusFile))continue;
                using var status=JsonDocument.Parse(await File.ReadAllTextAsync(statusFile,token));if(!NativeOrderReconciliation.IsServerFailure(status.RootElement))continue;
                using var p=JsonDocument.Parse(await File.ReadAllTextAsync(file,token));var row=p.RootElement;var intent=row.GetProperty("intent");
                if(row.GetProperty("CommanderId").GetString()==checkpoint.Commander&&row.GetProperty("CurrentShipId").GetInt64()==checkpoint.ShipId
                    &&intent.GetProperty("market").GetInt64()==leg.To.MarketId&&intent.GetProperty("delta").GetInt32()==-bought.QuantityDelta
                    &&intent.GetProperty("Commodity").GetString()==bought.Commodity)pendingFiles.Add(file);
            }
            if(pendingFiles.Count!=1)continue;var pending=pendingFiles[0];
            JsonElement? sale=null;var conflict=false;decimal laterTradeCashChange=0;
            var laterTrades=new List<JsonElement>();var laterCargo=new Dictionary<string,long>(StringComparer.Ordinal);
            foreach(var file in Directory.EnumerateFiles(journals,"Journal*.log").Where(p=>File.GetLastWriteTimeUtc(p)>=bought.ConfirmedAt.UtcDateTime).Order(StringComparer.Ordinal))
            {
                await using var stream=new FileStream(file,FileMode.Open,FileAccess.Read,FileShare.ReadWrite|FileShare.Delete);using var reader=new StreamReader(stream);var fid="";long ship=0;
                while(await reader.ReadLineAsync(token) is{} line)
                {
                    JsonDocument d;try{d=JsonDocument.Parse(line);}catch(JsonException){continue;}
                    using var parsed=d;var r=d.RootElement;var kind=r.TryGetProperty("event",out var e)?e.GetString():null;
                    if(kind=="LoadGame"){fid=r.TryGetProperty("FID",out var f)?f.GetString()??"":"";ship=r.TryGetProperty("ShipID",out var s)&&s.TryGetInt64(out var id)?id:0;}
                    if(kind=="Loadout")ship=r.TryGetProperty("ShipID",out var s)&&s.TryGetInt64(out var id)?id:0;
                    if(fid!=checkpoint.Commander||ship!=checkpoint.ShipId||!r.TryGetProperty("timestamp",out var t)||t.GetDateTimeOffset()<bought.ConfirmedAt)continue;
                    if(kind=="MarketBuy")
                    {
                        if(sale is null){conflict=true;break;}
                        var commodity=r.GetProperty("Type").GetString()!;var quantity=r.GetProperty("Count").GetInt32();var cost=r.GetProperty("TotalCost").GetInt64();
                        if(quantity<=0||cost<=0||cost!=(decimal)quantity*r.GetProperty("BuyPrice").GetInt64()){conflict=true;break;}
                        laterCargo[commodity]=laterCargo.GetValueOrDefault(commodity)+quantity;laterTradeCashChange-=cost;laterTrades.Add(r.Clone());continue;
                    }
                    if(kind!="MarketSell")continue;
                    if(sale is not null)
                    {
                        var commodity=r.GetProperty("Type").GetString()!;var quantity=r.GetProperty("Count").GetInt32();var proceeds=r.GetProperty("TotalSale").GetInt64();
                        if(quantity<=0||proceeds<=0||proceeds!=(decimal)quantity*r.GetProperty("SellPrice").GetInt64()||laterCargo.GetValueOrDefault(commodity)<quantity){conflict=true;break;}
                        laterCargo[commodity]-=quantity;laterTradeCashChange+=proceeds;laterTrades.Add(r.Clone());continue;
                    }
                    if(r.GetProperty("MarketID").GetInt64()!=leg.To.MarketId||r.GetProperty("Type").GetString()!=bought.Commodity
                        ||r.GetProperty("Count").GetInt32()!=bought.QuantityDelta||r.GetProperty("AvgPricePaid").GetInt64()!=bought.UnitPrice
                        ||r.GetProperty("TotalSale").GetInt64()!=(decimal)r.GetProperty("SellPrice").GetInt64()*bought.QuantityDelta){conflict=true;break;}
                    sale=r.Clone();
                }
                if(conflict)break;
            }
            if(conflict||sale is not{} sold||laterCargo.Values.Any(quantity=>quantity!=0))continue;
            var queued=await channel.CallAsync(new{op="commerce.market.read",marketId=player.MarketId.Value,refreshClient=false},token);var generation=queued.GetProperty("generation").GetUInt64();JsonElement market=default;
            for(var attempt=0;attempt<350;attempt++)
            {market=await channel.CallAsync(new{op="commerce.market.status",generation,maximum=50},token);if(market.GetProperty("state").GetInt32()>=3)break;await Task.Delay(100,token);}
            if(market.GetProperty("state").GetInt32()!=3||!market.TryGetProperty("cargo",out var cargo)||cargo.ValueKind!=JsonValueKind.Object||cargo.EnumerateObject().Any(p=>p.Value.GetInt64()!=0))continue;
            var actual=await channel.CallAsync(new{op="commerce.inventory"},token);if(!actual.GetProperty("verified").GetBoolean()||actual.GetProperty("count").GetInt32()!=0||actual.GetProperty("credits").GetInt64()!=player.Credits)continue;
            if(market.TryGetProperty("serverCredits",out var serverCredits)&&serverCredits.GetInt64()!=player.Credits)continue;
            using var ledger=new TradeProfitLedger(Path.Combine(directory,"ledger"));ledger.RecordTrade(bought);
            using var openingDocument=JsonDocument.Parse(await File.ReadAllTextAsync(Path.Combine(directory,"opening-wallet.json"),token));var opening=openingDocument.RootElement.GetProperty("credits").GetInt64();
            var key=Convert.ToHexStringLower(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(sold.GetRawText())));var revenue=sold.GetProperty("TotalSale").GetInt64();
            var soldAt=sold.GetProperty("timestamp").GetDateTimeOffset();
            var allFees=await ExternalServiceCosts.ReadAsync(journals,checkpoint.Commander,checkpoint.ShipId,bought.ConfirmedAt,[leg.From.MarketId,leg.To.MarketId],token);
            var fees=allFees.Where(p=>p.Entry.At<=soldAt&&!ledger.ContainsOperation(p.Entry.OperationKey)).ToArray();var laterFees=allFees.Where(p=>p.Entry.At>soldAt).ToArray();
            var afterSettlementCashChange=laterTradeCashChange-laterFees.Sum(p=>p.Entry.Amount);
            var expected=opening+ledger.Snapshot.CashChange+(ledger.ContainsOperation(key)?0:revenue)-fees.Sum(p=>p.Entry.Amount);
            var observedWallet=actual.GetProperty("credits").GetInt64();var closing=observedWallet-afterSettlementCashChange;
            var walletReconciled=expected==closing;
            ledger.Record(new(key,TradeCashKind.Sale,revenue,leg.To.MarketId,sold.GetProperty("timestamp").GetDateTimeOffset(),bought.Commodity,bought.QuantityDelta));
            var feeProofs=Path.Combine(directory,"external-service-proofs");Directory.CreateDirectory(feeProofs);
            foreach(var fee in fees){await Save(Path.Combine(feeProofs,fee.Entry.OperationKey+".json"),fee,token);ledger.Record(fee.Entry);}
            var proof=new{status="manual-sale-confirmed",proofKind="official-manual-sale-after-native-failure",commanderId=checkpoint.Commander,shipId=checkpoint.ShipId,
                operationKey=Path.GetFileName(pending).Replace(".pending.json",""),purchaseOperationKey=bought.OperationKey,
                pendingSha256=Convert.ToHexStringLower(SHA256.HashData(await File.ReadAllBytesAsync(pending,token))),saleProof=sold,actualInventory=actual,originalServerCargo=cargo,
                observedWallet,afterSettlementCashChange,laterTrades,laterFees,historicalClosingWalletFromVerifiedJournalChain=closing,laterManualTradesIncludedInAutomationProfit=false,
                walletReconciled,expectedClosingFromConfirmedReceipts=expected,unexplainedWalletDifference=closing-expected,
                originalNativeReceiptRewritten=false,nativeJourneyPassed=false,at=DateTimeOffset.UtcNow};
            var marker=pending.Replace(".pending.json",".externally-settled.json",StringComparison.Ordinal);
            await Save(marker,proof,token);
            await Save(Path.Combine(directory,"external-result.json"),new{passed=walletReconciled,tradeSettled=true,walletReconciled,manual=true,nativeJourneyPassed=false,profit=ledger.Snapshot,openingCredits=opening,closingCredits=closing,finished=DateTimeOffset.UtcNow,proof=marker},token);
            await Save(Path.Combine(directory,"external-completion.json"),proof,token);
            await Save(Path.Combine(directory,"settled.json"),new{manual=true,nativeJourneyPassed=false,at=DateTimeOffset.UtcNow},token);
        }
    }
    private static async Task Save(string file,object value,CancellationToken token)
    {await File.WriteAllTextAsync(file+".tmp",JsonSerializer.Serialize(value,ToolboxSettings.JsonOptions),token);File.Move(file+".tmp",file,true);}
}