XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Text.Json;
using System.Text.RegularExpressions;
using XFEEDToolBox.Core.Engineering;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Localization;
namespace XFEEDToolBox.Core.Outfitting;

public sealed record ModuleDefinition(string Item, string Name, string Category, int Size, string Rating, string Mount, string Ship)
{
    public string Display => ChineseNames.Module(Item,Name);
    public string OriginalName => ChineseNames.ModuleEnglish(Item,Name);
    public decimal? ReferencePrice=>ModulePrices.Find(Item);
}
public sealed record ShipShape(Dictionary<string,int> Core, IReadOnlyList<int> Hardpoints)
{
    public IReadOnlyList<int> MiningHardpoints { get; init; } = [];
    public Dictionary<string,int> InternalSlots {get;init;}=[];
}
public sealed record OutfittingCatalog
{
    public static OutfittingCatalog Default { get; } = Read();
    public IReadOnlyList<ModuleDefinition> Modules { get; init; } = [];
    public Dictionary<string,string> ShipNames { get; init; } = [];
    public Dictionary<string,ShipShape> Ships { get; init; } = [];
    private static OutfittingCatalog Read()
    {
        using var stream = typeof(OutfittingCatalog).Assembly.GetManifestResourceStream("XFEEDToolBox.Core.Outfitting.catalog.json")!;
        var result=JsonSerializer.Deserialize<OutfittingCatalog>(stream, ToolboxSettings.JsonOptions)!;
        using var layouts=typeof(OutfittingCatalog).Assembly.GetManifestResourceStream("XFEEDToolBox.Core.Outfitting.Edsy.slot-reference.json")!;
        foreach(var (type,shape) in JsonSerializer.Deserialize<Dictionary<string,ShipShape>>(layouts,ToolboxSettings.JsonOptions)!)
            result.Ships[type]=result.Ships.TryGetValue(type,out var old)?old with{InternalSlots=shape.InternalSlots}:shape;
        return result;
    }
    public ModuleDefinition? Find(string item) => Modules.FirstOrDefault(m => m.Item.Equals(item.Trim('$',';'), StringComparison.OrdinalIgnoreCase));
}
public sealed record RefitModuleChoice(string InstanceKey, string Origin, FittedModule Module, ModuleDefinition Definition)
{
    public string IconItem=>Definition.Item;
    public string CardName=>Definition.Display;
    public string CardOriginal=>Definition.OriginalName;
    public string SlotSize=>Definition.Size.ToString();
    public string CurrentGrade=>Module.Engineering is {Level:>0} e?$"G{e.Level}":"";
    public bool Owned => InstanceKey.Length > 0;
    public string Display => Definition.Display + " · " + Origin + (Module.Engineering is { } e ? $" · {e.Summary}" : "");
    public string OriginalName => Definition.OriginalName+(Module.Engineering is {} e?" · "+e.EnglishSummary:"");
}
public sealed record RefitTarget(string Blueprint, int Grade, double Quality, string Experimental, long EngineerId, bool Remote, bool FastAdvance);
public sealed record RefitSlot
{
    public string Slot { get; init; } = "";
    public string Category { get; init; } = "";
    public int Capacity { get; init; }
    public string Family { get; init; } = "";
    public bool MiningOnly { get; init; }
    public FittedModule? Original { get; init; }
    public FittedModule? Module { get; init; }
    public string InstanceKey { get; init; } = "";
    public string Origin { get; init; } = "原船装配";
    public RefitTarget? Target { get; init; }
    public string Caption => $"{ChineseNames.Slot(Slot)} · {Module?.DisplayName ?? "空槽"}";
    public string OriginalName => ChineseNames.SlotEnglish(Slot)+" · "+(Module?.OriginalName??"Empty");
    public string Summary => (Module is null ? "未安装" : Target is null ? Module.EngineeringText : $"{ChineseNames.Translate(Target.Blueprint)} · G{Target.Grade} {Target.Quality:P0} · " + (Target.Experimental.Length > 0 ? ChineseNames.Translate(Target.Experimental) : "无实验效果")) + $" · {Origin}";
    public string OriginalSummary => Module is null?"Empty":Target is null?Module.EngineeringOriginal:$"{ChineseNames.English(Target.Blueprint)} · G{Target.Grade} · {(Target.Experimental.Length>0?ChineseNames.English(Target.Experimental):"None")}";
    public bool CanEdit => Category.Length > 0;
    public string SlotSize=>Capacity.ToString();
    public string CurrentGrade=>Module?.Engineering is {Level:>0} e?$"G{e.Level}":"";
    public string CardName=>Module?.DisplayName??"空槽";
    public string CardOriginal=>Module?.OriginalName??"Empty slot";
    public string IconItem=>Module?.Item??"";
}
public sealed record ShipRefitDocument(long ShipId, string ShipType, string Name, DateTimeOffset? SourceAt, IReadOnlyList<RefitSlot> Slots)
{
    // Retain EDSY-only metadata and untouched module hashes when editing an imported build.
    public string EdsyHash { get; init; } = "";
    public string Ident { get; init; } = "";
}
public sealed record RefitModuleResult(string Slot, string Module, int Rolls, int FinalEngineerRank, string Status)
{
    public string SlotName => ChineseNames.Slot(Slot);
    public string SlotOriginal => ChineseNames.SlotEnglish(Slot);
    public string Detail => $"{Rolls} 次改装 · 结束权限 G{FinalEngineerRank} · {Status}";
}
public sealed record ShipRefitResult(IReadOnlyList<MaterialNeed> Materials, IReadOnlyList<RefitModuleResult> Modules, IReadOnlyList<string> Notes, bool Complete, bool Exact);

