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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
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;}
    }
}