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 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 RefitSlots { get; set; } = []; public LiveCollection RefitVisibleSlots { get; } = new(s=>s.Slot,s=>new(s)); [ObservableProperty] public partial int RefitCategoryIndex { get; set; } public IReadOnlyList 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 RefitAvailableModules { get; set; } = []; [ObservableProperty] public partial IReadOnlyList RefitModuleTypes { get; set; } = []; [ObservableProperty] public partial IReadOnlyList 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 RefitProperties { get; } = new(s=>s.OriginalName,s=>new(s)); public LiveCollection 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 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 RefitEngineers { get; set; } = []; [ObservableProperty] public partial EngineerLiveRow? RefitEngineer { get; set; } [ObservableProperty] public partial IReadOnlyList 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 RefitNeeds { get; set; } = []; [ObservableProperty] public partial IReadOnlyList 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(); 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? refitStatisticsTask; private async Task PublishRefitPropertiesAsync(RefitSlot? slot,List 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(()=> { 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;} } }