public static class ShipRefit
{
    public static ShipRefitDocument RefreshOwnedProgress(ShipRefitDocument document,PlayerState player)
    {
        var changed=false;
        var slots=document.Slots.Select(slot=>
        {
            var parts=slot.InstanceKey.Split(':',3);
            if(slot.Module is null||parts.Length!=3||parts[0]!="ship"||!long.TryParse(parts[1],out var shipId))return slot;
            var ship=player.Ships.FirstOrDefault(s=>s.ShipId==shipId&&s.ConfigurationAt is not null&&!s.ConfigurationChanged);
            var actual=ship?.Modules.FirstOrDefault(m=>m.Slot==parts[2]&&m.Item.Equals(slot.Module.Item,StringComparison.OrdinalIgnoreCase));
            if(actual is null)return slot;
            var current=actual with{Slot=slot.Slot};
            if(current==slot.Module)return slot;
            changed=true;
            return slot with{Module=current,Original=slot.Original?.Item.Equals(actual.Item,StringComparison.OrdinalIgnoreCase)==true?current:slot.Original};
        }).ToArray();
        return changed?document with{Slots=slots,SourceAt=player.Ships.FirstOrDefault(s=>s.ShipId==document.ShipId)?.ConfigurationAt??document.SourceAt}:document;
    }
    public static string CoreFamily(string slot)=>slot switch {"Armour"=>"Armour","PowerPlant"=>"Power Plant","MainEngines"=>"Thrusters","FrameShiftDrive"=>"Frame Shift Drive","LifeSupport"=>"Life Support","PowerDistributor"=>"Power Distributor","Radar"=>"Sensors","FuelTank"=>"Fuel Tank",_=>""};
    public static ShipRefitDocument Create(ShipRecord ship, OutfittingCatalog catalog)
    {
        if (ship.ConfigurationAt is null || ship.ConfigurationChanged) throw new InvalidOperationException("这艘飞船尚无有效完整装配快照,不能建立模拟配置。");
        var shape=catalog.Ships.GetValueOrDefault(ship.Type.ToLowerInvariant()) ?? catalog.Ships.FirstOrDefault(p=>EngineeringCatalog.Key(catalog.ShipNames.GetValueOrDefault(p.Key,p.Key))==EngineeringCatalog.Key(ship.Type)).Value;
        var slots=ship.Modules.Select(m =>
        {
            var definition = catalog.Find(m.Item);
            var capacity=shape?.Core.GetValueOrDefault(m.Slot,definition?.Size??0)??definition?.Size??0;
            var size=Regex.Match(m.Slot,@"_Size(\d+)$");if(size.Success)capacity=int.Parse(size.Groups[1].Value);
            foreach(var (prefix,value) in new[]{("TinyHardpoint",0),("SmallHardpoint",1),("MediumHardpoint",2),("LargeHardpoint",3),("HugeHardpoint",4)})if(m.Slot.StartsWith(prefix,StringComparison.Ordinal))capacity=value;
            return new RefitSlot { Slot = m.Slot, Module = m, Original = m, InstanceKey = $"ship:{ship.ShipId}:{m.Slot}", Category = definition?.Category ?? "", Capacity = capacity,
                Family = EngineeringCatalog.ModuleType(m.Item).Length > 0 ? EngineeringCatalog.ModuleType(m.Item) : definition?.Name ?? "" };
        }).ToList();
        if(shape is not null)
        {
            foreach(var (slot,capacity) in shape.InternalSlots)
                if(!slots.Any(s=>s.Slot.Equals(slot,StringComparison.OrdinalIgnoreCase)||slot.StartsWith("Slot")&&s.Slot.StartsWith(slot.Split('_')[0]+"_",StringComparison.OrdinalIgnoreCase)))
                    slots.Add(new(){Slot=slot,Capacity=capacity,Category="internal",Origin="空槽位"});
            var counts=new Dictionary<int,int>();
            for(var point=0;point<shape.Hardpoints.Count;point++)
            {
                var capacity=shape.Hardpoints[point];
                var number=counts.GetValueOrDefault(capacity)+1;counts[capacity]=number;
                var prefix=capacity switch{0=>"Tiny",1=>"Small",2=>"Medium",3=>"Large",4=>"Huge",_=>""};if(prefix.Length==0)continue;
                var slot=prefix+"Hardpoint"+number;
                var existing=slots.FindIndex(s=>s.Slot==slot);
                if(existing<0)slots.Add(new(){Slot=slot,Capacity=capacity,Category=capacity==0?"utility":"hardpoint",Origin="参考空槽",MiningOnly=shape.MiningHardpoints.Contains(point)});
                else if(shape.MiningHardpoints.Contains(point))slots[existing]=slots[existing] with{MiningOnly=true};
            }
        }
        return new(ship.ShipId,ship.Type,ship.Name,ship.ConfigurationAt,slots){Ident=ship.Ident};
    }
    public static bool Fits(ShipRefitDocument ship, RefitSlot slot, ModuleDefinition module, OutfittingCatalog catalog)
    {
        if (!slot.CanEdit || module.Category != slot.Category || module.Size > slot.Capacity) return false;
        if((slot.Slot=="PlanetaryApproachSuite")!=module.Item.StartsWith("int_planetapproachsuite",StringComparison.OrdinalIgnoreCase))return false;
        if(slot.MiningOnly&&!new[]{"Mining","Abrasion","Seismic","Displacement","Pulse Wave"}.Any(s=>module.Name.Contains(s,StringComparison.OrdinalIgnoreCase)))return false;
        if (module.Ship.Length > 0 && !EngineeringCatalog.Key(module.Ship).Equals(EngineeringCatalog.Key(catalog.ShipNames.GetValueOrDefault(ship.ShipType.ToLowerInvariant(), ship.ShipType)), StringComparison.Ordinal)) return false;
        if (slot.Category == "standard")
        {
            var family = EngineeringCatalog.ModuleType(module.Item);
            if (family.Length == 0) family = module.Name;
            if (family != slot.Family) return false;
            if (slot.Slot is "Radar" or "LifeSupport" && module.Size != slot.Capacity) return false;
        }
        if (slot.Slot.StartsWith("Military",StringComparison.OrdinalIgnoreCase) && !(module.Name.Contains("Reinforcement",StringComparison.OrdinalIgnoreCase) || module.Name.Contains("Shield Cell",StringComparison.OrdinalIgnoreCase))) return false;
        return true;
    }
    public static ShipRefitDocument Install(ShipRefitDocument document, string slotName, RefitModuleChoice? choice, OutfittingCatalog catalog)
    {
        var slot = document.Slots.Single(s => s.Slot == slotName);
        if (!slot.CanEdit) throw new InvalidOperationException("该槽位没有可验证的模块类型信息。");
        if (choice is not null)
        {
            if (!Fits(document,slot,choice.Definition,catalog)) throw new InvalidOperationException("模块类型、尺寸或舰船限制与槽位不匹配。");
            if (choice.Owned && document.Slots.Any(s => s.Slot != slotName && s.Module is not null && s.InstanceKey == choice.InstanceKey)) throw new InvalidOperationException("这个已拥有模块已装入其他模拟槽位,请先卸下;同一模块不会复制成两件。");
        }
        var replacement = slot with { Module = choice is null ? null : choice.Module with { Slot = slotName }, InstanceKey = choice?.InstanceKey ?? "", Origin = choice?.Origin ?? "模拟卸下", Target = null };
        return document with { Slots = document.Slots.Select(s => s.Slot == slotName ? replacement : s).ToArray() };
    }
    public static ShipRefitDocument SetTarget(ShipRefitDocument document,string slot,RefitTarget? target)
    {
        var current = document.Slots.Single(s => s.Slot == slot);
        if (current.Module is null) throw new InvalidOperationException("请先在此槽位安装模拟模块。");
        return document with { Slots = document.Slots.Select(s => s.Slot == slot ? s with { Target = target } : s).ToArray() };
    }
    public static IReadOnlyList<RefitModuleChoice> Choices(PlayerState p,OutfittingCatalog catalog)
    {
        var choices = new List<RefitModuleChoice>();
        foreach (var ship in p.Ships)
            foreach (var module in ship.Modules)
                if (catalog.Find(module.Item) is { } definition) choices.Add(new($"ship:{ship.ShipId}:{module.Slot}", $"现有飞船 {ship.DisplayName} / {module.SlotName}",module,definition));
        foreach (var stored in p.StoredModules)
            if (catalog.Find(stored.Item) is { } definition) choices.Add(new($"store:{stored.StorageSlot}", $"寄存仓库 #{stored.StorageSlot} · {stored.SystemName}",new() { Item=stored.Item,Name=stored.Name,Enabled=true,
                Engineering=stored.Engineering.Length==0?null:new() { Blueprint=stored.Engineering,Level=stored.EngineeringLevel??0,Quality=stored.EngineeringQuality } },definition));
        choices.AddRange(catalog.Modules.Select(m => new RefitModuleChoice("","计划购入",new() { Item=m.Item,Name=m.Display,Enabled=true },m)));
        return choices;
    }
}