using System.Text.Json;
using XFEEDToolBox.Core.Presentation;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using XFEEDToolBox.Core.Engineering;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Outfitting;
using XFEEDToolBox.Core.Localization;
namespace XFEEDToolBox.ViewModels;
public sealed partial class RefitCategoryRow(string key,string name,string originalName) : ObservableObject
{
public string Key {get;}=key;
public string Name {get;}=name;
public string OriginalName {get;}=originalName;
public string Icon => XFEEDToolBox.Core.Rendering.GameUiIcons.Asset(Key switch { "hardpoint" => XFEEDToolBox.Core.Rendering.GameUiIcon.OutfittingHardpoint, "utility" => XFEEDToolBox.Core.Rendering.GameUiIcon.OutfittingUtility, "standard" => XFEEDToolBox.Core.Rendering.GameUiIcon.OutfittingCore, _ => XFEEDToolBox.Core.Rendering.GameUiIcon.OutfittingOptional });
[ObservableProperty] public partial string Count {get;set;}="0 已安装 / 0 槽位";
}
public partial class ToolboxViewModel
{
private readonly OutfittingCatalog outfitting = OutfittingCatalog.Default;
private ShipRefitDocument? refitDocument;
private IReadOnlyList<RefitModuleChoice> refitChoices = [];
private bool loadingRefit;
private bool publishingRefit;
private int refitVersion;
private int calculatedRefitVersion = -1;
private string refitCommander = "";
private string refitPlayerFingerprint = "";
public ShipRefitDocument? RefitDocument => refitDocument;
internal void LoadRefitProbeDocument(ShipRefitDocument document)
{
if(!Environment.GetCommandLineArgs().Any(a=>a is "--smoke-refinement" or "--smoke-interface"))throw new InvalidOperationException("仅限独立界面验收。");
refitDocument=document;PublishRefitSlots(null);LoadRefitEditor();
}
internal void RefreshRefitProbePlayer(PlayerState player)
{
if(!Environment.GetCommandLineArgs().Any(a=>a is "--smoke-refinement" or "--smoke-interface"))throw new InvalidOperationException("仅限独立界面验收。");
RefreshRefitPlayer(player);
}
[ObservableProperty] public partial string RefitShipType { get; set; }="";
[ObservableProperty] public partial ShipLiveRow? RefitSourceShip { get; set; }
[ObservableProperty] public partial IReadOnlyList<RefitSlot> RefitSlots { get; set; } = [];
public LiveCollection<RefitSlotLiveRow,RefitSlot,string> RefitVisibleSlots { get; } = new(s=>s.Slot,s=>new(s));
[ObservableProperty] public partial int RefitCategoryIndex { get; set; }
public IReadOnlyList<RefitCategoryRow> RefitCategories { get; } = [new("hardpoint","武器挂点","Hardpoints"),new("utility","多功能槽位","Utility mounts"),new("standard","核心内部模块","Core internal"),new("internal","可选内部模块","Optional internal")];
[ObservableProperty] public partial string RefitCategoryName { get; set; } = "武器挂点";
[ObservableProperty] public partial string RefitCategoryOriginal { get; set; } = "Hardpoints";
[ObservableProperty] public partial string RefitEdsyLink { get; set; } = "";
[ObservableProperty] public partial string RefitEdsyStatus { get; set; } = "导入 EDSY 装配链接,或导出当前模拟配置。";
[ObservableProperty] public partial bool RefitEdsyBusy { get; set; }
[ObservableProperty] public partial string RefitSlotSearch { get; set; } = "";
[ObservableProperty] public partial string RefitSlotCount { get; set; } = "暂无模拟配置";
[ObservableProperty] public partial RefitSlotLiveRow? RefitSlot { get; set; }
[ObservableProperty] public partial string RefitStatus { get; set; } = "选择已有装配的飞船,建立本机模拟副本。";
[ObservableProperty] public partial string RefitModuleSearch { get; set; } = "";
[ObservableProperty] public partial bool RefitOwnedOnly { get; set; }
[ObservableProperty] public partial IReadOnlyList<RefitModuleChoice> RefitAvailableModules { get; set; } = [];
[ObservableProperty] public partial IReadOnlyList<RefitModuleType> RefitModuleTypes { get; set; } = [];
[ObservableProperty] public partial IReadOnlyList<RefitModuleChoice> RefitTypeModels { get; set; } = [];
[ObservableProperty,NotifyPropertyChangedFor(nameof(RefitChoosingType)),NotifyPropertyChangedFor(nameof(RefitChoosingModel))]
public partial RefitModuleType? RefitSelectedModuleType { get; set; }
public bool RefitChoosingType=>RefitSelectedModuleType is null;
public bool RefitChoosingModel=>!RefitChoosingType;
partial void OnRefitSelectedModuleTypeChanged(RefitModuleType? value)=>FilterRefitTypeModels();
[ObservableProperty] public partial RefitModuleChoice? RefitInstallModule { get; set; }
[ObservableProperty] public partial string RefitSlotCaption { get; set; } = "选择一个模拟槽位。";
[ObservableProperty] public partial string RefitSlotOriginal { get; set; }="";
[ObservableProperty] public partial string RefitSelectedName { get; set; } = "选择一个模块";
[ObservableProperty] public partial string RefitSelectedOriginal { get; set; } = "";
[ObservableProperty] public partial string RefitEngineeringName { get; set; } = "未安装模块";
[ObservableProperty] public partial string RefitEngineeringOriginal { get; set; } = "";
[ObservableProperty] public partial string RefitTargetSummary { get; set; } = "保持当前装配";
[ObservableProperty] public partial string RefitTargetOriginal { get; set; } = "";
public LiveCollection<RefitPropertyLiveRow,RefitPropertyRow,string> RefitProperties { get; } = new(s=>s.OriginalName,s=>new(s));
public LiveCollection<RefitModifierLiveRow,EngineeringModifier,string> RefitModifiers { get; } = new(s=>s.Label,s=>new(s));
[ObservableProperty] public partial bool RefitHasModifiers { get; set; }
[ObservableProperty] public partial bool RefitCanEngineer { get; set; }
[ObservableProperty] public partial string RefitEngineeringHint { get; set; } = "先选择已安装的模块。";
[ObservableProperty] public partial bool RefitEditEnabled { get; set; }
[ObservableProperty] public partial IReadOnlyList<string> RefitBlueprints { get; set; } = [];
[ObservableProperty] public partial string? RefitBlueprint { get; set; }
[ObservableProperty] public partial double RefitGrade { get; set; } = 5;
[ObservableProperty] public partial double RefitQuality { get; set; } = 100;
[ObservableProperty] public partial IReadOnlyList<EngineerLiveRow> RefitEngineers { get; set; } = [];
[ObservableProperty] public partial EngineerLiveRow? RefitEngineer { get; set; }
[ObservableProperty] public partial IReadOnlyList<EngineeringEffectChoice> RefitEffects { get; set; } = [];
[ObservableProperty] public partial EngineeringEffectChoice? RefitEffect { get; set; }
[ObservableProperty] public partial bool RefitRemote { get; set; }
[ObservableProperty] public partial bool RefitFast { get; set; } = true;
[ObservableProperty] public partial bool RefitBusy { get; set; }
public bool RefitCanAddToList=>!RefitBusy&&!UpgradeBusy;
partial void OnRefitBusyChanged(bool value)=>OnPropertyChanged(nameof(RefitCanAddToList));
[ObservableProperty] public partial double RefitCurrentProgress {get;set;}
[ObservableProperty] public partial string RefitCurrentProgressText {get;set;}="—";
[ObservableProperty] public partial string RefitResult { get; set; } = "模拟安装或修改改装目标后,统计整船材料。";
[ObservableProperty] public partial string RefitNotes { get; set; } = "按游戏槽位顺序核对材料与共享工程师声望。";
[ObservableProperty] public partial IReadOnlyList<MaterialNeed> RefitNeeds { get; set; } = [];
[ObservableProperty] public partial IReadOnlyList<RefitModuleResult> RefitModuleResults { get; set; } = [];
private void RefreshRefitPlayer(PlayerState p)
{
if (refitCommander.Length > 0 && p.CommanderId != refitCommander) { refitDocument=null; PublishRefitSlots(null); RefitNeeds=[]; RefitModuleResults=[]; }
refitCommander=p.CommanderId;
var fingerprint=$"{p.MaterialsAt}:{p.MaterialsReliable}:{p.EngineersAt}:{p.MismatchedEngineeringRolls}";
if(fingerprint!=refitPlayerFingerprint)
{
refitPlayerFingerprint=fingerprint;
if(RefitModuleResults.Count>0)
{
InvalidateRefitResult();
RefitResult="仓库或工程师权限已更新,重新统计整船材料以核对当前缺口。";
}
}
loadingRefit=true;
try { RefitSourceShip=refitDocument is {EdsyHash.Length:>0}?null:Ships.FirstOrDefault(s=>s.ShipId==(refitDocument?.ShipId??p.CurrentShipId))??Ships.FirstOrDefault(); }
finally { loadingRefit=false; }
refitChoices=ShipRefit.Choices(p,outfitting); FilterRefitModules();
if(refitDocument is null && RefitSourceShip is { ConfigurationAt:not null,ConfigurationChanged:false }) RebuildRefit();
if(refitDocument is not null)
{
var updated=ShipRefit.RefreshOwnedProgress(refitDocument,p);
if(!ReferenceEquals(updated,refitDocument))
{
var selected=RefitSlot?.Slot;
SaveRefitEditor(false);
refitDocument=ShipRefit.RefreshOwnedProgress(refitDocument!,p);
PublishRefitSlots(selected,reloadEditor:false);InvalidateRefitResult();
}
}
if(refitDocument is { EdsyHash.Length: >0 } imported) RefitStatus=$"{ChineseNames.Ship(imported.ShipType)} · {imported.Name} · EDSY 导入 · {imported.Slots.Count(s=>s.Module is not null)} 个已安装模块 · 本机模拟副本";
else if(refitDocument is not null && RefitSourceShip is { } source)
{
var changed=source.Modules.Any(m=>refitDocument.Slots.FirstOrDefault(s=>s.Slot==m.Slot)?.Original is not { } old || old.Item!=m.Item || old.Engineering?.Blueprint!=m.Engineering?.Blueprint || old.Engineering?.Level!=m.Engineering?.Level || old.Engineering?.Quality!=m.Engineering?.Quality || old.Engineering?.ExperimentalEffectId!=m.Engineering?.ExperimentalEffectId);
RefitStatus=$"{source.DisplayName} · 基线 {refitDocument.SourceAt?.ToLocalTime():MM-dd HH:mm:ss} · {refitDocument.Slots.Count(s=>s.Module is not null)} 个模拟模块"+(changed?"\n游戏装配已有新变化,当前模拟仍保留原基线;可用最新装配重建。":"\n本机模拟副本,材料按当前仓库数量核对。");
}
RefreshRefitOverview(p);
}
partial void OnRefitSourceShipChanged(ShipLiveRow? value) { if(!loadingRefit && value is not null && value.ShipId!=refitDocument?.ShipId) RebuildRefit(); }
[RelayCommand] private void RebuildRefit()
{
try
{
var source=RefitSourceShip??Ships.FirstOrDefault(s=>s.ShipId==CurrentPlayer.CurrentShipId)??Ships.FirstOrDefault()??throw new InvalidOperationException("请选择飞船。");
loadingRefit=true;try{RefitSourceShip=source;}finally{loadingRefit=false;}
refitDocument=ShipRefit.Create(source.Record,outfitting); PublishRefitSlots(null); InvalidateRefitResult();
RefitStatus=source.DisplayName+" · 已建立模拟副本";
}
catch(Exception e) { refitDocument=null;PublishRefitSlots(null);RefitStatus=e.Message; }
}
private void PublishRefitSlots(string? selected,bool reloadEditor=true)
{
publishingRefit=true;
try {
RefitShipType=refitDocument?.ShipType??"";
RefitSlots=refitDocument?.Slots??[];
foreach(var row in RefitCategories)row.Count=$"{RefitSlots.Count(s=>s.Category==row.Key&&s.Module is not null)} 已安装 / {RefitSlots.Count(s=>s.Category==row.Key)} 槽位";
FilterRefitSlots(selected);
} finally { publishingRefit=false; }
if(reloadEditor)LoadRefitEditor();else UpdateRefitCurrentValues();
}
partial void OnRefitCategoryIndexChanged(int value) => FilterRefitSlots(RefitSlot?.Slot);
partial void OnRefitSlotSearchChanged(string value) => FilterRefitSlots(RefitSlot?.Slot);
private void FilterRefitSlots(string? selected)
{
var index=Math.Clamp(RefitCategoryIndex,0,3);
var category=new[]{"hardpoint","utility","standard","internal"}[index];
RefitCategoryName=new[]{"武器挂点","多功能槽位","核心内部模块","可选内部模块"}[index];
RefitCategoryOriginal=new[]{"Hardpoints","Utility mounts","Core internal","Optional internal"}[index];
var search=RefitSlotSearch.Trim();
RefitVisibleSlots.Apply(RefitSlots.Where(s=>(category.Length==0||s.Category==category)&&
(search.Length==0||s.Caption.Contains(search,StringComparison.OrdinalIgnoreCase)||s.OriginalName.Contains(search,StringComparison.OrdinalIgnoreCase))));
RefitSlotCount=$"显示 {RefitVisibleSlots.Count} / {RefitSlots.Count} 个槽位";
RefitSlot=RefitVisibleSlots.FirstOrDefault(s=>s.Slot==selected)??RefitVisibleSlots.FirstOrDefault();
}
partial void OnRefitSlotChanging(RefitSlotLiveRow? oldValue,RefitSlotLiveRow? newValue)
{
if(loadingRefit||publishingRefit||oldValue is null||refitDocument is null)return;
try {SaveRefitEditor(false);}catch(Exception e){RefitResult=e.Message;}
}
partial void OnRefitSlotChanged(RefitSlotLiveRow? value){if(!publishingRefit)LoadRefitEditor();}
private void LoadRefitEditor()
{
loadingRefit=true;
try
{
var slot=refitDocument?.Slots.FirstOrDefault(s=>s.Slot==RefitSlot?.Slot)??RefitSlot?.Record; var type=EngineeringCatalog.ModuleType(slot?.Module?.Item??"");
RefitSlotCaption=slot is null?"选择一个模拟槽位。":ChineseNames.Slot(slot.Slot);
RefitSlotOriginal=slot is null?"":ChineseNames.SlotEnglish(slot.Slot);
RefitSelectedName=slot?.Module?.DisplayName??(slot is null?"选择一个模块":"空槽");
RefitSelectedOriginal=slot?.Module?.OriginalName??(slot is null?"":"Empty slot");
RefitEngineeringName=slot?.Module?.EngineeringText??"未安装模块";
RefitEngineeringOriginal=slot?.Module?.EngineeringOriginal??"Empty slot";
RefitTargetSummary=slot?.Summary??"选择左侧槽位查看属性";
RefitTargetOriginal=slot?.OriginalSummary??"";
LoadRefitProperties(slot);
RefitCurrentProgress=(slot?.Module?.Engineering?.Quality??0)*100;
RefitCurrentProgressText=slot?.Module is null?"—":slot.Module.Engineering is null?"0%":slot.Module.Engineering.Quality is {} q?q.ToString("P0"):"—";
RefitBlueprints=StableSequence(RefitBlueprints,engineeringCatalog.Recipes.Where(r=>r.Type==type && r.Grade>0).Select(r=>r.Name).Distinct().Order());
RefitCanEngineer=slot?.Module is not null&&RefitBlueprints.Count>0;
RefitEngineeringHint=slot?.Module is null?"点击槽位卡片右下角的更改按钮选择模块。":RefitCanEngineer?"目标保存在模拟配置中,按游戏槽位顺序计算材料及共享工程师声望。":"该模块没有可用的工程蓝图。";
var current=engineeringCatalog.Recipes.FirstOrDefault(r=>r.Type==type && EngineeringCatalog.SameBlueprint(r,slot?.Module?.Engineering));
RefitBlueprint=slot?.Target?.Blueprint??current?.Name??RefitBlueprints.FirstOrDefault();
RefitEditEnabled=slot?.Target is not null;
RefitGrade=slot?.Target?.Grade??(slot?.Module?.Engineering?.Level is >0 and var level?level:0);
RefitQuality=slot?.Target is { } t?t.Quality*100:100;
RefreshRefitEngineeringChoices(slot?.Target?.EngineerId,slot?.Target?.Experimental);
RefitRemote=slot?.Target?.Remote??false;RefitFast=slot?.Target?.FastAdvance??true;
}
finally{loadingRefit=false;}
FilterRefitModules();
}
private void UpdateRefitCurrentValues()
{
// A Journal update changes the current values without resetting the user's target controls.
var slot=refitDocument?.Slots.FirstOrDefault(s=>s.Slot==RefitSlot?.Slot);
RefitSelectedName=slot?.Module?.DisplayName??"空槽";RefitSelectedOriginal=slot?.Module?.OriginalName??"Empty slot";
RefitEngineeringName=slot?.Module?.EngineeringText??"未安装模块";RefitEngineeringOriginal=slot?.Module?.EngineeringOriginal??"Empty slot";
RefitCurrentProgress=(slot?.Module?.Engineering?.Quality??0)*100;
RefitCurrentProgressText=slot?.Module is null?"—":slot.Module.Engineering is null?"0%":slot.Module.Engineering.Quality is {} q?q.ToString("P0"):"—";
LoadRefitProperties(slot);
}
private void LoadRefitProperties(RefitSlot? slot)
{
var rows=new List<RefitPropertyRow>();
if(slot is not null)
{
rows.Add(new("模块类别","Module category",ChineseNames.Category(slot.Category),slot.Category switch{"hardpoint"=>"Hardpoint","utility"=>"Utility mount","standard"=>"Core internal","internal"=>"Optional internal",_=>ChineseNames.English(slot.Category)}));
rows.Add(new("槽位容量","Slot size",slot.Capacity.ToString()));
if(slot.MiningOnly)rows.Add(new("槽位限制","Slot restriction","仅限采矿模块"));
if(slot.Module is { } module)
{
if(outfitting.Find(module.Item) is { } definition)
{
rows.Add(new("模块规格","Class / rating",$"{definition.Size}{definition.Rating}"));
if(definition.Mount.Length>0)rows.Add(new("武器挂载","Mount",ChineseNames.Translate(definition.Mount),definition.Mount));
}
rows.Add(new("运行状态","Power state",module.State,$"{(module.Enabled?"Enabled":"Disabled")} · Priority {module.Priority+1}"));
rows.Add(new("模块完整度","Integrity",module.HealthText));
rows.Add(new("模块来源","Source",slot.Origin));
rows.Add(new("记录价值","Recorded value",module.Value is {} value?$"{value:N0} Cr":"未记录"));
if(module.AmmoInClip is not null||module.AmmoInHopper is not null)rows.Add(new("弹匣 / 储备","Clip / reserve",module.AmmoText));
}
}
_=PublishRefitPropertiesAsync(slot,rows,++refitPropertiesRequest);
RefitModifiers.Apply(slot?.Module?.Engineering?.Modifiers??[]);
RefitHasModifiers=RefitModifiers.Count>0;
}
private int refitPropertiesRequest;
private ShipRefitDocument? refitStatisticsDocument;
private Task<EquipmentStatistics?>? refitStatisticsTask;
private async Task PublishRefitPropertiesAsync(RefitSlot? slot,List<RefitPropertyRow> metadata,int request)
{
var document=refitDocument;
EquipmentStatistics? stats=null;
if(document is not null)
{
if(!ReferenceEquals(document,refitStatisticsDocument)||refitStatisticsTask is null)
{
refitStatisticsDocument=document;
refitStatisticsTask=Task.Run<EquipmentStatistics?>(()=>
{
try{return EdsyLinkCodec.Statistics(document);}
catch(Exception){return null;}
});
}
stats=await refitStatisticsTask;
}
if(request!=refitPropertiesRequest)return;
var values=slot is not null&&stats?.Modules.TryGetValue(slot.Slot,out var found)==true?found:null;
RefitProperties.Apply(values is null?metadata:EquipmentStatusRows.Module(values).Concat(metadata));
PublishRefitOverview(document,stats);
}
private void RefreshRefitEngineeringChoices(long? engineerId,string? effect)
{
var type=EngineeringCatalog.ModuleType(RefitSlot?.Module?.Item??"");
var names=engineeringCatalog.Recipes.Where(r=>r.Type==type && r.Name==RefitBlueprint && r.Grade>0).SelectMany(r=>r.Engineers).Distinct().ToArray();
RefitEngineers=StableSequence(RefitEngineers,EngineerAccessList.Where(e=>names.Any(n=>EngineeringCatalog.SameEngineer(n,e.Name))).OrderByDescending(e=>e.Unlocked).ThenByDescending(e=>e.Rank));
RefitEngineer=RefitEngineers.FirstOrDefault(e=>e.Id==engineerId)??RefitEngineers.FirstOrDefault();
var actual=effect??EngineeringExperimental.Current(RefitSlot?.Module?.Engineering);
RefitEffects=BuildEngineeringEffects(type,actual);
RefitEffect=RefitEffects.FirstOrDefault(e=>EngineeringExperimental.Same(engineeringCatalog,type,e.Key,actual))??RefitEffects[0];
}
partial void OnRefitBlueprintChanged(string? value)
{
if(loadingRefit)return;
loadingRefit=true;try{RefreshRefitEngineeringChoices(RefitEngineer?.Id,RefitEffect?.Key);}finally{loadingRefit=false;}
RefitTargetEdited(true);
}
partial void OnRefitGradeChanged(double value)=>RefitTargetEdited(true);
partial void OnRefitQualityChanged(double value)=>RefitTargetEdited(true);
partial void OnRefitEffectChanged(EngineeringEffectChoice? value)=>RefitTargetEdited(true);
partial void OnRefitEngineerChanged(EngineerLiveRow? value)=>RefitTargetEdited();
partial void OnRefitEditEnabledChanged(bool value)=>RefitTargetEdited();
partial void OnRefitRemoteChanged(bool value)=>RefitTargetEdited();
partial void OnRefitFastChanged(bool value)=>RefitTargetEdited();
private void RefitTargetEdited(bool enable=false)
{
if(loadingRefit||publishingRefit)return;
loadingRefit=true;
try{if(enable)RefitEditEnabled=true;if(RefitEditEnabled&&RefitGrade<1)RefitGrade=1;}
finally{loadingRefit=false;}
try
{
SaveRefitEditor(false);
if(refitDocument?.Slots.FirstOrDefault(s=>s.Slot==RefitSlot?.Slot) is {} slot)
{RefitSlot?.Update(slot);RefitTargetSummary=slot.Summary;RefitTargetOriginal=slot.OriginalSummary;}
InvalidateRefitResult();
}
catch(Exception error){RefitResult=error.Message;}
}
partial void OnRefitModuleSearchChanged(string value) => FilterRefitModules();
partial void OnRefitOwnedOnlyChanged(bool value) => FilterRefitModules();
private void FilterRefitModules()
{
var old=RefitInstallModule;
RefitAvailableModules=StableSequence(RefitAvailableModules,refitDocument is null || RefitSlot is null?[]:RefitModuleBrowser.Compatible(refitDocument,RefitSlot.Record,refitChoices,outfitting,RefitModuleSearch.Trim(),RefitOwnedOnly));
RefitInstallModule=RefitAvailableModules.FirstOrDefault(c=>c.InstanceKey==old?.InstanceKey && c.Module.Item==old?.Module.Item)??RefitAvailableModules.FirstOrDefault();
RefitModuleTypes=StableSequence(RefitModuleTypes,RefitModuleBrowser.Types(RefitAvailableModules));
FilterRefitTypeModels();
}
private void FilterRefitTypeModels()
{
RefitTypeModels=StableSequence(RefitTypeModels,RefitSelectedModuleType is null?[]:RefitAvailableModules.Where(c=>RefitModuleBrowser.TypeKey(c.Definition).Equals(RefitSelectedModuleType.Key,StringComparison.OrdinalIgnoreCase)).ToArray());
if(RefitInstallModule is not null&&!RefitTypeModels.Contains(RefitInstallModule))RefitInstallModule=null;
}
internal void BeginRefitModuleSelection(RefitSlotLiveRow slot)
{
RefitSlot=slot;SaveRefitEditor(false);RefitModuleSearch="";RefitOwnedOnly=false;RefitSelectedModuleType=null;RefitInstallModule=null;
FilterRefitModules();
}
private void InvalidateRefitResult() { refitVersion++; RefitNeeds=[];RefitModuleResults=[];RefitResult="模拟配置已变化,统计整船材料以更新总表。"; }
[RelayCommand] private void InstallRefitModule()
{
try { if(refitDocument is null||RefitSlot is null||RefitInstallModule is null)throw new InvalidOperationException("请选择槽位和候选模块。");var slot=RefitSlot.Slot;refitDocument=ShipRefit.Install(refitDocument,slot,RefitInstallModule,outfitting);PublishRefitSlots(slot);InvalidateRefitResult(); }
catch(Exception e){RefitResult=e.Message;}
}
[RelayCommand] private void UninstallRefitModule()
{
try { if(refitDocument is null||RefitSlot is null)return;var slot=RefitSlot.Slot;refitDocument=ShipRefit.Install(refitDocument,slot,null,outfitting);PublishRefitSlots(slot);InvalidateRefitResult(); }
catch(Exception e){RefitResult=e.Message;}
}
private void SaveRefitEditor(bool publish=true)
{
if(refitDocument is null||RefitSlot is null)return;
var slot=RefitSlot.Slot;
if(RefitSlot.Module is null && !RefitEditEnabled)return;
RefitTarget? target=null;
if(RefitEditEnabled)
{
if(RefitBlueprint is null)throw new InvalidOperationException("请为此模块选择蓝图。");
if(!double.IsFinite(RefitGrade)||RefitGrade!=Math.Truncate(RefitGrade)||RefitGrade is <1 or >5||!double.IsFinite(RefitQuality)||RefitQuality is <=0 or >100)throw new InvalidOperationException("等级为 G1–G5 整数,完成度为 1%–100%。");
target=new(RefitBlueprint,(int)RefitGrade,RefitQuality/100,RefitEffect?.Key??"",RefitEngineer?.Id??0,RefitRemote,RefitFast);
}
refitDocument=ShipRefit.SetTarget(refitDocument,slot,target);
if(publish)PublishRefitSlots(slot);else RefitSlots=refitDocument.Slots;
}
[RelayCommand] private async Task CalculateRefitAsync(CancellationToken cancellationToken=default)
{
using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token,cancellationToken);var token=stop.Token;
RefitBusy=true;
try
{
SaveRefitEditor();var version=++refitVersion;var doc=refitDocument??throw new InvalidOperationException("请先建立模拟飞船。");var p=CurrentPlayer;
if(p.MismatchedEngineeringRolls>0)throw new InvalidOperationException("实际改装进度与规则不符,请先校准。");
var result=await Task.Run(()=>ShipRefitPlanner.Calculate(doc,engineeringCatalog,p.Engineers,p.Materials,token),token);
token.ThrowIfCancellationRequested();
if(version!=refitVersion)return;
RefitNeeds=result.Materials;RefitModuleResults=result.Modules;calculatedRefitVersion=version;
RefitResult=$"{result.Modules.Count} 个模块改装目标 · {result.Modules.Sum(m=>m.Rolls)} 次蓝图改装 · {result.Materials.Count} 种材料\n库存缺口 {result.Materials.Sum(m=>m.Missing):N0} · {(result.Exact?"已知声望":"声望区间预算")} · {(result.Complete?"目标路径可完成":"部分目标需要额外提升权限")}";
RefitNotes=string.Join("\n",result.Notes)+(p.MaterialsReliable?"":"\n库存存在缺口,数量需校准。");
}
catch(OperationCanceledException){RefitResult="材料计算已取消。";}catch(Exception e){RefitResult=e.Message;}
finally{RefitBusy=false;}
}
[RelayCommand] private async Task ExportRefitAsync(CancellationToken cancellationToken=default)
{
using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token,cancellationToken);var token=stop.Token;
try
{
await CalculateRefitAsync(token);
token.ThrowIfCancellationRequested();
if(refitDocument is null)throw new InvalidOperationException("请先建立模拟飞船。");
if(calculatedRefitVersion!=refitVersion)throw new InvalidOperationException("材料计算未完成,未保存过时的材料表。"+RefitResult);
var folder=Path.Combine(runtime.DataDirectory,"refits");Directory.CreateDirectory(folder);var path=Path.Combine(folder,$"refit-{refitDocument.ShipId}-{DateTime.Now:yyyyMMdd-HHmmss}.json");
await File.WriteAllTextAsync(path,JsonSerializer.Serialize(new{createdAt=DateTimeOffset.UtcNow,document=refitDocument,materialsAt=CurrentPlayer.MaterialsAt,engineersAt=CurrentPlayer.EngineersAt,summary=RefitResult,notes=RefitNotes,materials=RefitNeeds,modules=RefitModuleResults},ToolboxSettings.JsonOptions),token);
RefitResult="模拟配置已保存:"+path;
}
catch(OperationCanceledException){RefitResult="保存已取消。";}
catch(Exception e){RefitResult=e.Message;}
}
private bool CanTransferEdsy()=>!RefitEdsyBusy;
partial void OnRefitEdsyBusyChanged(bool value){ImportEdsyCommand.NotifyCanExecuteChanged();ExportEdsyCommand.NotifyCanExecuteChanged();}
[RelayCommand(CanExecute=nameof(CanTransferEdsy))] private async Task ImportEdsyAsync(CancellationToken cancellationToken=default)
{
using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token,cancellationToken);var token=stop.Token;
RefitEdsyBusy=true;
try
{
var imported=await EdsyLinkCodec.ImportAsync(RefitEdsyLink,token);
token.ThrowIfCancellationRequested();
if(refitDocument?.ShipType.Equals(imported.ShipType,StringComparison.OrdinalIgnoreCase)==true)
imported=imported with{Slots=imported.Slots.Concat(refitDocument.Slots.Where(s=>s.Slot=="PlanetaryApproachSuite").Select(s=>s with{Origin="本机保留 · EDSY 不记录行星接近套件"})).ToArray()};
loadingRefit=true;try{RefitSourceShip=null;}finally{loadingRefit=false;}
refitDocument=imported;PublishRefitSlots(null);InvalidateRefitResult();
RefitEdsyStatus=$"已导入 {ChineseNames.Ship(imported.ShipType)} · {imported.Slots.Count(s=>s.Module is not null)} 个已安装模块。"+(imported.Slots.Any(s=>s.Slot=="PlanetaryApproachSuite")?"行星接近套件保留自本机配置。":"");
RefitStatus=$"{ChineseNames.Ship(imported.ShipType)} · {imported.Name} · EDSY 导入 · 本机模拟副本";
}
catch(OperationCanceledException){RefitEdsyStatus="导入已取消。";}
catch(Exception e){RefitEdsyStatus=e.Message;}
finally{RefitEdsyBusy=false;}
}
[RelayCommand(CanExecute=nameof(CanTransferEdsy))] private async Task ExportEdsyAsync(CancellationToken cancellationToken=default)
{
using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token,cancellationToken);var token=stop.Token;
RefitEdsyBusy=true;
try
{
SaveRefitEditor();var document=refitDocument??throw new InvalidOperationException("请先选择飞船或导入装配。");
var link=await Task.Run(()=>EdsyLinkCodec.Export(document),token);
token.ThrowIfCancellationRequested();RefitEdsyLink=link;
RefitEdsyStatus="EDSY 链接已生成,可复制或在 EDSY 打开。"+(document.Slots.Any(s=>s.Slot=="PlanetaryApproachSuite")?"EDSY 链接不记录行星接近套件与外观。":"");
}
catch(OperationCanceledException){RefitEdsyStatus="导出已取消。";}
catch(Exception e){RefitEdsyStatus=e.Message;}
finally{RefitEdsyBusy=false;}
}
}
using System.Text.Json;
using XFEEDToolBox.Core.Presentation;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using XFEEDToolBox.Core.Engineering;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Outfitting;
using XFEEDToolBox.Core.Localization;
namespace XFEEDToolBox.ViewModels;
public sealed partial class RefitCategoryRow(string key,string name,string originalName) : ObservableObject
{
public string Key {get;}=key;
public string Name {get;}=name;
public string OriginalName {get;}=originalName;
public string Icon => XFEEDToolBox.Core.Rendering.GameUiIcons.Asset(Key switch { "hardpoint" => XFEEDToolBox.Core.Rendering.GameUiIcon.OutfittingHardpoint, "utility" => XFEEDToolBox.Core.Rendering.GameUiIcon.OutfittingUtility, "standard" => XFEEDToolBox.Core.Rendering.GameUiIcon.OutfittingCore, _ => XFEEDToolBox.Core.Rendering.GameUiIcon.OutfittingOptional });
[ObservableProperty] public partial string Count {get;set;}="0 已安装 / 0 槽位";
}
public partial class ToolboxViewModel
{
private readonly OutfittingCatalog outfitting = OutfittingCatalog.Default;
private ShipRefitDocument? refitDocument;
private IReadOnlyList<RefitModuleChoice> refitChoices = [];
private bool loadingRefit;
private bool publishingRefit;
private int refitVersion;
private int calculatedRefitVersion = -1;
private string refitCommander = "";
private string refitPlayerFingerprint = "";
public ShipRefitDocument? RefitDocument => refitDocument;
internal void LoadRefitProbeDocument(ShipRefitDocument document)
{
if(!Environment.GetCommandLineArgs().Any(a=>a is "--smoke-refinement" or "--smoke-interface"))throw new InvalidOperationException("仅限独立界面验收。");
refitDocument=document;PublishRefitSlots(null);LoadRefitEditor();
}
internal void RefreshRefitProbePlayer(PlayerState player)
{
if(!Environment.GetCommandLineArgs().Any(a=>a is "--smoke-refinement" or "--smoke-interface"))throw new InvalidOperationException("仅限独立界面验收。");
RefreshRefitPlayer(player);
}
[ObservableProperty] public partial string RefitShipType { get; set; }="";
[ObservableProperty] public partial ShipLiveRow? RefitSourceShip { get; set; }
[ObservableProperty] public partial IReadOnlyList<RefitSlot> RefitSlots { get; set; } = [];
public LiveCollection<RefitSlotLiveRow,RefitSlot,string> RefitVisibleSlots { get; } = new(s=>s.Slot,s=>new(s));
[ObservableProperty] public partial int RefitCategoryIndex { get; set; }
public IReadOnlyList<RefitCategoryRow> RefitCategories { get; } = [new("hardpoint","武器挂点","Hardpoints"),new("utility","多功能槽位","Utility mounts"),new("standard","核心内部模块","Core internal"),new("internal","可选内部模块","Optional internal")];
[ObservableProperty] public partial string RefitCategoryName { get; set; } = "武器挂点";
[ObservableProperty] public partial string RefitCategoryOriginal { get; set; } = "Hardpoints";
[ObservableProperty] public partial string RefitEdsyLink { get; set; } = "";
[ObservableProperty] public partial string RefitEdsyStatus { get; set; } = "导入 EDSY 装配链接,或导出当前模拟配置。";
[ObservableProperty] public partial bool RefitEdsyBusy { get; set; }
[ObservableProperty] public partial string RefitSlotSearch { get; set; } = "";
[ObservableProperty] public partial string RefitSlotCount { get; set; } = "暂无模拟配置";
[ObservableProperty] public partial RefitSlotLiveRow? RefitSlot { get; set; }
[ObservableProperty] public partial string RefitStatus { get; set; } = "选择已有装配的飞船,建立本机模拟副本。";
[ObservableProperty] public partial string RefitModuleSearch { get; set; } = "";
[ObservableProperty] public partial bool RefitOwnedOnly { get; set; }
[ObservableProperty] public partial IReadOnlyList<RefitModuleChoice> RefitAvailableModules { get; set; } = [];
[ObservableProperty] public partial IReadOnlyList<RefitModuleType> RefitModuleTypes { get; set; } = [];
[ObservableProperty] public partial IReadOnlyList<RefitModuleChoice> RefitTypeModels { get; set; } = [];
[ObservableProperty,NotifyPropertyChangedFor(nameof(RefitChoosingType)),NotifyPropertyChangedFor(nameof(RefitChoosingModel))]
public partial RefitModuleType? RefitSelectedModuleType { get; set; }
public bool RefitChoosingType=>RefitSelectedModuleType is null;
public bool RefitChoosingModel=>!RefitChoosingType;
partial void OnRefitSelectedModuleTypeChanged(RefitModuleType? value)=>FilterRefitTypeModels();
[ObservableProperty] public partial RefitModuleChoice? RefitInstallModule { get; set; }
[ObservableProperty] public partial string RefitSlotCaption { get; set; } = "选择一个模拟槽位。";
[ObservableProperty] public partial string RefitSlotOriginal { get; set; }="";
[ObservableProperty] public partial string RefitSelectedName { get; set; } = "选择一个模块";
[ObservableProperty] public partial string RefitSelectedOriginal { get; set; } = "";
[ObservableProperty] public partial string RefitEngineeringName { get; set; } = "未安装模块";
[ObservableProperty] public partial string RefitEngineeringOriginal { get; set; } = "";
[ObservableProperty] public partial string RefitTargetSummary { get; set; } = "保持当前装配";
[ObservableProperty] public partial string RefitTargetOriginal { get; set; } = "";
public LiveCollection<RefitPropertyLiveRow,RefitPropertyRow,string> RefitProperties { get; } = new(s=>s.OriginalName,s=>new(s));
public LiveCollection<RefitModifierLiveRow,EngineeringModifier,string> RefitModifiers { get; } = new(s=>s.Label,s=>new(s));
[ObservableProperty] public partial bool RefitHasModifiers { get; set; }
[ObservableProperty] public partial bool RefitCanEngineer { get; set; }
[ObservableProperty] public partial string RefitEngineeringHint { get; set; } = "先选择已安装的模块。";
[ObservableProperty] public partial bool RefitEditEnabled { get; set; }
[ObservableProperty] public partial IReadOnlyList<string> RefitBlueprints { get; set; } = [];
[ObservableProperty] public partial string? RefitBlueprint { get; set; }
[ObservableProperty] public partial double RefitGrade { get; set; } = 5;
[ObservableProperty] public partial double RefitQuality { get; set; } = 100;
[ObservableProperty] public partial IReadOnlyList<EngineerLiveRow> RefitEngineers { get; set; } = [];
[ObservableProperty] public partial EngineerLiveRow? RefitEngineer { get; set; }
[ObservableProperty] public partial IReadOnlyList<EngineeringEffectChoice> RefitEffects { get; set; } = [];
[ObservableProperty] public partial EngineeringEffectChoice? RefitEffect { get; set; }
[ObservableProperty] public partial bool RefitRemote { get; set; }
[ObservableProperty] public partial bool RefitFast { get; set; } = true;
[ObservableProperty] public partial bool RefitBusy { get; set; }
public bool RefitCanAddToList=>!RefitBusy&&!UpgradeBusy;
partial void OnRefitBusyChanged(bool value)=>OnPropertyChanged(nameof(RefitCanAddToList));
[ObservableProperty] public partial double RefitCurrentProgress {get;set;}
[ObservableProperty] public partial string RefitCurrentProgressText {get;set;}="—";
[ObservableProperty] public partial string RefitResult { get; set; } = "模拟安装或修改改装目标后,统计整船材料。";
[ObservableProperty] public partial string RefitNotes { get; set; } = "按游戏槽位顺序核对材料与共享工程师声望。";
[ObservableProperty] public partial IReadOnlyList<MaterialNeed> RefitNeeds { get; set; } = [];
[ObservableProperty] public partial IReadOnlyList<RefitModuleResult> RefitModuleResults { get; set; } = [];
private void RefreshRefitPlayer(PlayerState p)
{
if (refitCommander.Length > 0 && p.CommanderId != refitCommander) { refitDocument=null; PublishRefitSlots(null); RefitNeeds=[]; RefitModuleResults=[]; }
refitCommander=p.CommanderId;
var fingerprint=$"{p.MaterialsAt}:{p.MaterialsReliable}:{p.EngineersAt}:{p.MismatchedEngineeringRolls}";
if(fingerprint!=refitPlayerFingerprint)
{
refitPlayerFingerprint=fingerprint;
if(RefitModuleResults.Count>0)
{
InvalidateRefitResult();
RefitResult="仓库或工程师权限已更新,重新统计整船材料以核对当前缺口。";
}
}
loadingRefit=true;
try { RefitSourceShip=refitDocument is {EdsyHash.Length:>0}?null:Ships.FirstOrDefault(s=>s.ShipId==(refitDocument?.ShipId??p.CurrentShipId))??Ships.FirstOrDefault(); }
finally { loadingRefit=false; }
refitChoices=ShipRefit.Choices(p,outfitting); FilterRefitModules();
if(refitDocument is null && RefitSourceShip is { ConfigurationAt:not null,ConfigurationChanged:false }) RebuildRefit();
if(refitDocument is not null)
{
var updated=ShipRefit.RefreshOwnedProgress(refitDocument,p);
if(!ReferenceEquals(updated,refitDocument))
{
var selected=RefitSlot?.Slot;
SaveRefitEditor(false);
refitDocument=ShipRefit.RefreshOwnedProgress(refitDocument!,p);
PublishRefitSlots(selected,reloadEditor:false);InvalidateRefitResult();
}
}
if(refitDocument is { EdsyHash.Length: >0 } imported) RefitStatus=$"{ChineseNames.Ship(imported.ShipType)} · {imported.Name} · EDSY 导入 · {imported.Slots.Count(s=>s.Module is not null)} 个已安装模块 · 本机模拟副本";
else if(refitDocument is not null && RefitSourceShip is { } source)
{
var changed=source.Modules.Any(m=>refitDocument.Slots.FirstOrDefault(s=>s.Slot==m.Slot)?.Original is not { } old || old.Item!=m.Item || old.Engineering?.Blueprint!=m.Engineering?.Blueprint || old.Engineering?.Level!=m.Engineering?.Level || old.Engineering?.Quality!=m.Engineering?.Quality || old.Engineering?.ExperimentalEffectId!=m.Engineering?.ExperimentalEffectId);
RefitStatus=$"{source.DisplayName} · 基线 {refitDocument.SourceAt?.ToLocalTime():MM-dd HH:mm:ss} · {refitDocument.Slots.Count(s=>s.Module is not null)} 个模拟模块"+(changed?"\n游戏装配已有新变化,当前模拟仍保留原基线;可用最新装配重建。":"\n本机模拟副本,材料按当前仓库数量核对。");
}
RefreshRefitOverview(p);
}
partial void OnRefitSourceShipChanged(ShipLiveRow? value) { if(!loadingRefit && value is not null && value.ShipId!=refitDocument?.ShipId) RebuildRefit(); }
[RelayCommand] private void RebuildRefit()
{
try
{
var source=RefitSourceShip??Ships.FirstOrDefault(s=>s.ShipId==CurrentPlayer.CurrentShipId)??Ships.FirstOrDefault()??throw new InvalidOperationException("请选择飞船。");
loadingRefit=true;try{RefitSourceShip=source;}finally{loadingRefit=false;}
refitDocument=ShipRefit.Create(source.Record,outfitting); PublishRefitSlots(null); InvalidateRefitResult();
RefitStatus=source.DisplayName+" · 已建立模拟副本";
}
catch(Exception e) { refitDocument=null;PublishRefitSlots(null);RefitStatus=e.Message; }
}
private void PublishRefitSlots(string? selected,bool reloadEditor=true)
{
publishingRefit=true;
try {
RefitShipType=refitDocument?.ShipType??"";
RefitSlots=refitDocument?.Slots??[];
foreach(var row in RefitCategories)row.Count=$"{RefitSlots.Count(s=>s.Category==row.Key&&s.Module is not null)} 已安装 / {RefitSlots.Count(s=>s.Category==row.Key)} 槽位";
FilterRefitSlots(selected);
} finally { publishingRefit=false; }
if(reloadEditor)LoadRefitEditor();else UpdateRefitCurrentValues();
}
partial void OnRefitCategoryIndexChanged(int value) => FilterRefitSlots(RefitSlot?.Slot);
partial void OnRefitSlotSearchChanged(string value) => FilterRefitSlots(RefitSlot?.Slot);
private void FilterRefitSlots(string? selected)
{
var index=Math.Clamp(RefitCategoryIndex,0,3);
var category=new[]{"hardpoint","utility","standard","internal"}[index];
RefitCategoryName=new[]{"武器挂点","多功能槽位","核心内部模块","可选内部模块"}[index];
RefitCategoryOriginal=new[]{"Hardpoints","Utility mounts","Core internal","Optional internal"}[index];
var search=RefitSlotSearch.Trim();
RefitVisibleSlots.Apply(RefitSlots.Where(s=>(category.Length==0||s.Category==category)&&
(search.Length==0||s.Caption.Contains(search,StringComparison.OrdinalIgnoreCase)||s.OriginalName.Contains(search,StringComparison.OrdinalIgnoreCase))));
RefitSlotCount=$"显示 {RefitVisibleSlots.Count} / {RefitSlots.Count} 个槽位";
RefitSlot=RefitVisibleSlots.FirstOrDefault(s=>s.Slot==selected)??RefitVisibleSlots.FirstOrDefault();
}
partial void OnRefitSlotChanging(RefitSlotLiveRow? oldValue,RefitSlotLiveRow? newValue)
{
if(loadingRefit||publishingRefit||oldValue is null||refitDocument is null)return;
try {SaveRefitEditor(false);}catch(Exception e){RefitResult=e.Message;}
}
partial void OnRefitSlotChanged(RefitSlotLiveRow? value){if(!publishingRefit)LoadRefitEditor();}
private void LoadRefitEditor()
{
loadingRefit=true;
try
{
var slot=refitDocument?.Slots.FirstOrDefault(s=>s.Slot==RefitSlot?.Slot)??RefitSlot?.Record; var type=EngineeringCatalog.ModuleType(slot?.Module?.Item??"");
RefitSlotCaption=slot is null?"选择一个模拟槽位。":ChineseNames.Slot(slot.Slot);
RefitSlotOriginal=slot is null?"":ChineseNames.SlotEnglish(slot.Slot);
RefitSelectedName=slot?.Module?.DisplayName??(slot is null?"选择一个模块":"空槽");
RefitSelectedOriginal=slot?.Module?.OriginalName??(slot is null?"":"Empty slot");
RefitEngineeringName=slot?.Module?.EngineeringText??"未安装模块";
RefitEngineeringOriginal=slot?.Module?.EngineeringOriginal??"Empty slot";
RefitTargetSummary=slot?.Summary??"选择左侧槽位查看属性";
RefitTargetOriginal=slot?.OriginalSummary??"";
LoadRefitProperties(slot);
RefitCurrentProgress=(slot?.Module?.Engineering?.Quality??0)*100;
RefitCurrentProgressText=slot?.Module is null?"—":slot.Module.Engineering is null?"0%":slot.Module.Engineering.Quality is {} q?q.ToString("P0"):"—";
RefitBlueprints=StableSequence(RefitBlueprints,engineeringCatalog.Recipes.Where(r=>r.Type==type && r.Grade>0).Select(r=>r.Name).Distinct().Order());
RefitCanEngineer=slot?.Module is not null&&RefitBlueprints.Count>0;
RefitEngineeringHint=slot?.Module is null?"点击槽位卡片右下角的更改按钮选择模块。":RefitCanEngineer?"目标保存在模拟配置中,按游戏槽位顺序计算材料及共享工程师声望。":"该模块没有可用的工程蓝图。";
var current=engineeringCatalog.Recipes.FirstOrDefault(r=>r.Type==type && EngineeringCatalog.SameBlueprint(r,slot?.Module?.Engineering));
RefitBlueprint=slot?.Target?.Blueprint??current?.Name??RefitBlueprints.FirstOrDefault();
RefitEditEnabled=slot?.Target is not null;
RefitGrade=slot?.Target?.Grade??(slot?.Module?.Engineering?.Level is >0 and var level?level:0);
RefitQuality=slot?.Target is { } t?t.Quality*100:100;
RefreshRefitEngineeringChoices(slot?.Target?.EngineerId,slot?.Target?.Experimental);
RefitRemote=slot?.Target?.Remote??false;RefitFast=slot?.Target?.FastAdvance??true;
}
finally{loadingRefit=false;}
FilterRefitModules();
}
private void UpdateRefitCurrentValues()
{
// A Journal update changes the current values without resetting the user's target controls.
var slot=refitDocument?.Slots.FirstOrDefault(s=>s.Slot==RefitSlot?.Slot);
RefitSelectedName=slot?.Module?.DisplayName??"空槽";RefitSelectedOriginal=slot?.Module?.OriginalName??"Empty slot";
RefitEngineeringName=slot?.Module?.EngineeringText??"未安装模块";RefitEngineeringOriginal=slot?.Module?.EngineeringOriginal??"Empty slot";
RefitCurrentProgress=(slot?.Module?.Engineering?.Quality??0)*100;
RefitCurrentProgressText=slot?.Module is null?"—":slot.Module.Engineering is null?"0%":slot.Module.Engineering.Quality is {} q?q.ToString("P0"):"—";
LoadRefitProperties(slot);
}
private void LoadRefitProperties(RefitSlot? slot)
{
var rows=new List<RefitPropertyRow>();
if(slot is not null)
{
rows.Add(new("模块类别","Module category",ChineseNames.Category(slot.Category),slot.Category switch{"hardpoint"=>"Hardpoint","utility"=>"Utility mount","standard"=>"Core internal","internal"=>"Optional internal",_=>ChineseNames.English(slot.Category)}));
rows.Add(new("槽位容量","Slot size",slot.Capacity.ToString()));
if(slot.MiningOnly)rows.Add(new("槽位限制","Slot restriction","仅限采矿模块"));
if(slot.Module is { } module)
{
if(outfitting.Find(module.Item) is { } definition)
{
rows.Add(new("模块规格","Class / rating",$"{definition.Size}{definition.Rating}"));
if(definition.Mount.Length>0)rows.Add(new("武器挂载","Mount",ChineseNames.Translate(definition.Mount),definition.Mount));
}
rows.Add(new("运行状态","Power state",module.State,$"{(module.Enabled?"Enabled":"Disabled")} · Priority {module.Priority+1}"));
rows.Add(new("模块完整度","Integrity",module.HealthText));
rows.Add(new("模块来源","Source",slot.Origin));
rows.Add(new("记录价值","Recorded value",module.Value is {} value?$"{value:N0} Cr":"未记录"));
if(module.AmmoInClip is not null||module.AmmoInHopper is not null)rows.Add(new("弹匣 / 储备","Clip / reserve",module.AmmoText));
}
}
_=PublishRefitPropertiesAsync(slot,rows,++refitPropertiesRequest);
RefitModifiers.Apply(slot?.Module?.Engineering?.Modifiers??[]);
RefitHasModifiers=RefitModifiers.Count>0;
}
private int refitPropertiesRequest;
private ShipRefitDocument? refitStatisticsDocument;
private Task<EquipmentStatistics?>? refitStatisticsTask;
private async Task PublishRefitPropertiesAsync(RefitSlot? slot,List<RefitPropertyRow> metadata,int request)
{
var document=refitDocument;
EquipmentStatistics? stats=null;
if(document is not null)
{
if(!ReferenceEquals(document,refitStatisticsDocument)||refitStatisticsTask is null)
{
refitStatisticsDocument=document;
refitStatisticsTask=Task.Run<EquipmentStatistics?>(()=>
{
try{return EdsyLinkCodec.Statistics(document);}
catch(Exception){return null;}
});
}
stats=await refitStatisticsTask;
}
if(request!=refitPropertiesRequest)return;
var values=slot is not null&&stats?.Modules.TryGetValue(slot.Slot,out var found)==true?found:null;
RefitProperties.Apply(values is null?metadata:EquipmentStatusRows.Module(values).Concat(metadata));
PublishRefitOverview(document,stats);
}
private void RefreshRefitEngineeringChoices(long? engineerId,string? effect)
{
var type=EngineeringCatalog.ModuleType(RefitSlot?.Module?.Item??"");
var names=engineeringCatalog.Recipes.Where(r=>r.Type==type && r.Name==RefitBlueprint && r.Grade>0).SelectMany(r=>r.Engineers).Distinct().ToArray();
RefitEngineers=StableSequence(RefitEngineers,EngineerAccessList.Where(e=>names.Any(n=>EngineeringCatalog.SameEngineer(n,e.Name))).OrderByDescending(e=>e.Unlocked).ThenByDescending(e=>e.Rank));
RefitEngineer=RefitEngineers.FirstOrDefault(e=>e.Id==engineerId)??RefitEngineers.FirstOrDefault();
var actual=effect??EngineeringExperimental.Current(RefitSlot?.Module?.Engineering);
RefitEffects=BuildEngineeringEffects(type,actual);
RefitEffect=RefitEffects.FirstOrDefault(e=>EngineeringExperimental.Same(engineeringCatalog,type,e.Key,actual))??RefitEffects[0];
}
partial void OnRefitBlueprintChanged(string? value)
{
if(loadingRefit)return;
loadingRefit=true;try{RefreshRefitEngineeringChoices(RefitEngineer?.Id,RefitEffect?.Key);}finally{loadingRefit=false;}
RefitTargetEdited(true);
}
partial void OnRefitGradeChanged(double value)=>RefitTargetEdited(true);
partial void OnRefitQualityChanged(double value)=>RefitTargetEdited(true);
partial void OnRefitEffectChanged(EngineeringEffectChoice? value)=>RefitTargetEdited(true);
partial void OnRefitEngineerChanged(EngineerLiveRow? value)=>RefitTargetEdited();
partial void OnRefitEditEnabledChanged(bool value)=>RefitTargetEdited();
partial void OnRefitRemoteChanged(bool value)=>RefitTargetEdited();
partial void OnRefitFastChanged(bool value)=>RefitTargetEdited();
private void RefitTargetEdited(bool enable=false)
{
if(loadingRefit||publishingRefit)return;
loadingRefit=true;
try{if(enable)RefitEditEnabled=true;if(RefitEditEnabled&&RefitGrade<1)RefitGrade=1;}
finally{loadingRefit=false;}
try
{
SaveRefitEditor(false);
if(refitDocument?.Slots.FirstOrDefault(s=>s.Slot==RefitSlot?.Slot) is {} slot)
{RefitSlot?.Update(slot);RefitTargetSummary=slot.Summary;RefitTargetOriginal=slot.OriginalSummary;}
InvalidateRefitResult();
}
catch(Exception error){RefitResult=error.Message;}
}
partial void OnRefitModuleSearchChanged(string value) => FilterRefitModules();
partial void OnRefitOwnedOnlyChanged(bool value) => FilterRefitModules();
private void FilterRefitModules()
{
var old=RefitInstallModule;
RefitAvailableModules=StableSequence(RefitAvailableModules,refitDocument is null || RefitSlot is null?[]:RefitModuleBrowser.Compatible(refitDocument,RefitSlot.Record,refitChoices,outfitting,RefitModuleSearch.Trim(),RefitOwnedOnly));
RefitInstallModule=RefitAvailableModules.FirstOrDefault(c=>c.InstanceKey==old?.InstanceKey && c.Module.Item==old?.Module.Item)??RefitAvailableModules.FirstOrDefault();
RefitModuleTypes=StableSequence(RefitModuleTypes,RefitModuleBrowser.Types(RefitAvailableModules));
FilterRefitTypeModels();
}
private void FilterRefitTypeModels()
{
RefitTypeModels=StableSequence(RefitTypeModels,RefitSelectedModuleType is null?[]:RefitAvailableModules.Where(c=>RefitModuleBrowser.TypeKey(c.Definition).Equals(RefitSelectedModuleType.Key,StringComparison.OrdinalIgnoreCase)).ToArray());
if(RefitInstallModule is not null&&!RefitTypeModels.Contains(RefitInstallModule))RefitInstallModule=null;
}
internal void BeginRefitModuleSelection(RefitSlotLiveRow slot)
{
RefitSlot=slot;SaveRefitEditor(false);RefitModuleSearch="";RefitOwnedOnly=false;RefitSelectedModuleType=null;RefitInstallModule=null;
FilterRefitModules();
}
private void InvalidateRefitResult() { refitVersion++; RefitNeeds=[];RefitModuleResults=[];RefitResult="模拟配置已变化,统计整船材料以更新总表。"; }
[RelayCommand] private void InstallRefitModule()
{
try { if(refitDocument is null||RefitSlot is null||RefitInstallModule is null)throw new InvalidOperationException("请选择槽位和候选模块。");var slot=RefitSlot.Slot;refitDocument=ShipRefit.Install(refitDocument,slot,RefitInstallModule,outfitting);PublishRefitSlots(slot);InvalidateRefitResult(); }
catch(Exception e){RefitResult=e.Message;}
}
[RelayCommand] private void UninstallRefitModule()
{
try { if(refitDocument is null||RefitSlot is null)return;var slot=RefitSlot.Slot;refitDocument=ShipRefit.Install(refitDocument,slot,null,outfitting);PublishRefitSlots(slot);InvalidateRefitResult(); }
catch(Exception e){RefitResult=e.Message;}
}
private void SaveRefitEditor(bool publish=true)
{
if(refitDocument is null||RefitSlot is null)return;
var slot=RefitSlot.Slot;
if(RefitSlot.Module is null && !RefitEditEnabled)return;
RefitTarget? target=null;
if(RefitEditEnabled)
{
if(RefitBlueprint is null)throw new InvalidOperationException("请为此模块选择蓝图。");
if(!double.IsFinite(RefitGrade)||RefitGrade!=Math.Truncate(RefitGrade)||RefitGrade is <1 or >5||!double.IsFinite(RefitQuality)||RefitQuality is <=0 or >100)throw new InvalidOperationException("等级为 G1–G5 整数,完成度为 1%–100%。");
target=new(RefitBlueprint,(int)RefitGrade,RefitQuality/100,RefitEffect?.Key??"",RefitEngineer?.Id??0,RefitRemote,RefitFast);
}
refitDocument=ShipRefit.SetTarget(refitDocument,slot,target);
if(publish)PublishRefitSlots(slot);else RefitSlots=refitDocument.Slots;
}
[RelayCommand] private async Task CalculateRefitAsync(CancellationToken cancellationToken=default)
{
using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token,cancellationToken);var token=stop.Token;
RefitBusy=true;
try
{
SaveRefitEditor();var version=++refitVersion;var doc=refitDocument??throw new InvalidOperationException("请先建立模拟飞船。");var p=CurrentPlayer;
if(p.MismatchedEngineeringRolls>0)throw new InvalidOperationException("实际改装进度与规则不符,请先校准。");
var result=await Task.Run(()=>ShipRefitPlanner.Calculate(doc,engineeringCatalog,p.Engineers,p.Materials,token),token);
token.ThrowIfCancellationRequested();
if(version!=refitVersion)return;
RefitNeeds=result.Materials;RefitModuleResults=result.Modules;calculatedRefitVersion=version;
RefitResult=$"{result.Modules.Count} 个模块改装目标 · {result.Modules.Sum(m=>m.Rolls)} 次蓝图改装 · {result.Materials.Count} 种材料\n库存缺口 {result.Materials.Sum(m=>m.Missing):N0} · {(result.Exact?"已知声望":"声望区间预算")} · {(result.Complete?"目标路径可完成":"部分目标需要额外提升权限")}";
RefitNotes=string.Join("\n",result.Notes)+(p.MaterialsReliable?"":"\n库存存在缺口,数量需校准。");
}
catch(OperationCanceledException){RefitResult="材料计算已取消。";}catch(Exception e){RefitResult=e.Message;}
finally{RefitBusy=false;}
}
[RelayCommand] private async Task ExportRefitAsync(CancellationToken cancellationToken=default)
{
using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token,cancellationToken);var token=stop.Token;
try
{
await CalculateRefitAsync(token);
token.ThrowIfCancellationRequested();
if(refitDocument is null)throw new InvalidOperationException("请先建立模拟飞船。");
if(calculatedRefitVersion!=refitVersion)throw new InvalidOperationException("材料计算未完成,未保存过时的材料表。"+RefitResult);
var folder=Path.Combine(runtime.DataDirectory,"refits");Directory.CreateDirectory(folder);var path=Path.Combine(folder,$"refit-{refitDocument.ShipId}-{DateTime.Now:yyyyMMdd-HHmmss}.json");
await File.WriteAllTextAsync(path,JsonSerializer.Serialize(new{createdAt=DateTimeOffset.UtcNow,document=refitDocument,materialsAt=CurrentPlayer.MaterialsAt,engineersAt=CurrentPlayer.EngineersAt,summary=RefitResult,notes=RefitNotes,materials=RefitNeeds,modules=RefitModuleResults},ToolboxSettings.JsonOptions),token);
RefitResult="模拟配置已保存:"+path;
}
catch(OperationCanceledException){RefitResult="保存已取消。";}
catch(Exception e){RefitResult=e.Message;}
}
private bool CanTransferEdsy()=>!RefitEdsyBusy;
partial void OnRefitEdsyBusyChanged(bool value){ImportEdsyCommand.NotifyCanExecuteChanged();ExportEdsyCommand.NotifyCanExecuteChanged();}
[RelayCommand(CanExecute=nameof(CanTransferEdsy))] private async Task ImportEdsyAsync(CancellationToken cancellationToken=default)
{
using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token,cancellationToken);var token=stop.Token;
RefitEdsyBusy=true;
try
{
var imported=await EdsyLinkCodec.ImportAsync(RefitEdsyLink,token);
token.ThrowIfCancellationRequested();
if(refitDocument?.ShipType.Equals(imported.ShipType,StringComparison.OrdinalIgnoreCase)==true)
imported=imported with{Slots=imported.Slots.Concat(refitDocument.Slots.Where(s=>s.Slot=="PlanetaryApproachSuite").Select(s=>s with{Origin="本机保留 · EDSY 不记录行星接近套件"})).ToArray()};
loadingRefit=true;try{RefitSourceShip=null;}finally{loadingRefit=false;}
refitDocument=imported;PublishRefitSlots(null);InvalidateRefitResult();
RefitEdsyStatus=$"已导入 {ChineseNames.Ship(imported.ShipType)} · {imported.Slots.Count(s=>s.Module is not null)} 个已安装模块。"+(imported.Slots.Any(s=>s.Slot=="PlanetaryApproachSuite")?"行星接近套件保留自本机配置。":"");
RefitStatus=$"{ChineseNames.Ship(imported.ShipType)} · {imported.Name} · EDSY 导入 · 本机模拟副本";
}
catch(OperationCanceledException){RefitEdsyStatus="导入已取消。";}
catch(Exception e){RefitEdsyStatus=e.Message;}
finally{RefitEdsyBusy=false;}
}
[RelayCommand(CanExecute=nameof(CanTransferEdsy))] private async Task ExportEdsyAsync(CancellationToken cancellationToken=default)
{
using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token,cancellationToken);var token=stop.Token;
RefitEdsyBusy=true;
try
{
SaveRefitEditor();var document=refitDocument??throw new InvalidOperationException("请先选择飞船或导入装配。");
var link=await Task.Run(()=>EdsyLinkCodec.Export(document),token);
token.ThrowIfCancellationRequested();RefitEdsyLink=link;
RefitEdsyStatus="EDSY 链接已生成,可复制或在 EDSY 打开。"+(document.Slots.Any(s=>s.Slot=="PlanetaryApproachSuite")?"EDSY 链接不记录行星接近套件与外观。":"");
}
catch(OperationCanceledException){RefitEdsyStatus="导出已取消。";}
catch(Exception e){RefitEdsyStatus=e.Message;}
finally{RefitEdsyBusy=false;}
}
}