XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
UTF-8
using System.Globalization;

namespace XFEEliteDangerous.Core;

/// <summary>
/// Bridges the editable private inventory and an explicitly supplied native save.
/// Native cargo row keys retain separate owner/mission/value metadata; two stacks
/// of the same commodity are not silently collapsed. No ships or module IDs are minted.
/// </summary>
public static class NativeCommanderProjection
{
    public static CommanderState CreateInventory(NativeSavegameSnapshot snapshot, NativeSavegameCatalogue catalogue,
        uint accountNativeId, string commanderId, string commanderName, long revision)
    {
        _ = NativeSavegameDocument.Serialize(snapshot, catalogue);
        if (accountNativeId == 0 || snapshot.CommanderNativeId != accountNativeId)
            throw new ArgumentException("Native save belongs to a different private commander.");
        if (snapshot.Credits is < 0 or > 9_000_000_000_000 || string.IsNullOrWhiteSpace(commanderName)
            || commanderName.Length > 64 || commanderName.Any(char.IsControl))
            throw new ArgumentException("Native save cannot be represented by the editable private inventory.");
        var current = snapshot.Vessels.Single(ship => ship.Id == snapshot.CurrentVesselId);
        return new CommanderState
        {
            Id = commanderId, Name = commanderName, Revision = revision, Credits = snapshot.Credits,
            Source = "private-native-save",
            Ships = snapshot.Vessels.Select(ship => new ShipState
            {
                Id = Number(ship.Id), ShipType = ship.Name, Name = ship.Name, Active = ship.Id == snapshot.CurrentVesselId,
                Modules = ship.Modules.Where(slot => slot.Module is not null).Select(slot => InventoryModule(slot.Name, slot.Module!))
                    .Concat(ship.FixedModules.Where(slot => slot.Module is not null)
                        .Select(slot => InventoryModule("fixed:" + slot.Name, slot.Module!))).ToList()
            }).ToList(),
            Cargo = current.Cargo.Select((cargo, index) => new InventoryStack
            {
                Id = CargoKey(index), Name = cargo.Commodity, Quantity = checked((int)cargo.Quantity), Category = "cargo"
            }).ToList()
        };
    }

