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

/// <summary>Deterministic journal projection. No network, game memory or account credentials.</summary>
public sealed class PlayerProjector
{
    private readonly FleetProjector fleet = new();
    private readonly CommanderProfileProjector profile=new();
    private readonly XFEEDToolBox.Core.Navigation.NavigationProjector navigation=new();
    private readonly XFEEDToolBox.Core.Engineering.EngineerProjector engineers = new();
    public PlayerState State { get; private set; } = new();
    private DateTimeOffset cargoAt;
    private DateTimeOffset materialAt;
    private DateTimeOffset lockerAt;
    private DateTimeOffset backpackAt;

    public void ConfirmStatusRead(DateTimeOffset eventAt,DateTimeOffset observedAt,DateTimeOffset? writtenAt=null)
    {
        if(State.Status is not null&&State.StatusAt==eventAt)
            State=State with{StatusReadAt=observedAt,StatusWrittenAt=writtenAt};
    }

    public void Apply(JsonElement e)
    {
        var name = e.Text("event");
        var time = e.Time() ?? State.UpdatedAt;
        var s = State;
        switch (name)
        {
            case "Fileheader": s = s with { GameVersion = e.Text("gameversion"), GameBuild = e.Text("build") }; break;
            case "LoadGame":
                var fid = e.Text("FID");
                if (s.CommanderId != fid)
                {
                    s = new() { GameVersion = s.GameVersion, GameBuild = s.GameBuild }; cargoAt = materialAt = lockerAt = backpackAt = default; fleet.Reset(); engineers.Reset(); navigation.Reset();profile.Reset();
                }
                s = s with
                {
                    Commander = e.Text("Commander", s.Commander), CommanderId = fid,
                    Credits = e.Number("Credits") is { } c ? (decimal)c : s.Credits,
                    CreditsReliable = e.Get("Credits").ValueKind == JsonValueKind.Number,
                    Ship = e.Text("Ship_Localised", e.Text("Ship")), InGame = true, CreditsAt = time,
                    GameMode = e.Text("GameMode"), GroupName = e.Text("Group"), LoadGameAt = time,
                    StationServices=[],StationServicesKnown=false
                };
                break;
            case "Loadout":
                var shipName = e.Text("ShipName").Trim();
                s = s with
                {
                    Ship = shipName.Length > 0 ? shipName : e.Text("Ship"),
                    CargoCapacity = (int)e.Long("CargoCapacity", s.CargoCapacity), MaxJumpRange = e.Number("MaxJumpRange") ?? s.MaxJumpRange,
                    Rebuy = (decimal)(e.Number("Rebuy") ?? (double)s.Rebuy),
                    AdvancedDocking = e.Get("Modules").Items().Any(m => m.Text("Item").Equals("int_dockingcomputer_advanced", StringComparison.OrdinalIgnoreCase) && m.Flag("On")),
                    SupercruiseAssist = e.Get("Modules").Items().Any(m => m.Text("Item").Equals("int_supercruiseassist", StringComparison.OrdinalIgnoreCase) && m.Flag("On"))
                };
                break;
            case "Location":
            case "FSDJump":
            case "CarrierJump":
                s = s with { SystemName = e.Text("StarSystem", s.SystemName), Position = e.Position() ?? s.Position, Docked = e.Flag("Docked"), StationName = e.Text("StationName"), MarketId = e.Long("MarketID") is > 0 and var m ? m : null, LocationAt = time,
                    StationServices=e.Flag("Docked")?ReadStationServices(e):[],StationServicesKnown=e.Flag("Docked")&&e.Get("StationServices").ValueKind==JsonValueKind.Array };
                break;
            case "Docked":
                s = s with { Docked = true, SystemName = e.Text("StarSystem", s.SystemName), StationName = e.Text("StationName"), MarketId = e.Long("MarketID"), LocationAt = time,
                    StationServices=ReadStationServices(e),StationServicesKnown=e.Get("StationServices").ValueKind==JsonValueKind.Array };
                break;
            case "Undocked": s = s with { Docked = false, StationName = "", MarketId = null, LocationAt = time,StationServices=[],StationServicesKnown=false }; break;
            case "Cargo" when time >= cargoAt && e.Text("Vessel", "Ship") == "Ship":
                if (e.Get("Inventory").ValueKind == JsonValueKind.Array)
                {
                    var inventory = ReadInventory(e.Get("Inventory"), "货舱");
                    s = s with { Inventory = inventory, CargoCount = (int)e.Long("Count", inventory.Sum(x => x.Count)), CargoReliable = true };
                }
                else s = s with { CargoCount = (int)e.Long("Count", s.CargoCount), CargoReliable = e.Get("Count").ValueKind == JsonValueKind.Number && s.CargoReliable && s.Inventory.Sum(x => x.Count) == e.Long("Count") };
                cargoAt = time;
                s = s with { CargoAt = time };
                break;
            case "MarketBuy":
            case "MarketSell":
                var buy = name == "MarketBuy";
                if (time >= cargoAt)
                {
                    var count = (int)e.Long("Count");
                    s = ChangeCargo(s, e.Text("Type"), e.Text("Type_Localised", e.Text("Type")), buy ? count : -count);
                    cargoAt = time;
                    s = s with { CargoAt = time };
                }
                var total = e.Number(buy ? "TotalCost" : "TotalSale");
                s = total is { } t ? Delta(s, buy ? -(decimal)t : (decimal)t) : s with { CreditsReliable = false };
                break;
            case "CollectCargo":
            case "EjectCargo":
                if (time >= cargoAt)
                {
                    s = ChangeCargo(s, e.Text("Type"), e.Text("Type_Localised", e.Text("Type")), name == "CollectCargo" ? 1 : -(int)e.Long("Count", 1));
                    cargoAt = time;
                    s = s with { CargoAt = time };
                }
                break;
            case "Materials" when time >= materialAt:
                s = s with { Materials = new[] { "Raw", "Manufactured", "Encoded" }.SelectMany(k => ReadInventory(e.Get(k), k)).ToArray(), MaterialsAt = time, MaterialsReliable = true };
                materialAt = time;
                break;
            case "MaterialCollected" or "MaterialDiscarded" when time >= materialAt:
                s = s with { Materials = ChangeInventory(s.Materials, e.Text("Name"), e.Text("Name_Localised", e.Text("Name")), (int)e.Long("Count") * (name == "MaterialCollected" ? 1 : -1), e.Text("Category")), MaterialsAt = time };
                materialAt = time;
                break;
            case "MaterialTrade" when time >= materialAt:
                foreach (var (field, direction) in new[] { ("Paid", -1), ("Received", 1) })
                {
                    var item = e.Get(field);
                    if (item.Text("Material").Length == 0 || item.Get("Quantity").ValueKind != JsonValueKind.Number) { s = s with { MaterialsReliable = false }; continue; }
                    s = s with { Materials = ChangeInventory(s.Materials, item.Text("Material"), item.Text("Material_Localised", item.Text("Material")), (int)item.Long("Quantity") * direction, item.Text("Category")) };
                }
                materialAt = time; s = s with { MaterialsAt = time };
                break;
            case "EngineerCraft" when time >= materialAt:
            case "Synthesis" when time >= materialAt:
                var ingredients = e.Get(name == "EngineerCraft" ? "Ingredients" : "Materials");
                if (ingredients.ValueKind != JsonValueKind.Array) s = s with { MaterialsReliable = false };
                foreach (var item in ingredients.Items()) s = s with { Materials = ChangeInventory(s.Materials, item.Text("Name"), item.Text("Name"), -(int)item.Long("Count"), "") };
                materialAt = time; s = s with { MaterialsAt = time };
                break;
            case "ShipLocker" when time >= lockerAt:
                if (e.Get("Items").ValueKind == JsonValueKind.Array)
                    s = s with { Locker = new[] { "Items", "Components", "Consumables", "Data" }.SelectMany(k => ReadInventory(e.Get(k), k)).ToArray(), LockerReliable = true };
                else s = s with { LockerReliable = false };
                lockerAt = time;
                s = s with { LockerAt = time };
                break;
            case "Backpack" when time >= backpackAt:
                if (e.Get("Items").ValueKind == JsonValueKind.Array)
                    s = s with { Backpack = new[] { "Items", "Components", "Consumables", "Data" }.SelectMany(k => ReadInventory(e.Get(k), k)).ToArray(), BackpackReliable = true };
                else s = s with { BackpackReliable = false };
                backpackAt = time; s = s with { BackpackAt = time };
                break;
            case "Status" when s.StatusAt is null || time >= s.StatusAt:
                var flags = e.Long("Flags");
                if(e.Number("Balance") is >=0 and var balance)s=s with{Credits=(decimal)balance,CreditsReliable=true,CreditsAt=time};
                s = s with { InDanger = (flags & ((1L << 22) | (1L << 23) | (1L << 20))) != 0, StatusAt = time, StatusReadAt=null,StatusWrittenAt=null,
                    Status = new(flags, e.Long("Flags2"), e.Get("Fuel").Number("FuelMain"), e.Get("Fuel").Number("FuelReservoir"), e.Number("Cargo"), e.Number("Latitude"), e.Number("Longitude"), e.Number("Altitude"), e.Number("Heading"), e.Get("Pips").Items().Where(x => x.ValueKind == JsonValueKind.Number).Select(x => x.GetInt32()).ToArray()) };
                break;
            case "UnderAttack":
            case "Interdicted": s = s with { InDanger = true }; break;
            case "Died": s = s with { InDanger = true, InGame = false }; break;
            case "Resurrect":
                // Rebuy resumes the same client without another LoadGame.
                // Location/loadout/status must republish the new ship before
                // any guarded input; the previous wallet is no longer reliable.
                s = s with { InGame = true, InDanger = false, LoadGameAt = time, Docked = false,
                    StationName = "", MarketId = null, StationServices = [], StationServicesKnown = false,
                    CreditsReliable = false, CargoReliable = false, StatusReadAt = null };
                break;
            case "Shutdown": s = s with { InGame = false }; break;
            case "RefuelAll":
            case "RefuelPartial":
            case "Repair":
            case "RepairAll":
            case "RestockVehicle":
            case "BuyAmmo":
            case "BuyDrones":
            case "PayFines":
            case "PayBounties":
            case "FetchRemoteModule":
            case "ShipyardTransfer":
                var expense = e.Number("Cost") ?? e.Number("BuyPrice") ?? e.Number("TransferPrice");
                s = expense is { } cost ? Delta(s, -(decimal)cost) : s with { CreditsReliable = false }; break;
            case "ModuleBuy":
                s = e.Number("BuyPrice") is { } modulePrice ? Delta(s, (decimal)((e.Number("SellPrice") ?? 0) - modulePrice)) : s with { CreditsReliable = false }; break;
            case "ShipyardBuy":
                s = e.Number("ShipPrice") is { } shipPrice ? Delta(s, (decimal)((e.Number("SellPrice") ?? 0) - shipPrice)) : s with { CreditsReliable = false }; break;
            case "MissionCompleted":
                s = Delta(s, (decimal)(e.Number("Reward") ?? 0));
                if (time >= materialAt && e.Get("MaterialsReward").ValueKind == JsonValueKind.Array)
                {
                    foreach (var item in e.Get("MaterialsReward").Items()) s = s with { Materials = ChangeInventory(s.Materials, item.Text("Name"), item.Text("Name_Localised", item.Text("Name")), (int)item.Long("Count"), item.Text("Category")) };
                    materialAt = time; s = s with { MaterialsAt = time };
                }
                break;
            case "RedeemVoucher": s = Delta(s, (decimal)(e.Number("Amount") ?? 0)); break;
            case "ModuleSell": s = Delta(s, (decimal)(e.Number("SellPrice") ?? 0)); break;
            case "SellDrones": s = Delta(s, (decimal)(e.Number("TotalSale") ?? 0)); break;
            case "SellExplorationData":
            case "MultiSellExplorationData": s = Delta(s, (decimal)(e.Number("TotalEarnings") ?? 0)); break;
            // Transactions with complex pricing cannot safely be inferred from one field.
            case "ShipyardSell":
            case "ShipyardSwap":
            case "CarrierBankTransfer":
            case "BuyMicroResources":
            case "SellMicroResources":
            case "TechnologyBroker": s = s with { CreditsReliable = false }; break;
        }
        if (s.Credits != State.Credits) s = s with { CreditsAt = time };
        if (name == "EngineerContribution" && e.Text("Type").Equals("materials", StringComparison.OrdinalIgnoreCase) && time >= materialAt)
        {
            if (e.Text("Material").Length > 0 && e.Number("Quantity") is { } amount) { s = s with { Materials = ChangeInventory(s.Materials, e.Text("Material"), e.Text("Material"), -(int)amount, ""), MaterialsAt = time }; materialAt = time; }
            else s = s with { MaterialsReliable = false };
        }
        if (name == "TechnologyBroker") s = s with { MaterialsReliable = false };
        if (name is "TransferMicroResources" or "BuyMicroResources" or "SellMicroResources" or "TradeMicroResources" or "UseConsumable" or "CollectItems" or "DropItems") s = s with { LockerReliable = false, BackpackReliable = false };
        navigation.Apply(e);
        profile.Apply(e);
        engineers.Apply(e, fleet.Ships, fleet.CurrentShipId, s.MarketId);
        fleet.Apply(e, s.SystemName);
        State = s with { UpdatedAt = time > s.UpdatedAt ? time : s.UpdatedAt, Ships = fleet.Ships, CurrentShipId = fleet.CurrentShipId, StoredModules = fleet.StoredModules, FleetAt = fleet.FleetAt, StoredModulesAt = fleet.StoredModulesAt,
            Engineers = engineers.Engineers, EngineersAt = engineers.UpdatedAt, VerifiedEngineeringRolls = engineers.VerifiedQualityRolls, MismatchedEngineeringRolls = engineers.MismatchedQualityRolls, Navigation=navigation.State,Profile=profile.State };
    }

