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;
}
}
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;
}
}