    public static NativeSavegameSnapshot ApplyInventory(NativeSavegameSnapshot baseline, NativeSavegameCatalogue catalogue,
        uint accountNativeId, CommanderState inventory,bool materialsHandled=false)
    {
        if (accountNativeId == 0 || baseline.CommanderNativeId != accountNativeId)
            throw new ArgumentException("Native save belongs to a different private commander.");
        if (inventory.Ships.Count != baseline.Vessels.Count || inventory.Ships.Select(ship => ship.Id).Distinct().Count() != inventory.Ships.Count
            || inventory.Ships.Count(ship => ship.Active) != 1
            || !inventory.Ships.Any(ship => ship.Active && ship.Id == Number(baseline.CurrentVesselId)))
            throw new ArgumentException("Native vessel membership or active selection changed; import a complete native save instead.");
        if (inventory.Materials.Count != 0&&!materialsHandled)
            throw new ArgumentException("Native material editing requires its typed resource mapping; partial Journal materials cannot replace it.");
        var current = baseline.Vessels.Single(ship => ship.Id == baseline.CurrentVesselId);
        var cargoByKey = inventory.Cargo.ToDictionary(stack => stack.Id, StringComparer.Ordinal);
        var expectedKeys = current.Cargo.Select((_, index) => CargoKey(index)).ToHashSet(StringComparer.Ordinal);
        if (cargoByKey.Keys.Any(key => !expectedKeys.Contains(key)))
            throw new ArgumentException("New native cargo requires an explicit owner and value in a complete native save.");
        var cargo = new List<NativeCargoSnapshot>();
        for (var index = 0; index < current.Cargo.Count; index++)
        {
            var original = current.Cargo[index];
            if (!cargoByKey.TryGetValue(CargoKey(index), out var editable)) continue;
            if (editable.Name != original.Commodity || editable.Category != "cargo" || editable.Quantity is < 0 or > ushort.MaxValue)
                throw new ArgumentException("Native cargo row identity or quantity is invalid.");
            if (editable.Quantity == 0) continue;
            var quantity = (uint)editable.Quantity;
            // Retain the exact total on unchanged rows, including division remainder.
            var total = quantity == original.Quantity ? original.TotalValue
                : original.Quantity == 0 ? 0 : checked(original.TotalValue / original.Quantity * quantity);
            cargo.Add(original with { Quantity = quantity, TotalValue = total });
        }
        var vessels = baseline.Vessels.Select(ship =>
        {
            var editable = inventory.Ships.SingleOrDefault(candidate => candidate.Id == Number(ship.Id))
                ?? throw new ArgumentException("Native vessel was removed from editable inventory.");
            if (editable.ShipType != ship.Name) throw new ArgumentException("Native vessel resource identity changed.");
            var expectedSlots = ship.Modules.Where(slot => slot.Module is not null).Select(slot => slot.Name)
                .Concat(ship.FixedModules.Where(slot => slot.Module is not null).Select(slot => "fixed:" + slot.Name)).ToHashSet(StringComparer.Ordinal);
            if (editable.Modules.Count != expectedSlots.Count || editable.Modules.Select(module => module.Slot).Distinct().Count() != expectedSlots.Count
                || editable.Modules.Any(module => !expectedSlots.Contains(module.Slot)))
                throw new ArgumentException("Native module membership changed; import a complete native save instead.");
            NativeSlotSnapshot MapSlot(NativeSlotSnapshot slot, bool fixedModule)
            {
                if (slot.Module is not { } module) return slot;
                var changed = editable.Modules.Single(candidate => candidate.Slot == (fixedModule ? "fixed:" : "") + slot.Name);
                if (changed.ItemId != module.Name || changed.Priority is < 0 or > 4 || !double.IsFinite(changed.Health)
                    || changed.Health is < 0 or > 1)
                    throw new ArgumentException("Native editable module identity, health or priority is invalid.");
                return slot with { Module = module with { On = changed.Enabled, Priority = (uint)changed.Priority,
                    HealthMillion = checked((uint)Math.Round(changed.Health * 1_000_000, MidpointRounding.AwayFromZero)) } };
            }
            return ship with { Modules = ship.Modules.Select(slot => MapSlot(slot, false)).ToArray(),
                FixedModules = ship.FixedModules.Select(slot => MapSlot(slot, true)).ToArray(),
                Cargo = ship.Id == baseline.CurrentVesselId ? cargo : ship.Cargo };
        }).ToArray();
        var projected = baseline with { Credits = inventory.Credits, Vessels = vessels };
        if (projected.Credits is < 0 or > 9_000_000_000_000) throw new ArgumentException("Native credits exceed private inventory limits.");
        _ = NativeSavegameDocument.Serialize(projected, catalogue);
        return projected;
    }

    private static ModuleState InventoryModule(string slot, NativeModuleSnapshot module)
    {
        if (module.Value > long.MaxValue || module.Priority > 4 || module.HealthMillion > 1_000_000)
            throw new ArgumentException("Native module exceeds editable private inventory limits.");
        return new() { Slot = slot, ItemId = module.Name, Priority = (int)module.Priority, Enabled = module.On,
            Health = module.HealthMillion / 1_000_000d, Value = (long)module.Value };
    }
    private static string Number(uint value) => value.ToString(CultureInfo.InvariantCulture);
    private static string CargoKey(int index) => "native-cargo-" + index.ToString(CultureInfo.InvariantCulture);

    internal static CatalogEntry? CargoDefinition(NativeSavegameSnapshot snapshot, string rowId)
    {
        var current = snapshot.Vessels.Single(ship => ship.Id == snapshot.CurrentVesselId);
        for (var index = 0; index < current.Cargo.Count; index++)
            if (rowId == CargoKey(index)) return new(rowId, current.Cargo[index].Commodity, "cargo");
        return null;
    }
}