    private static PlayerState Delta(PlayerState s, decimal delta) => s with { Credits = s.Credits is { } c ? Math.Max(0, c + delta) : null };
    private static IReadOnlyList<string> ReadStationServices(JsonElement e)=>e.Get("StationServices").Items()
        .Where(x=>x.ValueKind==JsonValueKind.String).Select(x=>x.GetString()!.Trim().ToLowerInvariant()).Where(x=>x.Length>0).Distinct().ToArray();
    private static PlayerState ChangeCargo(PlayerState s, string key, string label, int delta) => s with
    {
        Inventory = ChangeInventory(s.Inventory, key, label, delta, "货舱"), CargoCount = Math.Max(0, s.CargoCount + delta)
    };
    private static IReadOnlyList<InventoryItem> ChangeInventory(IReadOnlyList<InventoryItem> list, string key, string name, int delta, string group)
    {
        key = JsonFields.CommodityKey(key);
        var result = list.ToList();
        var at = result.FindIndex(x => x.Key == key && x.MissionId is null && x.Stolen == 0);
        var count = Math.Max(0, (at >= 0 ? result[at].Count : 0) + delta);
        if (at >= 0) result.RemoveAt(at);
        if (count > 0) result.Add(new(key, at >= 0 ? list[at].Name : name, count, Group: group.Length > 0 ? group : at >= 0 ? list[at].Group : "未知分类"));
        return result;
    }
    private static InventoryItem[] ReadInventory(JsonElement array, string group) => array.Items().Select(x =>
        new InventoryItem(JsonFields.CommodityKey(x.Text("Name")), x.Text("Name_Localised", x.Text("Name")), (int)x.Long("Count"), (int)x.Long("Stolen"), group, x.Long("MissionID") is > 0 and var id ? id : null)).ToArray();
}