using System.Collections.ObjectModel; using XFEEDToolBox.Core.Presentation; using System.Diagnostics; using System.Text.Json; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.UI.Dispatching; using Windows.ApplicationModel.DataTransfer; using Windows.Storage.Pickers; using XFEEDToolBox.Core.Data; using XFEEDToolBox.Core.Execution; using XFEEDToolBox.Core.Models; using XFEEDToolBox.Core.Planning; using XFEEDToolBox.Core.Services; using XFEEDToolBox.Core.Native; using XFEEDToolBox.Views; using XFEEDToolBox.Core.Session; using XFEEDToolBox.Services; namespace XFEEDToolBox.ViewModels; public partial class ToolboxViewModel : ViewModelBase, IDisposable { private readonly ToolboxRuntime runtime; private readonly DispatcherQueue dispatcher; private readonly DispatcherQueueTimer timer; private bool initialized, refreshing, revertingDemo; private int dirty, marketPage; private long? lastMarketFilterId; private long marketRequest; private readonly CancellationTokenSource lifetime = new(); private RunProgress? lastRun; private GameEnvironment? gameEnvironment; private readonly GameSessionController sessions; private WindowsGlobalHotkeys? hotkeys; private CancellationTokenSource? sessionStop; public PlayerState CurrentPlayer => DemoMode ? DemoData.Player : runtime.Player; public string AppVersionLabel=>typeof(App).Assembly.GetName().Version is {} version?$"XFE ED TOOLBOX / {version.Major}.{version.Minor}.{version.Build}":"XFE ED TOOLBOX"; [ObservableProperty] public partial bool DemoMode { get; set; } [ObservableProperty] public partial bool IsBusy { get; set; } [ObservableProperty] public partial bool RunActive { get; set; } [ObservableProperty] public partial bool SessionBusy { get; set; } [ObservableProperty, NotifyPropertyChangedFor(nameof(StartingGame),nameof(RestartingGame),nameof(ReloggingGame))] public partial SessionOperation? ActiveSessionOperation { get; set; } public bool StartingGame => ActiveSessionOperation == SessionOperation.Start; public bool RestartingGame => ActiveSessionOperation == SessionOperation.Restart; public bool ReloggingGame => ActiveSessionOperation == SessionOperation.Relog; [ObservableProperty, NotifyPropertyChangedFor(nameof(HasSessionMessage))] public partial string SessionMessage { get; set; } = ""; public bool HasSessionMessage => !string.IsNullOrWhiteSpace(SessionMessage); [ObservableProperty] public partial bool HotkeysEnabled { get; set; } [ObservableProperty] public partial string RelogHotkey { get; set; } = ""; [ObservableProperty] public partial string RestartHotkey { get; set; } = ""; [ObservableProperty] public partial string HotkeyStatus { get; set; } = "全局快捷键未启用。"; [ObservableProperty] public partial bool HasError { get; set; } [ObservableProperty] public partial string ErrorMessage { get; set; } = ""; [ObservableProperty] public partial string StatusText { get; set; } = "正在读取本机数据"; [ObservableProperty] public partial string Commander { get; set; } = "等待 Journal"; [ObservableProperty] public partial string Ship { get; set; } = "—"; [ObservableProperty] public partial string CreditsText { get; set; } = "—"; [ObservableProperty] public partial string CargoText { get; set; } = "—"; [ObservableProperty] public partial string MaterialTotalText { get; set; } = "—"; [ObservableProperty] public partial string MaterialKindsText { get; set; } = "等待材料快照"; [ObservableProperty] public partial string PlayerSnapshotText { get; set; } = "正在读取本机指挥官数据"; [ObservableProperty] public partial string BalanceNote { get; set; } = "现金由 LoadGame 与后续交易事件推导"; [ObservableProperty] public partial string SystemName { get; set; } = "等待位置数据"; [ObservableProperty] public partial string StationName { get; set; } = "—"; [ObservableProperty] public partial string DockingText { get; set; } = "未连接游戏"; [ObservableProperty] public partial string CargoNote { get; set; } = "货舱数据待更新"; [ObservableProperty] public partial string CoverageText { get; set; } = "0 个站点"; [ObservableProperty] public partial string QuoteCountText { get; set; } = "0"; [ObservableProperty] public partial string LatestText { get; set; } = "尚无行情"; [ObservableProperty] public partial string SourceNote { get; set; } = "EDDN 增量行情 + 本地 Journal"; [ObservableProperty] public partial string EddnCountText { get; set; } = "0"; [ObservableProperty] public partial string MarketSearch { get; set; } = ""; [ObservableProperty] public partial int MarketSortIndex { get; set; } [ObservableProperty] public partial bool OnlyCurrentStation { get; set; } [ObservableProperty] public partial string MarketPageText { get; set; } = "第 1 页"; public LiveCollection MarketRows { get; } = new(x => (x.Station.MarketId,x.Quote.Commodity),x => new(x)); public LiveCollection Stations { get; } = new(x => x.MarketId,x => new(x)); [ObservableProperty] public partial int InventoryTab { get; set; } [ObservableProperty] public partial string InventorySearch { get; set; } = ""; public LiveCollection InventoryRows { get; } = new(x => (x.Group,x.Key,x.MissionId),x => new(x)); public LiveCollection Ships { get; } = new(x => x.ShipId,x => new(x)); [ObservableProperty] public partial ShipLiveRow? SelectedShip { get; set; } public LiveCollection FittedModules { get; } = new(x => x.Slot,x => new(x)); public LiveCollection StoredModules { get; } = new(x => x.StorageSlot,x => new(x)); public LiveCollection ReadChecks { get; } = new(x => x.Category,x => new(x)); [ObservableProperty] public partial string FleetSummary { get; set; } = "等待船队与装配数据"; [ObservableProperty] public partial string SelectedShipSummary { get; set; } = "选择一艘飞船查看最后记录的配置。"; [ObservableProperty] public partial string CurrentShipSummary { get; set; } = "等待 Loadout"; [ObservableProperty] public partial string CockpitSummary { get; set; } = "等待 Status.json"; [ObservableProperty] public partial string ReadSummary { get; set; } = "正在核对读取覆盖范围"; [ObservableProperty] public partial string ReadReportPath { get; set; } = "验收报告保存在工具箱自己的数据目录。"; public LiveCollection Activity { get; } = new(x => x.Id,x => new(x)); [ObservableProperty] public partial StationLiveRow? SelectedStart { get; set; } [ObservableProperty] public partial double Cash { get; set; } = 1000000; [ObservableProperty] public partial double Reserve { get; set; } = 100000; [ObservableProperty] public partial double Hold { get; set; } = 64; [ObservableProperty] public partial double JumpRange { get; set; } = 20; [ObservableProperty] public partial double MaxDistance { get; set; } = 60; [ObservableProperty] public partial double Radius { get; set; } = 500; [ObservableProperty] public partial double MaxLegs { get; set; } = 4; [ObservableProperty] public partial double MaxAgeHours { get; set; } = 6; [ObservableProperty] public partial double MaxArrival { get; set; } = 10000; [ObservableProperty] public partial double JumpMinutes { get; set; } = 1; [ObservableProperty] public partial double DockMinutes { get; set; } = 4; [ObservableProperty] public partial double CostPerJump { get; set; } = 100; [ObservableProperty] public partial double CostPerMinute { get; set; } [ObservableProperty] public partial bool Loop { get; set; } = true; [ObservableProperty] public partial bool IncludePlanetary { get; set; } [ObservableProperty] public partial bool IncludeCarriers { get; set; } [ObservableProperty] public partial bool AllowUnknown { get; set; } [ObservableProperty] public partial int RiskIndex { get; set; } = 1; [ObservableProperty] public partial int ObjectiveIndex { get; set; } [ObservableProperty] public partial int PadIndex { get; set; } = 2; [ObservableProperty] public partial RoutePlan? Plan { get; set; } [ObservableProperty] public partial TradeLeg? SelectedLeg { get; set; } [ObservableProperty] public partial string PlanProfit { get; set; } = "—"; [ObservableProperty] public partial string PlanHourly { get; set; } = "—"; [ObservableProperty] public partial string PlanDuration { get; set; } = "—"; [ObservableProperty] public partial string PlanInfo { get; set; } = "选择起点和约束,计算你的下一趟航程。"; [ObservableProperty] public partial string PlanWarnings { get; set; } = "路线估计基于已知行情,出发前需确认游戏中的报价和航线。"; [ObservableProperty] public partial int ExecutionModeIndex { get; set; } [ObservableProperty] public partial string ExecutionHint { get; set; } = "模拟演练只展示步骤,不向游戏发送输入。"; [ObservableProperty] public partial string RunStatus { get; set; } = "等待路线"; [ObservableProperty] public partial string RunMessage { get; set; } = "输入投入资金并规划路线,选择方案后开始执行。"; [ObservableProperty] public partial string ActualProfit { get; set; } = "0 Cr"; [ObservableProperty] public partial string ProfitLabel { get; set; } = "已确认净利润"; [ObservableProperty] public partial string ProfitDetails { get; set; } = "按实际成交和停靠服务回执更新。"; [ObservableProperty] public partial double RunPercent { get; set; } [ObservableProperty] public partial string NextDestination { get; set; } = "—"; [ObservableProperty] public partial string JournalDirectory { get; set; } = ""; [ObservableProperty] public partial string EddnEndpoint { get; set; } = ""; [ObservableProperty] public partial bool EddnOnStartup { get; set; } [ObservableProperty] public partial string ImportSystemName { get; set; } = ""; [ObservableProperty] public partial bool ImportNearby { get; set; } = true; [ObservableProperty] public partial string ImportStatus { get; set; } = "可按星系补全,或导入 Spansh JSON / GZip 快照。"; [ObservableProperty] public partial string EnvironmentText { get; set; } = "正在自动读取本机舰船与游戏配置"; public string AcceptanceMode => "数据只读接入 · 会话操作由主页按钮或已启用的快捷键触发"; public string DataDirectory => runtime.DataDirectory; internal bool OfflinePreview{get;} public ToolboxViewModel(ToolboxRuntime runtime, DispatcherQueue dispatcher,bool offlinePreview=false) { galaxyLifetimeToken=lifetime.Token; OfflinePreview=offlinePreview; this.runtime = runtime; this.dispatcher = dispatcher; systemNavigator=runtime.FlightAutomation.Local;systemNavigator.Progress+=OnSystemFlightProgress; sessions = new(new WindowsGameSessionDriver(() => runtime.Player, () => PluginGameExecutable, () => PluginRelogReady, ReportRelogProgress, EnsureNativeFlightProtectionAsync), () => RunActive||NavigationActive||SystemFlightActive||NavigationRouteBusy); sessions.Progress += p => dispatcher.TryEnqueue(() => { if (p.Message.Length > 0) { SessionMessage = p.Message; runtime.Log("会话", p.Message); } }); JournalDirectory = runtime.Settings.JournalDirectory; EddnEndpoint = runtime.Settings.EddnEndpoint; EddnOnStartup = runtime.Settings.ConnectEddnOnStart; HotkeysEnabled = runtime.Settings.Hotkeys.Enabled; RelogHotkey = runtime.Settings.Hotkeys.Relog; RestartHotkey = runtime.Settings.Hotkeys.Restart; InitializePluginSettings(); InitializeTranslationSettings(); InitializeProfile(); InitializeNavigation(); InitializeMarketCacheSettings(); NavigationRouteOptions=runtime.Settings.NavigationRouteOptions; var r = runtime.Settings.Planner; Reserve = (double)r.ReserveCredits; Hold = r.CargoCapacity; JumpRange = r.LadenJumpRangeLy; MaxDistance = r.MaxLegDistanceLy; Radius = r.SearchRadiusLy; MaxLegs = r.MaxLegs; MaxAgeHours = r.MaxMarketAgeMinutes / 60.0; MaxArrival = r.MaxArrivalDistanceLs; Loop = r.Loop; RiskIndex = (int)r.Risk; ObjectiveIndex = (int)r.Objective; PadIndex = Math.Max(0, (int)r.RequiredPad - 1); IncludePlanetary = r.IncludePlanetary; IncludeCarriers = r.IncludeCarriers; AllowUnknown = r.AllowUnknownMetadata; JumpMinutes = r.MinutesPerJump; DockMinutes = r.DockingMinutes; CostPerJump = (double)r.CostPerJump; CostPerMinute = (double)r.CostPerMinute; InitializeTradePlanningOptions(r); InitializeTradeRegionScope(); runtime.DataChanged += OnDataChanged; runtime.RunChanged += OnRunChanged; runtime.LiveJournalEvent+=OnShieldJournal; timer = dispatcher.CreateTimer(); timer.Interval = TimeSpan.FromMilliseconds(200); timer.IsRepeating=false; timer.Tick += async (_, _) => { if(!initialized || lifetime.IsCancellationRequested)return; if(refreshing){timer.Start();return;} var changes=(RuntimeChange)Interlocked.Exchange(ref dirty,0); if(changes!=RuntimeChange.None)await RefreshDataAsync(changes); if(Volatile.Read(ref dirty)!=0)timer.Start(); }; } private void OnRuntimeChanged() => OnDataChanged(RuntimeChange.All); private void OnDataChanged(RuntimeChange changes) { if(lifetime.IsCancellationRequested)return; Interlocked.Or(ref dirty,(int)changes); dispatcher.TryEnqueue(()=>{if(initialized&&!timer.IsRunning&&!lifetime.IsCancellationRequested)timer.Start();}); } private static IReadOnlyList StableSequence(IReadOnlyList current,IEnumerable next) { var values=next.ToArray();return current.SequenceEqual(values)?current:values; } private void OnRunChanged(RunProgress progress) => dispatcher.TryEnqueue(() => { if (automaticTradeStop is not null) return; lastRun = progress; RunActive = progress.State is RunState.Running or RunState.Paused; RunStatus = progress.State switch { RunState.Running => "执行中", RunState.Paused => "已暂停", RunState.Completed => "已完成", RunState.Stopped => "已停止", RunState.Failed => "需要处理", _ => "等待路线" }; RunMessage = progress.Message; PauseRunCommand.NotifyCanExecuteChanged();ResumeRunCommand.NotifyCanExecuteChanged(); ActualProfit = $"{progress.ActualProfit:N0} Cr"; TradeConfirmedProfit=progress.IsSimulation?"0 Cr":ActualProfit; if(!progress.IsSimulation&&progress.State==RunState.Completed&&!plannedRoundCounted){CompletedTradeRounds++;plannedRoundCounted=true;} ProfitLabel = progress.IsSimulation ? "模拟预计净收益" : "已确认净利润"; var costs = progress.Accounting; ProfitDetails = progress.IsSimulation ? "规划估计值,未发生实际交易。" : $"燃油 {costs.Fuel:N0} · 维修 {costs.Repairs:N0} · 补给 {costs.Rearm:N0} · 其他支出 {costs.OtherExpenses:N0} Cr\n在途货物成本 {costs.CargoInvestment:N0} Cr · 现金变动 {costs.CashChange:+#,0;-#,0;0} Cr"; RunPercent = progress.State == RunState.Completed ? 100 : progress.TotalLegs == 0 ? 0 : (progress.Leg - 1d) / progress.TotalLegs * 100; NextDestination = Plan?.Legs.ElementAtOrDefault(Math.Max(0, progress.Leg - 1))?.To.DisplayName ?? "—"; RefreshTradePlanCommands(); CalculateCommand.NotifyCanExecuteChanged(); }); public async Task InitializeAsync() { initialized = true; Interlocked.Exchange(ref dirty,0); await RefreshAsync(); await LoadGalaxyCacheAsync(); // If the saved destination is already reached, present the route the // player has subsequently plotted in game. This never replots the game. if(CurrentPlayer.SystemName.Equals(NavigationDestination.Trim(),StringComparison.OrdinalIgnoreCase) &&CurrentPlayer.Navigation.Route.LastOrDefault() is {} plotted&&plotted.SystemAddress!=CurrentPlayer.Navigation.CurrentSystemAddress) NavigationDestination=plotted.Name; RefreshNavigation(CurrentPlayer); await RefreshUpgradeListAsync(); UsePlayerValues(); if(!OfflinePreview) { await ReadEnvironmentAsync(); await ReadPluginStatusAsync(); await ReadTranslationAsync(); await RefreshWalletAsync(); await InitializeAutomaticTradeAsync(); } if(runtime.Settings.ShieldZeroRestartEnabled&&sessionAutomationAllowed)ShieldZeroRestartEnabled=true; StartGameCommand.NotifyCanExecuteChanged(); RelogGameCommand.NotifyCanExecuteChanged(); RestartGameCommand.NotifyCanExecuteChanged(); } partial void OnDemoModeChanged(bool value) { if (revertingDemo) return; if (RunActive || SessionBusy) { ShowError("请先停止执行或会话操作,再切换数据模式。"); revertingDemo = true; DemoMode = !value; revertingDemo = false; return; } SetTradeCandidates([]); SelectedStart = null; marketPage = 0; UsePlayerValues(); OnRuntimeChanged(); StartGameCommand.NotifyCanExecuteChanged(); RelogGameCommand.NotifyCanExecuteChanged(); RestartGameCommand.NotifyCanExecuteChanged(); } partial void OnMarketSearchChanged(string value) { CancelMarketRead();marketPage = 0; Interlocked.Increment(ref marketRequest); OnDataChanged(RuntimeChange.Markets); } partial void OnOnlyCurrentStationChanged(bool value) { CancelMarketRead();marketPage = 0; Interlocked.Increment(ref marketRequest); OnDataChanged(RuntimeChange.Markets); } partial void OnMarketSortIndexChanged(int value) { CancelMarketRead();marketPage = 0; Interlocked.Increment(ref marketRequest); OnDataChanged(RuntimeChange.Markets); } partial void OnInventorySearchChanged(string value) => OnDataChanged(RuntimeChange.Player); partial void OnInventoryTabChanged(int value) => OnDataChanged(RuntimeChange.Player); partial void OnSelectedShipChanged(ShipLiveRow? value) { FittedModules.Apply(value?.Modules ?? []); SelectedShipSummary = value == null ? "没有飞船装配记录。" : $"{value.DisplayName} · {value.Ident}\n{value.Location} · {value.ConfigurationStatus}\n货舱 {value.CargoCapacity?.ToString() ?? "—"} t · 日志最大跳距 {value.MaxJumpRange?.ToString("F2") ?? "—"} ly · 重购 {value.Rebuy?.ToString("N0") ?? "—"} Cr\n最大跳距不是满载跳距,规划时仍需按载重校准。"; } partial void OnPlanChanged(RoutePlan? value) { NotifyTradePlanningSummary(); RefreshTradePlanCommands(); if (RunActive) return; RunStatus = value?.Legs.Count > 0 ? "待执行" : "等待路线"; RunMessage = value?.Legs.Count > 0 ? TradeHasExistingCargo?"方案已选定;请先处理现有货物,再开始自动执行。":"已选择路线,点击开始执行后启动。" : "输入投入资金并规划路线,再选择方案开始执行。"; NextDestination = value?.Legs.FirstOrDefault()?.To.DisplayName ?? "—"; } partial void OnExecutionModeIndexChanged(int value) => ExecutionHint = value switch { 0 => "模拟演练只展示步骤,不向游戏发送输入。", 1 => "你在游戏中操作;工具按 Journal 自动确认买入、到站、卖出并推进路线。", _ => "自动执行由工具箱完成采购、航行、出售及站内维护,并核对实际回执。" }; public void ShowError(string message) => dispatcher.TryEnqueue(() => { ErrorMessage = message; HasError = true; }); private const string MarketCacheFailureMessage="本地商品缓存恢复失败,行情暂不可用。飞船状态与导航继续更新,原始缓存和诊断记录已保留。"; [RelayCommand] private Task RefreshAsync() => RefreshDataAsync(RuntimeChange.All); private async Task RefreshDataAsync(RuntimeChange changes) { if(!initialized)return; if(refreshing){OnDataChanged(changes);return;} refreshing=true; try { var demo=DemoMode;var p=CurrentPlayer; if(OnlyCurrentStation&&lastMarketFilterId!=p.MarketId)changes|=RuntimeChange.Markets; if((changes&RuntimeChange.Markets)!=0) { try { var search=MarketSearch;var current=OnlyCurrentStation;var page=marketPage;var sort=(MarketSort)MarketSortIndex;var request=Volatile.Read(ref marketRequest); var searchToken=BeginMarketRead(); var data=await Task.Run(()=> { if(demo) { var book=DemoData.CreateBook(); var rows=book.Quotes.Select(q=>new MarketRow(book.Stations.First(s=>s.MarketId==q.MarketId),q)).Where(x=>(!current||x.Station.MarketId==p.MarketId)&&(x.Name.Contains(search,StringComparison.OrdinalIgnoreCase)||x.Location.Contains(search,StringComparison.OrdinalIgnoreCase))).Skip(page*150).Take(150).ToArray(); return(Rows:(IReadOnlyList)rows,Stations:book.Stations,Count:(Stations:(long)book.Stations.Count,Quotes:(long)book.Quotes.Count,Latest:(DateTimeOffset?)book.Quotes.Max(q=>q.CollectedAt))); } var rowsLive=current&&p.MarketId is null?[]:runtime.Markets.Search(search,current?p.MarketId:null,page*150,sort:sort,token:searchToken); searchToken.ThrowIfCancellationRequested(); var stations=runtime.Markets.GetStations(); if(p.MarketId is {} id&&!stations.Any(s=>s.MarketId==id)&&runtime.Markets.GetStation(id) is {} origin)stations=new[]{origin}.Concat(stations).ToArray(); return(Rows:rowsLive,Stations:stations,Count:runtime.Markets.Statistics()); },lifetime.Token); if(demo!=DemoMode){OnRuntimeChanged();return;} if(request!=Volatile.Read(ref marketRequest)){OnDataChanged(changes);return;} lastMarketFilterId=p.MarketId; MarketRows.Apply(data.Rows,MarketSortIndex!=0); MarketPageText=$"第 {marketPage+1} 页 · 本页 {MarketRows.Count} 条"; var oldStart=SelectedStart?.MarketId;var oldDestination=AutomaticDestination?.MarketId; var selectedStations=new[]{SelectedStart?.Record,AutomaticDestination?.Record,automaticTradeParameters?.Source,automaticTradeParameters?.Destination}.OfType(); Stations.Apply(data.Stations.Concat(selectedStations)); SelectedStart=Stations.FirstOrDefault(s=>s.MarketId==(oldStart??p.MarketId)); AutomaticDestination=Stations.FirstOrDefault(s=>s.MarketId==oldDestination); NotifyTradePlanningSummary(); CoverageText=$"{data.Count.Stations:N0} 个已知站点";QuoteCountText=data.Count.Quotes.ToString("N0"); LatestText=data.Count.Latest is {} at?at.ToLocalTime().ToString("MM-dd HH:mm:ss"):"尚无行情"; RefreshMarketPresentation(); if(ErrorMessage==MarketCacheFailureMessage){ErrorMessage="";HasError=false;} } catch(OperationCanceledException)when(!lifetime.IsCancellationRequested) {OnDataChanged(RuntimeChange.Markets);} catch(TimeoutException) {MarketQueryStatus="查询超时,仍显示上次有效结果。可限定当前站点或输入完整商品名后重试。";} catch(Microsoft.Data.Sqlite.SqliteException error)when(error.SqliteErrorCode is 11 or 26) { runtime.Log("商品缓存",error.ToString()); try { await Task.Run(()=>runtime.Markets.RecoverAsync(lifetime.Token),lifetime.Token); MarketPageText="商品缓存已恢复,正在重新读取"; OnDataChanged(RuntimeChange.Markets); } catch(Exception recoveryError)when(recoveryError is not OperationCanceledException) { MarketPageText="商品缓存恢复失败"; runtime.Log("商品缓存恢复",recoveryError.ToString()); ShowError(MarketCacheFailureMessage); } } } if((changes&RuntimeChange.Player)!=0) { RefreshTradePlanningPlayer(p); OnPropertyChanged(nameof(AutomaticShipSummary)); RefreshRelogAvailability(); Commander=p.Commander;RefreshProfileName();Ship=string.IsNullOrEmpty(p.Ship)?"舰船数据未知":Core.Localization.ChineseNames.Ship(p.Ship); RefreshProfileData(p); CreditsText=p.Credits?.ToString("N0")??"—";CargoText=$"{p.CargoCount:N0} / {p.CargoCapacity:N0} t"; MaterialTotalText=p.Materials.Sum(m=>m.Count).ToString("N0");MaterialKindsText=$"{p.Materials.Count} 种已持有材料"; PlayerSnapshotText=$"本机状态 · {p.UpdatedAt.ToLocalTime():MM-dd HH:mm:ss}"; BalanceNote=p.CreditsReliable?"Journal 余额 · 交易后自动更新":"余额可能不完整 · 重新登录游戏可校准"; SystemName=string.IsNullOrWhiteSpace(p.SystemName)?"等待位置数据":p.SystemName; StationName=string.IsNullOrEmpty(p.StationName)?"深空 / 航行中":p.StationName; DockingText=demo?"内部界面测试":!p.InGame?"游戏会话已结束":p.Docked?"已停靠":"航行中"; CargoNote=$"可用货舱 {p.FreeCargo:N0} t · 任务货物保留"; var inventory=InventoryTab switch{1=>p.Materials,2=>p.Locker,3=>p.Backpack,_=>p.Inventory}; InventoryRows.Apply(inventory.Where(i=>i.Name.Contains(InventorySearch,StringComparison.OrdinalIgnoreCase)||i.Key.Contains(InventorySearch,StringComparison.OrdinalIgnoreCase)).OrderBy(i=>i.Name)); RefreshInventoryPresentation(); var selectedId=SelectedShip?.ShipId??p.CurrentShipId; Ships.Apply(p.Ships);StoredModules.Apply(p.StoredModules); SelectedShip=Ships.FirstOrDefault(x=>x.ShipId==selectedId)??Ships.FirstOrDefault(); OnSelectedShipChanged(SelectedShip); FleetSummary=$"{Ships.Count} 艘已知飞船 · {Ships.Count(s=>s.ConfigurationAt!=null)} 艘有历史装配 · {StoredModules.Count} 个寄存模块"; var activeShip=p.Ships.FirstOrDefault(x=>x.ShipId==p.CurrentShipId); CurrentShipSummary=activeShip is null?"当前飞船装配未读取":$"当前:{activeShip.DisplayName} · #{activeShip.ShipId}\n{activeShip.ConfigurationStatus}"; CockpitSummary=p.Status is {} cockpit?cockpit.FuelText+"\n"+cockpit.PipsText+$"\nFlags {cockpit.Flags} · Flags2 {cockpit.Flags2}":"尚未读取座舱状态"; RefreshEngineering(p); await RefreshUpgradeListAsync(); RefreshNavigation(p); if(ImportSystemName.Length==0&&p.SystemName.Length>0)ImportSystemName=p.SystemName; } if((changes&(RuntimeChange.Player|RuntimeChange.Markets))!=0) { ReadChecks.Apply(ReadAcceptance.Build(p,demo?null:runtime.Journal.LocalMarket)); ReadSummary=$"已解析 {runtime.Journal.EventsRead:N0} 条 Journal 事件 · 检测到变化时更新\n{runtime.Journal.DirectoryPath}\n每一项时间均为游戏最后写出时间,不代表全部数据当前有效。"; } if((changes&(RuntimeChange.Connection|RuntimeChange.Markets))!=0) { EddnCountText=(runtime.Eddn?.MessagesReceived??0).ToString("N0");StatusText=runtime.Eddn?.Status??"EDDN 未连接 · 本地数据可用"; SourceNote="本地 Market + EDDN 增量行情 + Spansh 快照"; } if((changes&RuntimeChange.Activity)!=0)Activity.Apply(runtime.Activity); if((changes&RuntimeChange.GameEvents)!=0&&GameLogPageVisible&&gameLogPage==0)await RefreshGameEventsAsync(); if((changes&(RuntimeChange.Player|RuntimeChange.Markets|RuntimeChange.Connection))!=0)RefreshDashboard(p); if((changes&(RuntimeChange.Player|RuntimeChange.Markets))!=0)await RefreshGalaxyAsync(p,(changes&RuntimeChange.Markets)!=0); } catch(OperationCanceledException)when(lifetime.IsCancellationRequested){} catch(Exception e){runtime.Log("界面数据刷新",e.ToString());ShowError(e.Message);} finally{refreshing=false;} } [RelayCommand] private void UsePlayerValues() { RefreshTradePlanningPlayer(CurrentPlayer,force:true); } partial void OnSessionBusyChanged(bool value) { StartGameCommand.NotifyCanExecuteChanged(); RelogGameCommand.NotifyCanExecuteChanged(); RestartGameCommand.NotifyCanExecuteChanged(); RefreshTradePlanCommands(); CalculateCommand.NotifyCanExecuteChanged(); StartNavigationCommand.NotifyCanExecuteChanged(); StartRemoteJourneyCommand.NotifyCanExecuteChanged(); UpdateSystemControlAvailability(); } partial void OnRunActiveChanged(bool value) { RefreshTradeExecutionTimer(value);RefreshTradePlanCommands();NotifyTradePlanningSummary(); PauseRunCommand.NotifyCanExecuteChanged();ResumeRunCommand.NotifyCanExecuteChanged();StartGameCommand.NotifyCanExecuteChanged(); RelogGameCommand.NotifyCanExecuteChanged(); RestartGameCommand.NotifyCanExecuteChanged();StartNavigationCommand.NotifyCanExecuteChanged();UpdateSystemControlAvailability(); } private bool CanManageSession() => initialized && !DemoMode && !RunActive && !SessionBusy&&!NavigationActive&&!SystemFlightActive&&!NavigationRouteBusy; private bool CanRelog() => CanManageSession() && sessions.CanRelog; [RelayCommand(CanExecute = nameof(CanManageSession))] private Task StartGameAsync() => RunSessionAsync(SessionOperation.Start); [RelayCommand(CanExecute = nameof(CanRelog))] private Task RelogGameAsync() => RunSessionAsync(SessionOperation.Relog); [RelayCommand(CanExecute = nameof(CanManageSession))] private Task RestartGameAsync() => RunSessionAsync(SessionOperation.Restart); [RelayCommand] private void CancelSession() => sessionStop?.Cancel(); public void InitializeHotkeys(bool activate) { sessionAutomationAllowed=activate; hotkeys = new(WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow), operation => dispatcher.TryEnqueue(() => { if (!CanManageSession()) { SessionMessage = "当前状态不能执行会话操作。"; return; } if (operation == SessionOperation.Relog && !sessions.CanRelog) { SessionMessage = RelogAvailabilityText; return; } _ = RunSessionAsync(operation); })); if (!activate) { HotkeyStatus = "隐藏界面检查:未注册任何用户快捷键。"; return; } try { hotkeys.Apply(runtime.Settings.Hotkeys); UpdateHotkeyStatus(); } catch (Exception e) { HotkeyStatus = e.Message; ShowError(e.Message); } } private void UpdateHotkeyStatus() { var s = hotkeys?.Settings ?? new(); HotkeyStatus = !s.Enabled ? "全局快捷键已关闭。" : $"当前生效:重进 {(s.Relog.Length > 0 ? s.Relog : "未绑定")} · 大退 {(s.Restart.Length > 0 ? s.Restart : "未绑定")}。"; } [RelayCommand] private async Task SaveHotkeysAsync() { if (hotkeys is null) return; var previous = hotkeys.Settings; try { hotkeys.Apply(new(HotkeysEnabled, RelogHotkey, RestartHotkey)); await runtime.SaveSettingsAsync(runtime.Settings with { Hotkeys = hotkeys.Settings }); RelogHotkey = hotkeys.Settings.Relog; RestartHotkey = hotkeys.Settings.Restart; UpdateHotkeyStatus(); } catch (Exception e) { try { hotkeys.Apply(previous); } catch (Exception restore) { HotkeyStatus = "快捷键恢复失败:" + restore.Message; } HotkeyStatus = "未应用本次修改:" + e.Message; ShowError(e.Message); } } private async Task RunSessionAsync(SessionOperation operation) { if (!CanManageSession()) return; ActiveSessionOperation=operation;SessionBusy = true; HasError = false; using var operationStop = CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token); sessionStop = operationStop; try { await sessions.RunAsync(operation, operationStop.Token); } catch (OperationCanceledException) { SessionMessage = operation == SessionOperation.Relog ? "已取消后续重进操作,已提交给游戏的菜单操作仍会完成。" : "会话操作已取消。"; } catch (Exception e) { ShowError(e.Message); } finally { sessionStop = null; SessionBusy = false; ActiveSessionOperation=null; } } private bool CanCalculate() => !RunActive && !SessionBusy && !IsBusy&&!TradeRegionBusy&&TradeShipPadKnown; private bool CanImportData() => !IsBusy; partial void OnIsBusyChanged(bool value) { OnPropertyChanged(nameof(TradeSelectionEnabled)); RefreshTradePlanCommands(); CalculateCommand.NotifyCanExecuteChanged();ImportSystemCommand.NotifyCanExecuteChanged(); ImportSpanshCommand.NotifyCanExecuteChanged();ImportCaptureCommand.NotifyCanExecuteChanged(); } [RelayCommand(IncludeCancelCommand = true, CanExecute = nameof(CanCalculate))] private async Task CalculateAsync(CancellationToken token) { IsBusy = true; HasError = false; var progress=BeginTradePlanningProgress(token); try { RefreshTradePlanningPlayer(CurrentPlayer); if(!double.IsFinite(Radius))Radius=runtime.Settings.MarketCacheRadiusLy; await PrepareTradeRegionForPlanningAsync(token); var request = BuildRequest(); var inputVersion=Volatile.Read(ref tradePlanningInputVersion); var demo = DemoMode; SetTradeCandidates([]);TradeEmptyTitle="正在比较路线";TradeEmptyDescription="正在核对范围内的行情、供需与飞船约束。"; TradePlanningStatus="正在比较可行路线与预计小时收益…"; IReadOnlyList goals=[]; if(request.CommunityGoalSupplyOnly) { ApplyTradePlanningProgress(new("核对 CG 供货条件",0,null,"只读确认有效活动、交付站与接收商品;未确认时不推荐普通路线。")); goals=await ReadTradeCommunityGoalsForPlanningAsync(demo,token); } var book=demo?DemoData.CreateBook():await Task.Run(()=>runtime.Markets.LoadPlanningRegion(request,token:token,progress:progress),token); var result = await Task.Run(() => new TradeRoutePlanner().CalculateCandidatesWithDiagnostics(book, request,3,token,progress,goals), token); Interlocked.Increment(ref tradePlanningOperation); if(!request.CommunityGoalSupplyOnly&&result.Candidates.Count>0) { ApplyTradePlanningProgress(new("核对CG活动目录",0,null,"最多等待三秒;活动未知不影响常规市场利润的比较。")); goals=await ReadTradeCommunityGoalsForPlanningAsync(demo,token); } else if(request.CommunityGoalSupplyOnly&&result.Candidates.Count>0 &&(tradeCommunityGoalVerifiedAt is not {} readAt||DateTimeOffset.UtcNow-readAt>TimeSpan.FromMinutes(2))) goals=await ReadTradeCommunityGoalsForPlanningAsync(demo,token); token.ThrowIfCancellationRequested(); if (demo != DemoMode) return; if(inputVersion!=Volatile.Read(ref tradePlanningInputVersion)) {SetTradePlanningIssue("资料已更新,请重新规划","飞船、货舱、位置或规划参数已在计算期间变化,请按当前数据重新规划。");return;} SetTradeCandidates(result.Candidates,result.Diagnostics,goals); runtime.Log("规划",JsonSerializer.Serialize(result.Diagnostics,ToolboxSettings.JsonOptions)); if (!demo) await runtime.SaveSettingsAsync(runtime.Settings with { Planner = request }); } catch (OperationCanceledException) { SetTradePlanningIssue("规划已取消","本次规划已取消,点击规划路线可重新比较。"); } catch (Exception e) { SetTradePlanningIssue("暂时无法生成方案",e.Message);ShowError(e.Message); } finally { EndTradePlanningProgress();IsBusy = false; } } private PlannerRequest BuildRequest() { var player=CurrentPlayer; var defaults=runtime.Settings.Planner; static double Optional(double value,double fallback)=>double.IsFinite(value)?value:fallback; var cash=Optional(Cash,(double)(player.Credits??0));var reserve=Optional(Reserve,(double)player.Rebuy); var investment=Optional(TradeInvestment,Math.Max(0,cash-reserve)); if(investment<=0||investment>Math.Max(0,cash-reserve))throw new ArgumentException("投入资金须大于零,且不能超过留出运营与保险资金后的可用余额。"); var shipPad=ShipPadPolicy.RequiredPad(player.Ship); if(shipPad==LandingPad.Unknown)throw new ArgumentException("当前飞船尺寸尚未识别,请等待真实飞船数据后再规划。"); var request = new PlannerRequest { StartMarketId = player.MarketId??0, Credits = (decimal)cash, ReserveCredits = Math.Max((decimal)reserve,(decimal)cash-(decimal)investment), InvestmentCredits=investmentFollowsBalance?null:(decimal)investment, CargoCapacity = checked((int)Optional(Hold,player.CargoCapacity)),OccupiedCargo=player.CargoCount,LadenJumpRangeLy = Optional(JumpRange,player.MaxJumpRange??defaults.LadenJumpRangeLy),JumpRangeIsEstimate=true, CurrentPosition=player.Position,CurrentSystemName=player.SystemName,CurrentMarketId=player.MarketId,MaxLegDistanceLy = Optional(MaxDistance,defaults.MaxLegDistanceLy), SearchRadiusLy = Optional(Radius,runtime.Settings.MarketCacheRadiusLy),SearchCenterPosition=EffectiveTradeRegionCenter,SearchCenterName=TradeRegionCenterText.Trim().Length==0?player.SystemName:resolvedTradeRegionName, MaxLegs = checked((int)Optional(MaxLegs,defaults.MaxLegs)), Loop = Loop,CommunityGoalSupplyOnly=TradePlanningModeIndex==1, CommunityGoalCommodityFilter=TradePlanningModeIndex==1?TradeSearchConstraints.ParseCommodityFilter(TradeCommoditySearchText):[], SourceMarketIdFilter=tradeSourceSearchMarketId,DestinationMarketIdFilter=tradeDestinationSearchMarketId, SourceSystemFilter=TradeSourceSystemText?.Trim()??"",SourceStationFilter=TradeSourceSearchStation?.Record.Name??TradeSourceSearchText?.Trim()??"", DestinationSystemFilter=TradeDestinationSystemText?.Trim()??"",DestinationStationFilter=TradeDestinationSearchStation?.Record.Name??TradeDestinationSearchText?.Trim()??"", Risk = RiskIndex is >=0 and <=2?(RiskTolerance)RiskIndex:defaults.Risk, Objective = ObjectiveIndex is >=0 and <=2?(RouteObjective)ObjectiveIndex:defaults.Objective, ActualShipType=player.Ship,ShipRequiredPad=shipPad,AllowedSourceKinds=SelectedTradeSourceKinds==0?StationSourceKinds.AllKnown:SelectedTradeSourceKinds, RequiredPad = (LandingPad)Math.Max((int)shipPad,PadIndex is >=0 and <=2?PadIndex+1:(int)shipPad), IncludePlanetary = true, IncludeCarriers = true, AllowUnknownMetadata = AllowUnknown, MaxMarketAgeMinutes = checked((int)(Optional(MaxAgeHours,defaults.MaxMarketAgeMinutes/60d) * 60)), MaxArrivalDistanceLs = Optional(MaxArrival,defaults.MaxArrivalDistanceLs), MinutesPerJump = Optional(JumpMinutes,defaults.MinutesPerJump), DockingMinutes = Optional(DockMinutes,defaults.DockingMinutes), CostPerJump = (decimal)Optional(CostPerJump,(double)defaults.CostPerJump), CostPerMinute = (decimal)Optional(CostPerMinute,(double)defaults.CostPerMinute) }; request.Validate(); return request; } private bool CanStartPlan() => Plan?.Legs.Count > 0 && !RunActive && !SessionBusy&&!NavigationActive; [RelayCommand(CanExecute = nameof(CanStartPlan))] private async Task StartPlanAsync(CancellationToken cancellationToken=default) { using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token,cancellationToken);var token=stop.Token; try { if (Plan == null) return; if (DemoMode && ExecutionModeIndex != 0) throw new InvalidOperationException("演示沙盘仅支持模拟演练。"); if (ExecutionModeIndex is < 0 or > 2) throw new InvalidOperationException("请选择有效的执行方式。"); if(ExecutionModeIndex==2&&(!CurrentPlayer.Docked||CurrentPlayer.MarketId!=Plan.Legs[0].From.MarketId)) throw new InvalidOperationException("请先停靠 "+Plan.Legs[0].From.DisplayName+" 再执行此完整路线;前往起点的耗时目前只计入估算。"); BeginPlannedTradeMetrics(); var nativeDriver=ExecutionModeIndex==2?await CreateNativeTradeDriverAsync(Plan,token):null; token.ThrowIfCancellationRequested(); AppShellPage.Current?.Navigate(typeof(PlannerPage)); RunActive = true; await runtime.Runner.RunAsync(Plan, (ExecutionMode)ExecutionModeIndex, nativeDriver, token); } catch(OperationCanceledException){runtime.Log("执行","执行已取消。");} catch (Exception e) { ShowError(e.Message); } finally { RunActive = false; RefreshTradePlanCommands(); CalculateCommand.NotifyCanExecuteChanged(); } } private bool CanPauseRun()=>automaticTradeStop is null&&runtime.Runner.State==RunState.Running; private bool CanResumeRun()=>automaticTradeStop is null&&runtime.Runner.State==RunState.Paused; [RelayCommand(CanExecute=nameof(CanPauseRun))] private void PauseRun() => runtime.Runner.Pause(); [RelayCommand(CanExecute=nameof(CanResumeRun))] private void ResumeRun() { try { runtime.Runner.Resume(); } catch (Exception e) { ShowError(e.Message); } } [RelayCommand] private void StopRun() { if (automaticTradeStop is { } stop) { RunStatus = "正在取消"; RunMessage = "正在停止跑商,核对已提交请求并释放控制。"; stop.Cancel(); return; } runtime.Runner.Stop(); } [RelayCommand] private void PreviousMarketPage() { CancelMarketRead();marketPage = Math.Max(0, marketPage - 1); Interlocked.Increment(ref marketRequest); OnDataChanged(RuntimeChange.Markets); } [RelayCommand] private void NextMarketPage() { CancelMarketRead();if (MarketRows.Count == 150) marketPage++; Interlocked.Increment(ref marketRequest); OnDataChanged(RuntimeChange.Markets); } [RelayCommand] private void CopyDestination() { var leg = Plan?.Legs.ElementAtOrDefault(Math.Max(0, (lastRun?.Leg ?? 1) - 1)); if (leg == null) return; var data = new DataPackage(); data.SetText(leg.To.SystemName); Clipboard.SetContent(data); runtime.Log("导航", $"已复制目的星系:{leg.To.SystemName}"); } [RelayCommand] private async Task ExportPlanAsync() { try { if (Plan == null) throw new InvalidOperationException("请先计算路线。"); var picker = new FileSavePicker { SuggestedFileName = $"ED-Trade-{DateTime.Now:yyyyMMdd-HHmm}" }; picker.FileTypeChoices.Add("路线 JSON", new[] { ".json" }); InitializePicker(picker); if (await picker.PickSaveFileAsync() is { } file) await File.WriteAllTextAsync(file.Path, JsonSerializer.Serialize(Plan, ToolboxSettings.JsonOptions)); } catch (Exception e) { ShowError(e.Message); } } [RelayCommand] private async Task ExportReadReportAsync() { try { if (DemoMode) throw new InvalidOperationException("请切换到实时接入后导出真实数据验收报告。"); var p = runtime.Player; var cache = runtime.Markets.Statistics(); var report = new { generatedAt = DateTimeOffset.UtcNow, mode = "external-read-only", journalDirectory = runtime.Journal.DirectoryPath, eventsRead = runtime.Journal.EventsRead, checks = ReadAcceptance.Build(p, runtime.Journal.LocalMarket), environment = gameEnvironment, player = p with { CommanderId = "" }, localMarket = runtime.Journal.LocalMarket, publicMarketCache = new { cache.Stations, cache.Quotes, cache.Latest }, limits = new[] { "未使用过的远程飞船可能只有清单而没有装配", "历史装配不等于实时配置", "公共行情不覆盖所有站点的即时价格", "本报告不证明零封禁风险" } }; var folder = Path.Combine(runtime.DataDirectory, "reports"); Directory.CreateDirectory(folder); var path = Path.Combine(folder, $"read-acceptance-{DateTime.Now:yyyyMMdd-HHmmss}.json"); await File.WriteAllTextAsync(path, JsonSerializer.Serialize(report, ToolboxSettings.JsonOptions)); ReadReportPath = path; runtime.Log("验收", "已导出只读数据报告(不含账号认证信息)。"); } catch (Exception e) { ShowError(e.Message); } } [RelayCommand] private async Task SaveSettingsAsync() { try { if (!Uri.TryCreate(EddnEndpoint, UriKind.Absolute, out var uri) || uri.Scheme != "tcp" || uri.Port <= 0) throw new ArgumentException("请输入有效的 EDDN TCP 地址。"); await runtime.SaveSettingsAsync(runtime.Settings with { JournalDirectory = JournalDirectory.Trim(), EddnEndpoint = EddnEndpoint.Trim(), ConnectEddnOnStart = EddnOnStartup }); OnRuntimeChanged(); } catch (Exception e) { ShowError(e.Message); } } [RelayCommand] private async Task ToggleEddnAsync() { try { if (runtime.Eddn == null) runtime.StartEddn(); else await runtime.StopEddnAsync(); } catch (Exception e) { ShowError(e.Message); } } [RelayCommand(IncludeCancelCommand = true,CanExecute=nameof(CanImportData))] private async Task ImportSystemAsync(CancellationToken token) { IsBusy = true; try { var progress = new Progress(p => ImportStatus = $"{p.Message} · {p.Systems} 星系 / {p.Stations} 站点 / {p.Quotes:N0} 报价"); var result = ImportNearby ? await runtime.Spansh.ImportNearbyAsync(ImportSystemName.Trim(), 20, progress, token) : await runtime.Spansh.ImportSystemAsync(ImportSystemName.Trim(), progress, token); ImportStatus = $"已处理 {result.Systems} 个星系、{result.Stations} 个站点;更新 {result.Quotes:N0} 条报价。保留源数据时间。"; runtime.Log("导入", ImportStatus); OnRuntimeChanged(); } catch (OperationCanceledException) { ImportStatus = "导入已取消,已完成的数据保留。"; } catch (Exception e) { ImportStatus = "导入未完成"; ShowError(e.Message); } finally { IsBusy = false; } } [RelayCommand(CanExecute=nameof(CanImportData))] private Task ImportSpanshAsync(CancellationToken token)=>ImportFileAsync("spansh",token); [RelayCommand(CanExecute=nameof(CanImportData))] private Task ImportCaptureAsync(CancellationToken token)=>ImportFileAsync("capture",token); private async Task ImportFileAsync(string kind, CancellationToken token) { IsBusy = true; try { var picker = new FileOpenPicker(); foreach (var ext in kind == "capture" ? new[] { ".zip" } : new[] { ".json", ".gz" }) picker.FileTypeFilter.Add(ext); InitializePicker(picker); if (await picker.PickSingleFileAsync().AsTask(token) is not { } file) return; token.ThrowIfCancellationRequested(); var progress = new Progress(p => ImportStatus = $"{p.Message} · {p.Stations} 站点 / {p.Quotes:N0} 报价"); var result = kind == "capture" ? await Task.Run(() => CaptureImporter.ImportAsync(file.Path, runtime.Markets, progress, token), token) : await Task.Run(() => runtime.Spansh.ImportFileAsync(file.Path, progress: progress, token: token), token); ImportStatus = $"完成:{result.Stations} 个站点,{result.Quotes:N0} 条报价。"; runtime.Log("导入", ImportStatus); OnRuntimeChanged(); } catch (OperationCanceledException) { ImportStatus = "导入已取消,已完成的数据保留。"; } catch (Exception e) { ShowError(e.Message); } finally { IsBusy = false; } } [RelayCommand] private async Task ChooseJournalAsync() { var picker = new FolderPicker(); picker.FileTypeFilter.Add("*"); InitializePicker(picker); if (await picker.PickSingleFolderAsync() is { } folder) JournalDirectory = folder.Path; } [RelayCommand] private async Task ReadEnvironmentAsync(CancellationToken cancellationToken=default) { using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token,cancellationToken);var token=stop.Token; try { gameEnvironment = await GameEnvironmentReader.ReadAsync(runtime.Player, token: token); EnvironmentText = gameEnvironment.Summary + "\n" + string.Join("\n", gameEnvironment.Diagnostics); } catch(OperationCanceledException){EnvironmentText="环境读取已取消。";} catch (Exception e) { ShowError(e.Message); } } [RelayCommand] private void OpenDataDirectory() => Process.Start(new ProcessStartInfo(runtime.DataDirectory) { UseShellExecute = true }); private static void InitializePicker(object picker) => WinRT.Interop.InitializeWithWindow.Initialize(picker, WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow)); public void Dispose() { runtime.FlightAutomation.Progress-=OnFlightAutomationProgress;runtime.FlightAutomation.Stop(); runtime.Navigator.Progress-=OnNavigationProgress;runtime.Navigator.Stop(); systemNavigator.Progress-=OnSystemFlightProgress;systemNavigator.Stop();systemFlightStop?.Cancel();currentSystemTimer?.Stop();currentSystemSource?.Dispose(); if(shipProtection is {} protection)_=protection.DisposeAsync(); hotkeys?.Dispose(); sessionStop?.Cancel(); runtime.LiveJournalEvent-=OnShieldJournal; walletTimer?.Stop(); tradeExecutionTimer?.Stop(); shipTelemetryTimer?.Stop(); timer.Stop(); runtime.DataChanged -= OnDataChanged; runtime.RunChanged -= OnRunChanged; lifetime.Cancel(); lifetime.Dispose(); galaxyStreaming.Dispose();galaxyCellStreaming.Dispose();if(nativeGalaxy is not null&&galaxyNativeProgressHandler is not null)nativeGalaxy.RegionProgress-=galaxyNativeProgressHandler; galaxyHttp.Dispose(); } }