using System.Text.Json;
using Microsoft.UI.Dispatching;
using XFEExtension.NetCore.WinUIHelper.Utilities;
using XFEEDToolBox.Core.Services;
using XFEEDToolBox.ViewModels;
using XFEEDToolBox.Views;
using XFEEDToolBox.Services;
namespace XFEEDToolBox;
public partial class App : Application
{
public static MainWindow MainWindow { get; private set; } = null!;
public static ToolboxRuntime Runtime { get; private set; } = null!;
public static ToolboxViewModel ViewModel { get; private set; } = null!;
public static readonly Type[] Pages = [typeof(MainPage),typeof(ProfilePage), typeof(GalaxyMapPage), typeof(NavigationPage),typeof(CurrentSystemPage), typeof(MarketPage), typeof(MaterialsPage), typeof(RefitPage), typeof(RefitShipDetailsPage), typeof(RefitModulePickerPage), typeof(ShipPage), typeof(EngineeringPage), typeof(UpgradeListPage), typeof(PlannerPage), typeof(ExecutionPage), typeof(TranslationPage), typeof(ReadPage), typeof(SettingPage), typeof(LogViewPage)];
public App()
{
InitializeComponent();
foreach (var page in Pages) PageManager.RegisterPage(page);
UnhandledException += (_, e) =>
{
Runtime?.Log("界面异常", e.Exception?.ToString()??e.Message);
ViewModel?.ShowError(e.Message);
e.Handled = true;
};
}
protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
var commandLine = Environment.GetCommandLineArgs();
var offlinePreview=commandLine.Contains("--offline-preview");
var marketProbe=GetArgument(commandLine,"--smoke-market") is not null||GetArgument(commandLine,"--check-market-cache") is not null;
if(offlinePreview&&GetArgument(commandLine,"--data-dir") is null)throw new ArgumentException("Offline preview requires an isolated --data-dir.");
var directory = GetArgument(commandLine, "--data-dir") ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "XFEstudio", "XFEEDToolBox");
Runtime = new(directory);
MainWindow = new();
if(GetArgument(commandLine,"--notify-announcement") is {} announcementPath)
{await SendAnnouncementOnceAsync(announcementPath);return;}
ViewModel = new(Runtime, MainWindow.DispatcherQueue,offlinePreview||marketProbe);
var hiddenProbe=GetArgument(commandLine,"--check-background-data") is not null||GetArgument(commandLine,"--smoke-notifications") is not null||GetArgument(commandLine,"--smoke-current-system") is not null||GetArgument(commandLine,"--smoke-interface") is not null||GetArgument(commandLine,"--smoke-ship-status") is not null||GetArgument(commandLine,"--smoke-refinement") is not null||GetArgument(commandLine,"--smoke-skybox") is not null||GetArgument(commandLine,"--smoke-ui") is not null||GetArgument(commandLine,"--smoke-layout") is not null||GetArgument(commandLine,"--smoke-models") is not null||GetArgument(commandLine,"--smoke-map") is not null||GetArgument(commandLine,"--accept-navigation") is not null||GetArgument(commandLine,"--accept-game-route") is not null||GetArgument(commandLine,"--check-navigation") is not null||GetArgument(commandLine,"--smoke-workspace") is not null;
hiddenProbe|=GetArgument(commandLine,"--smoke-game-icons") is not null;
hiddenProbe|=GetArgument(commandLine,"--smoke-relog") is not null;
hiddenProbe|=GetArgument(commandLine,"--smoke-loading-hologram") is not null;
hiddenProbe|=marketProbe;
hiddenProbe|=GetArgument(commandLine,"--check-system-read") is not null;
hiddenProbe|=GetArgument(commandLine,"--accept-sco") is not null;
ViewModel.InitializeHotkeys((!hiddenProbe||GetArgument(commandLine,"--accept-sco") is not null)&&!offlinePreview&&!commandLine.Contains("--no-session-automation"));
MainWindow.Content = new AppShellPage();
InitializeArrivalNotifications(!hiddenProbe&&!offlinePreview);
if (commandLine.Contains("--maximized")) ((Microsoft.UI.Windowing.OverlappedPresenter)MainWindow.AppWindow.Presenter).Maximize();
var closing=false;
MainWindow.Closed += (_, _) =>
{
closing=true;
StopArrivalNotifications();ViewModel.Dispose();
// A final-window async-void handler may be abandoned when the host
// exits. Complete background writers and their cache seals before
// returning; dispose runs without the closing UI context.
Task.Run(async()=>await Runtime.DisposeAsync()).GetAwaiter().GetResult();
};
if (!hiddenProbe&&!commandLine.Contains("--background-start"))
{if(commandLine.Contains("--show-inactive"))MainWindow.AppWindow.Show(false);else MainWindow.Activate();}
else MainWindow.AppWindow.Hide();
try
{
await Runtime.InitializeAsync(offline:offlinePreview||marketProbe,validateMarketCache:GetArgument(commandLine,"--check-market-cache") is not null);
if(closing)return;
if(GetArgument(commandLine,"--check-market-cache") is {} cacheReport){await RunMarketCacheCheckAsync(cacheReport);return;}
if(GetArgument(commandLine,"--check-background-data") is {} backgroundReport){await RunBackgroundDataProbeAsync(backgroundReport);return;}
await ViewModel.InitializeAsync();
if(closing)return;
if(GetArgument(commandLine,"--upgrade-native-only") is {} maintenance)
{await ViewModel.UpgradeNativeSupportOnlyAsync(maintenance,GetArgument(commandLine,"--maintenance-report")??throw new ArgumentException("Maintenance report path required."));return;}
if(GetArgument(commandLine,"--accept-sco") is {} scoReport){await RunScoAcceptanceAsync(commandLine,scoReport);return;}
if(GetArgument(commandLine,"--update-and-automatic-trade") is {} updateTrade){AppShellPage.Current!.Navigate(typeof(PlannerPage));await ViewModel.UpdateAndRunAutomaticTradeAsync(updateTrade);return;}
if(GetArgument(commandLine,"--automatic-trade") is {} automaticTrade){AppShellPage.Current!.Navigate(typeof(PlannerPage));await ViewModel.RunAutomaticTradeAsync(automaticTrade);return;}
if(GetArgument(commandLine,"--station-journey") is {} journey)
{
var destination=System.Text.Json.JsonSerializer.Deserialize<XFEEDToolBox.Core.Models.Station>(await File.ReadAllTextAsync(journey),XFEEDToolBox.Core.Models.ToolboxSettings.JsonOptions)
??throw new ArgumentException("Station journey requires a destination station.");
AppShellPage.Current!.Navigate(typeof(NavigationPage));await ViewModel.RunStationJourneyAsync(destination);return;
}
if(GetArgument(commandLine,"--check-system-read") is {} systemReadReport){await RunSystemReadCheckAsync(systemReadReport);return;}
if(GetArgument(commandLine,"--smoke-market") is {} marketReport){await RunMarketProbeAsync(marketReport);return;}
if(GetArgument(commandLine,"--smoke-relog") is {} relogReport){await RunRelogProbeAsync(relogReport);return;}
if(GetArgument(commandLine,"--smoke-loading-hologram") is {} hologramReport){await RunLoadingHologramProbeAsync(hologramReport,GetArgument(commandLine,"--game-executable")??throw new ArgumentException("Original game executable is required."));return;}
if(GetArgument(commandLine,"--smoke-game-icons") is {} iconReport){await RunGameIconProbeAsync(iconReport);return;}
if(GetArgument(commandLine,"--smoke-notifications") is {} notificationReport){await RunArrivalNotificationProbeAsync(notificationReport);return;}
if(GetArgument(commandLine,"--smoke-current-system") is {} systemReport){await RunCurrentSystemProbeAsync(systemReport);return;}
if(GetArgument(commandLine,"--smoke-interface") is {} interfaceReport){await RunInterfaceOnlyProbeAsync(interfaceReport);return;}
if(GetArgument(commandLine,"--smoke-ship-status") is {} shipStatusReport){await RunShipStatusProbeAsync(shipStatusReport);return;}
if(GetArgument(commandLine,"--smoke-refinement") is {} refinementReport){await RunRefinementProbeAsync(refinementReport);return;}
if(GetArgument(commandLine,"--smoke-skybox") is {} skyboxReport){await RunSkyboxProbeAsync(commandLine,skyboxReport);return;}
if(GetArgument(commandLine,"--smoke-workspace") is {} workspaceReport){await RunWorkspaceProbeAsync(workspaceReport);return;}
if(GetArgument(commandLine,"--check-navigation") is {} navigationCheck){await RunNavigationCheckAsync(commandLine,navigationCheck);return;}
if(GetArgument(commandLine,"--accept-game-route") is {} routeReport){await RunGameRouteAcceptanceAsync(commandLine,routeReport);return;}
if(GetArgument(commandLine,"--accept-navigation") is {} navigationReport){await RunNavigationAcceptanceAsync(commandLine,navigationReport);return;}
if(!hiddenProbe&&GetArgument(commandLine,"--page") is {} requestedPage)
{
var page=Pages.FirstOrDefault(p=>p.Name.Equals(requestedPage,StringComparison.OrdinalIgnoreCase)||p.Name.Equals(requestedPage+"Page",StringComparison.OrdinalIgnoreCase));
if(page is not null)AppShellPage.Current!.Navigate(page);
}
if(GetArgument(commandLine,"--smoke-layout") is {} layoutReport){await RunLayoutProbeAsync(layoutReport);return;}
if(GetArgument(commandLine,"--smoke-models") is {} modelReport){await RunModelProbeAsync(modelReport);return;}
if(GetArgument(commandLine,"--smoke-map") is {} mapReport){await RunMapPerformanceProbeAsync(mapReport);return;}
if (GetArgument(commandLine, "--smoke-ui") is { } report) await RunUiSmokeAsync(report);
if(GetArgument(commandLine,"--market-report") is {} marketRuntimeReport)_=WriteMarketRuntimeReportAsync(marketRuntimeReport);
}
catch (Exception e) { ViewModel.ShowError(e.Message); Runtime.Log("启动错误", e.ToString()); }
}
private static string? GetArgument(string[] args, string name)
{
var index = Array.IndexOf(args, name);
return index >= 0 && index + 1 < args.Length ? args[index + 1] : null;
}
private static async Task RunUiSmokeAsync(string reportPath)
{
var results = new List<object>();
ViewModel.DemoMode = false;
await ViewModel.RefreshCommand.ExecuteAsync(null);
await ViewModel.ExportReadReportCommand.ExecuteAsync(null);
var actualModules = ViewModel.FittedModules.Count;
var actualLocalization=new{untranslatedModules=Runtime.Player.Ships.SelectMany(s=>s.Modules).Where(m=>m.DisplayName.Contains("未收录")||m.SlotName.Contains("未收录")).Select(m=>new{m.Slot,m.Item}).ToArray(),untranslatedModifiers=Runtime.Player.Ships.SelectMany(s=>s.Modules).SelectMany(m=>m.Engineering?.Modifiers??[]).Where(m=>m.Display.Contains("未收录")).Select(m=>m.Label).Distinct().ToArray(),localizedEffectsWithEnglish=Runtime.Player.Ships.SelectMany(s=>s.Modules).Where(m=>m.Engineering?.ExperimentalEffectId.Length>0).All(m=>!Core.Localization.ChineseNames.IsChinese(m.EngineeringOriginal))};
var hotkeyProbe = await WindowsGlobalHotkeys.ProbeAsync(WinRT.Interop.WindowNative.GetWindowHandle(MainWindow));
var existing=ViewModel.EngineeringSources.FirstOrDefault(s=>s.Module?.Engineering is not null);
if(existing is not null)ViewModel.EngineeringSource=existing;
await ViewModel.CalculateEngineeringCommand.ExecuteAsync(null);
var engineering = new { engineers = Runtime.Player.Engineers.Count, verifiedRolls = Runtime.Player.VerifiedEngineeringRolls, mismatchedRolls = Runtime.Player.MismatchedEngineeringRolls,
materials = ViewModel.MaterialStocks.Count, materialTotal = Runtime.Player.Materials.Sum(x => x.Count), modules = ViewModel.EngineeringModules.Count,
module = ViewModel.EngineeringModule?.Slot, blueprint = ViewModel.EngineeringBlueprint, result = ViewModel.EngineeringResult, rolls = ViewModel.EngineeringRolls.Count, ingredients = ViewModel.EngineeringNeeds.Count };
var sourceModule = ViewModel.RefitSlot?.Module;
var sourceSlot = ViewModel.RefitSlot?.Slot;
if (sourceModule is not null && ViewModel.RefitEngineer is not null && ViewModel.RefitBlueprint is not null)
{
ViewModel.RefitEditEnabled = true; ViewModel.RefitGrade = 5; ViewModel.RefitQuality = 100;
var other = ViewModel.RefitSlots.FirstOrDefault(s => s.Slot != sourceSlot && s.Module is not null && Core.Engineering.EngineeringCatalog.ModuleType(s.Module.Item) == Core.Engineering.EngineeringCatalog.ModuleType(sourceModule.Item));
if (other is not null) { ViewModel.RefitSlot = ViewModel.RefitVisibleSlots.First(s=>s.Slot==other.Slot); ViewModel.RefitEditEnabled = true; ViewModel.RefitGrade = 5; }
await ViewModel.CalculateRefitCommand.ExecuteAsync(null);
}
var refit = new { source = ViewModel.RefitSourceShip?.Type, slots = ViewModel.RefitSlots.Count, jobs = ViewModel.RefitModuleResults.Count, ingredients = ViewModel.RefitNeeds.Count,
targets = ViewModel.RefitDocument?.Slots.Where(s => s.Target is not null).Select(s => new { s.Slot, s.Target }),
result = ViewModel.RefitResult, sourceUnchanged = sourceModule is null || Runtime.Player.Ships.FirstOrDefault(s => s.ShipId == ViewModel.RefitSourceShip?.ShipId)?.Modules.FirstOrDefault(m => m.Slot == sourceSlot) == sourceModule,
gameEditsInvoked = false };
var plugin = new { status = ViewModel.PluginStatusText, installed = ViewModel.PluginInstalled, tradeReady = ViewModel.PluginTradeReady, installationInvoked = false };
await ViewModel.ExportRefitCommand.ExecuteAsync(null);
var refitExport=Directory.Exists(Path.Combine(Runtime.DataDirectory,"refits"))
? Directory.GetFiles(Path.Combine(Runtime.DataDirectory,"refits"),"refit-*.json").OrderByDescending(File.GetLastWriteTimeUtc).FirstOrDefault() : null;
var acceptanceReport = ViewModel.ReadReportPath;
var refitWorkbench = await RunRefitWorkbenchProbeAsync(reportPath);
var incremental = await RunIncrementalProbeAsync();
var navigation=await RunNavigationProbeAsync();
var upgradeList=await RunUpgradeListProbeAsync();
var galaxyProbe=await RunGalaxyProbeAsync();
foreach (var page in Pages)
{
AppShellPage.Current!.Navigate(page);
await Task.Delay(250);
var root = AppShellPage.Current.CurrentPage;
results.Add(new { page = page.Name, loaded = root?.IsLoaded == true, elements = CountVisuals(root) });
foreach (var pivot in Descendants(root).OfType<Pivot>())
for (var index = 0; index < pivot.Items.Count; index++) { pivot.SelectedIndex = index; await Task.Delay(150); }
}
ViewModel.DemoMode = true;
await ViewModel.RefreshCommand.ExecuteAsync(null);
ViewModel.UsePlayerValuesCommand.Execute(null);
await ViewModel.CalculateCommand.ExecuteAsync(null);
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(reportPath))!);
await File.WriteAllTextAsync(reportPath, JsonSerializer.Serialize(new { pages = results, actualModules, acceptanceReport, engineering, refit, refitExport, refitWorkbench, plugin, incremental, navigation, upgradeList, actualLocalization, galaxyProbe, galaxy=new{systems=ViewModel.GalaxySystems.Count,route=ViewModel.GalaxyRoute.Count,centre=ViewModel.GalaxyCentre,generatedStars=false}, profile = new { avatarPresent=ViewModel.ProfileAvatar is not null, namePresent=ViewModel.ProfileDisplayName.Length>0 }, translation=ViewModel.TranslationStatusText, planLegs = ViewModel.Plan?.Legs.Count, error = ViewModel.ErrorMessage, hotkeyStatus = ViewModel.HotkeyStatus, hotkeyProbe, gameSessionActionsInvoked = false }, new JsonSerializerOptions { WriteIndented = true }));
MainWindow.Close();
}
private static async Task<object> RunNavigationProbeAsync()
{
var route=Runtime.Player.Navigation.Route;
if(route.Count>0)ViewModel.NavigationDestination=route[^1].Name;
AppShellPage.Current!.Navigate(typeof(NavigationPage));await Task.Delay(250);
var loaded=AppShellPage.Current.CurrentPage?.IsLoaded==true;
var start=Descendants(AppShellPage.Current.CurrentPage).OfType<Button>().FirstOrDefault(b=>b.Content as string=="开始自动导航");
var enabled=start?.IsEnabled==true;
var before=ViewModel.NavigationLegs.Select(r=>r.Record).ToArray();var changed=0;
var sample=new Core.Navigation.NavigationLegPreview(1,1,"Isolated UI fixture","K",10,true,false,true,2);
ViewModel.NavigationLegs.Apply([sample]);await Task.Delay(120);
System.Collections.Specialized.NotifyCollectionChangedEventHandler handler=(_,_)=>changed++;
ViewModel.NavigationLegs.CollectionChanged+=handler;
ViewModel.NavigationLegs.Apply([sample with{Completed=true,Next=false}]);await Task.Delay(100);
var updated=Descendants(AppShellPage.Current.CurrentPage).OfType<TextBlock>().Any(t=>t.Text=="已抵达");
ViewModel.NavigationLegs.CollectionChanged-=handler;ViewModel.NavigationLegs.Apply(before);
return new{pageLoaded=loaded,actualRouteWaypoints=route.Count,previewRows=before.Length,startRespondsWithPreflight=enabled,readinessHint=ViewModel.NavigationStartHint,incrementalStateBound=updated,collectionEvents=changed,gameFlightCommandsInvoked=false};
}
private static async Task<object> RunIncrementalProbeAsync()
{
AppShellPage.Current!.Navigate(typeof(MaterialsPage));await Task.Delay(250);
var materials=ViewModel.MaterialStocks.Select(r=>r.Record).ToArray();var materialRow=ViewModel.MaterialStocks.FirstOrDefault();
var materialEvents=0;var materialProperties=new List<string>();var materialBound=false;
if(materialRow is not null)
{
System.Collections.Specialized.NotifyCollectionChangedEventHandler collection=(_,_)=>materialEvents++;
System.ComponentModel.PropertyChangedEventHandler property=(_,e)=>materialProperties.Add(e.PropertyName??"");
ViewModel.MaterialStocks.CollectionChanged+=collection;materialRow.PropertyChanged+=property;
var text=Descendants(AppShellPage.Current.CurrentPage).OfType<TextBlock>().FirstOrDefault(t=>ReferenceEquals(t.DataContext,materialRow)&&t.Tag as string=="material-quantity");
ViewModel.MaterialStocks.Apply(materials.Select(m=>m.Key==materialRow.Key?m with{Owned=m.Owned+1}:m));
await Task.Delay(80);materialBound=text?.Text==materialRow.Quantity;
ViewModel.MaterialStocks.CollectionChanged-=collection;materialRow.PropertyChanged-=property;
ViewModel.MaterialStocks.Apply(materials);
}
AppShellPage.Current.Navigate(typeof(MarketPage));await Task.Delay(250);
var markets=ViewModel.MarketRows.Select(r=>r.Record).ToArray();var marketRow=ViewModel.MarketRows.FirstOrDefault();var marketEvents=0;var marketProperties=new List<string>();var marketBound=false;
if(marketRow is not null)
{
System.Collections.Specialized.NotifyCollectionChangedEventHandler collection=(_,_)=>marketEvents++;
System.ComponentModel.PropertyChangedEventHandler property=(_,e)=>marketProperties.Add(e.PropertyName??"");
ViewModel.MarketRows.CollectionChanged+=collection;marketRow.PropertyChanged+=property;
var text=Descendants(AppShellPage.Current.CurrentPage).OfType<TextBlock>().FirstOrDefault(t=>ReferenceEquals(t.DataContext,marketRow)&&Grid.GetColumn(t)==2);
ViewModel.MarketRows.Apply(markets.Select((m,i)=>i==0?m with{Quote=m.Quote with{BuyPrice=m.Quote.BuyPrice+1}}:m));
await Task.Delay(80);marketBound=text?.Text==marketRow.Buy;
ViewModel.MarketRows.CollectionChanged-=collection;marketRow.PropertyChanged-=property;
ViewModel.MarketRows.Apply(markets);
}
return new{materialCollectionEvents=materialEvents,materialProperties,materialTextUpdated=materialBound,marketCollectionEvents=marketEvents,marketProperties,marketTextUpdated=marketBound};
}
private static int CountVisuals(DependencyObject? node)
{
if (node is null) return 0;
var count = 1;
for (var i = 0; i < Microsoft.UI.Xaml.Media.VisualTreeHelper.GetChildrenCount(node); i++) count += CountVisuals(Microsoft.UI.Xaml.Media.VisualTreeHelper.GetChild(node, i));
return count;
}
private static IEnumerable<DependencyObject> Descendants(DependencyObject? node)
{
if (node is null) yield break;
yield return node;
for (var i = 0; i < Microsoft.UI.Xaml.Media.VisualTreeHelper.GetChildrenCount(node); i++)
foreach (var child in Descendants(Microsoft.UI.Xaml.Media.VisualTreeHelper.GetChild(node, i))) yield return child;
}
}
using System.Text.Json;
using Microsoft.UI.Dispatching;
using XFEExtension.NetCore.WinUIHelper.Utilities;
using XFEEDToolBox.Core.Services;
using XFEEDToolBox.ViewModels;
using XFEEDToolBox.Views;
using XFEEDToolBox.Services;
namespace XFEEDToolBox;
public partial class App : Application
{
public static MainWindow MainWindow { get; private set; } = null!;
public static ToolboxRuntime Runtime { get; private set; } = null!;
public static ToolboxViewModel ViewModel { get; private set; } = null!;
public static readonly Type[] Pages = [typeof(MainPage),typeof(ProfilePage), typeof(GalaxyMapPage), typeof(NavigationPage),typeof(CurrentSystemPage), typeof(MarketPage), typeof(MaterialsPage), typeof(RefitPage), typeof(RefitShipDetailsPage), typeof(RefitModulePickerPage), typeof(ShipPage), typeof(EngineeringPage), typeof(UpgradeListPage), typeof(PlannerPage), typeof(ExecutionPage), typeof(TranslationPage), typeof(ReadPage), typeof(SettingPage), typeof(LogViewPage)];
public App()
{
InitializeComponent();
foreach (var page in Pages) PageManager.RegisterPage(page);
UnhandledException += (_, e) =>
{
Runtime?.Log("界面异常", e.Exception?.ToString()??e.Message);
ViewModel?.ShowError(e.Message);
e.Handled = true;
};
}
protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
var commandLine = Environment.GetCommandLineArgs();
var offlinePreview=commandLine.Contains("--offline-preview");
var marketProbe=GetArgument(commandLine,"--smoke-market") is not null||GetArgument(commandLine,"--check-market-cache") is not null;
if(offlinePreview&&GetArgument(commandLine,"--data-dir") is null)throw new ArgumentException("Offline preview requires an isolated --data-dir.");
var directory = GetArgument(commandLine, "--data-dir") ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "XFEstudio", "XFEEDToolBox");
Runtime = new(directory);
MainWindow = new();
if(GetArgument(commandLine,"--notify-announcement") is {} announcementPath)
{await SendAnnouncementOnceAsync(announcementPath);return;}
ViewModel = new(Runtime, MainWindow.DispatcherQueue,offlinePreview||marketProbe);
var hiddenProbe=GetArgument(commandLine,"--check-background-data") is not null||GetArgument(commandLine,"--smoke-notifications") is not null||GetArgument(commandLine,"--smoke-current-system") is not null||GetArgument(commandLine,"--smoke-interface") is not null||GetArgument(commandLine,"--smoke-ship-status") is not null||GetArgument(commandLine,"--smoke-refinement") is not null||GetArgument(commandLine,"--smoke-skybox") is not null||GetArgument(commandLine,"--smoke-ui") is not null||GetArgument(commandLine,"--smoke-layout") is not null||GetArgument(commandLine,"--smoke-models") is not null||GetArgument(commandLine,"--smoke-map") is not null||GetArgument(commandLine,"--accept-navigation") is not null||GetArgument(commandLine,"--accept-game-route") is not null||GetArgument(commandLine,"--check-navigation") is not null||GetArgument(commandLine,"--smoke-workspace") is not null;
hiddenProbe|=GetArgument(commandLine,"--smoke-game-icons") is not null;
hiddenProbe|=GetArgument(commandLine,"--smoke-relog") is not null;
hiddenProbe|=GetArgument(commandLine,"--smoke-loading-hologram") is not null;
hiddenProbe|=marketProbe;
hiddenProbe|=GetArgument(commandLine,"--check-system-read") is not null;
hiddenProbe|=GetArgument(commandLine,"--accept-sco") is not null;
ViewModel.InitializeHotkeys((!hiddenProbe||GetArgument(commandLine,"--accept-sco") is not null)&&!offlinePreview&&!commandLine.Contains("--no-session-automation"));
MainWindow.Content = new AppShellPage();
InitializeArrivalNotifications(!hiddenProbe&&!offlinePreview);
if (commandLine.Contains("--maximized")) ((Microsoft.UI.Windowing.OverlappedPresenter)MainWindow.AppWindow.Presenter).Maximize();
var closing=false;
MainWindow.Closed += (_, _) =>
{
closing=true;
StopArrivalNotifications();ViewModel.Dispose();
// A final-window async-void handler may be abandoned when the host
// exits. Complete background writers and their cache seals before
// returning; dispose runs without the closing UI context.
Task.Run(async()=>await Runtime.DisposeAsync()).GetAwaiter().GetResult();
};
if (!hiddenProbe&&!commandLine.Contains("--background-start"))
{if(commandLine.Contains("--show-inactive"))MainWindow.AppWindow.Show(false);else MainWindow.Activate();}
else MainWindow.AppWindow.Hide();
try
{
await Runtime.InitializeAsync(offline:offlinePreview||marketProbe,validateMarketCache:GetArgument(commandLine,"--check-market-cache") is not null);
if(closing)return;
if(GetArgument(commandLine,"--check-market-cache") is {} cacheReport){await RunMarketCacheCheckAsync(cacheReport);return;}
if(GetArgument(commandLine,"--check-background-data") is {} backgroundReport){await RunBackgroundDataProbeAsync(backgroundReport);return;}
await ViewModel.InitializeAsync();
if(closing)return;
if(GetArgument(commandLine,"--upgrade-native-only") is {} maintenance)
{await ViewModel.UpgradeNativeSupportOnlyAsync(maintenance,GetArgument(commandLine,"--maintenance-report")??throw new ArgumentException("Maintenance report path required."));return;}
if(GetArgument(commandLine,"--accept-sco") is {} scoReport){await RunScoAcceptanceAsync(commandLine,scoReport);return;}
if(GetArgument(commandLine,"--update-and-automatic-trade") is {} updateTrade){AppShellPage.Current!.Navigate(typeof(PlannerPage));await ViewModel.UpdateAndRunAutomaticTradeAsync(updateTrade);return;}
if(GetArgument(commandLine,"--automatic-trade") is {} automaticTrade){AppShellPage.Current!.Navigate(typeof(PlannerPage));await ViewModel.RunAutomaticTradeAsync(automaticTrade);return;}
if(GetArgument(commandLine,"--station-journey") is {} journey)
{
var destination=System.Text.Json.JsonSerializer.Deserialize<XFEEDToolBox.Core.Models.Station>(await File.ReadAllTextAsync(journey),XFEEDToolBox.Core.Models.ToolboxSettings.JsonOptions)
??throw new ArgumentException("Station journey requires a destination station.");
AppShellPage.Current!.Navigate(typeof(NavigationPage));await ViewModel.RunStationJourneyAsync(destination);return;
}
if(GetArgument(commandLine,"--check-system-read") is {} systemReadReport){await RunSystemReadCheckAsync(systemReadReport);return;}
if(GetArgument(commandLine,"--smoke-market") is {} marketReport){await RunMarketProbeAsync(marketReport);return;}
if(GetArgument(commandLine,"--smoke-relog") is {} relogReport){await RunRelogProbeAsync(relogReport);return;}
if(GetArgument(commandLine,"--smoke-loading-hologram") is {} hologramReport){await RunLoadingHologramProbeAsync(hologramReport,GetArgument(commandLine,"--game-executable")??throw new ArgumentException("Original game executable is required."));return;}
if(GetArgument(commandLine,"--smoke-game-icons") is {} iconReport){await RunGameIconProbeAsync(iconReport);return;}
if(GetArgument(commandLine,"--smoke-notifications") is {} notificationReport){await RunArrivalNotificationProbeAsync(notificationReport);return;}
if(GetArgument(commandLine,"--smoke-current-system") is {} systemReport){await RunCurrentSystemProbeAsync(systemReport);return;}
if(GetArgument(commandLine,"--smoke-interface") is {} interfaceReport){await RunInterfaceOnlyProbeAsync(interfaceReport);return;}
if(GetArgument(commandLine,"--smoke-ship-status") is {} shipStatusReport){await RunShipStatusProbeAsync(shipStatusReport);return;}
if(GetArgument(commandLine,"--smoke-refinement") is {} refinementReport){await RunRefinementProbeAsync(refinementReport);return;}
if(GetArgument(commandLine,"--smoke-skybox") is {} skyboxReport){await RunSkyboxProbeAsync(commandLine,skyboxReport);return;}
if(GetArgument(commandLine,"--smoke-workspace") is {} workspaceReport){await RunWorkspaceProbeAsync(workspaceReport);return;}
if(GetArgument(commandLine,"--check-navigation") is {} navigationCheck){await RunNavigationCheckAsync(commandLine,navigationCheck);return;}
if(GetArgument(commandLine,"--accept-game-route") is {} routeReport){await RunGameRouteAcceptanceAsync(commandLine,routeReport);return;}
if(GetArgument(commandLine,"--accept-navigation") is {} navigationReport){await RunNavigationAcceptanceAsync(commandLine,navigationReport);return;}
if(!hiddenProbe&&GetArgument(commandLine,"--page") is {} requestedPage)
{
var page=Pages.FirstOrDefault(p=>p.Name.Equals(requestedPage,StringComparison.OrdinalIgnoreCase)||p.Name.Equals(requestedPage+"Page",StringComparison.OrdinalIgnoreCase));
if(page is not null)AppShellPage.Current!.Navigate(page);
}
if(GetArgument(commandLine,"--smoke-layout") is {} layoutReport){await RunLayoutProbeAsync(layoutReport);return;}
if(GetArgument(commandLine,"--smoke-models") is {} modelReport){await RunModelProbeAsync(modelReport);return;}
if(GetArgument(commandLine,"--smoke-map") is {} mapReport){await RunMapPerformanceProbeAsync(mapReport);return;}
if (GetArgument(commandLine, "--smoke-ui") is { } report) await RunUiSmokeAsync(report);
if(GetArgument(commandLine,"--market-report") is {} marketRuntimeReport)_=WriteMarketRuntimeReportAsync(marketRuntimeReport);
}
catch (Exception e) { ViewModel.ShowError(e.Message); Runtime.Log("启动错误", e.ToString()); }
}
private static string? GetArgument(string[] args, string name)
{
var index = Array.IndexOf(args, name);
return index >= 0 && index + 1 < args.Length ? args[index + 1] : null;
}
private static async Task RunUiSmokeAsync(string reportPath)
{
var results = new List<object>();
ViewModel.DemoMode = false;
await ViewModel.RefreshCommand.ExecuteAsync(null);
await ViewModel.ExportReadReportCommand.ExecuteAsync(null);
var actualModules = ViewModel.FittedModules.Count;
var actualLocalization=new{untranslatedModules=Runtime.Player.Ships.SelectMany(s=>s.Modules).Where(m=>m.DisplayName.Contains("未收录")||m.SlotName.Contains("未收录")).Select(m=>new{m.Slot,m.Item}).ToArray(),untranslatedModifiers=Runtime.Player.Ships.SelectMany(s=>s.Modules).SelectMany(m=>m.Engineering?.Modifiers??[]).Where(m=>m.Display.Contains("未收录")).Select(m=>m.Label).Distinct().ToArray(),localizedEffectsWithEnglish=Runtime.Player.Ships.SelectMany(s=>s.Modules).Where(m=>m.Engineering?.ExperimentalEffectId.Length>0).All(m=>!Core.Localization.ChineseNames.IsChinese(m.EngineeringOriginal))};
var hotkeyProbe = await WindowsGlobalHotkeys.ProbeAsync(WinRT.Interop.WindowNative.GetWindowHandle(MainWindow));
var existing=ViewModel.EngineeringSources.FirstOrDefault(s=>s.Module?.Engineering is not null);
if(existing is not null)ViewModel.EngineeringSource=existing;
await ViewModel.CalculateEngineeringCommand.ExecuteAsync(null);
var engineering = new { engineers = Runtime.Player.Engineers.Count, verifiedRolls = Runtime.Player.VerifiedEngineeringRolls, mismatchedRolls = Runtime.Player.MismatchedEngineeringRolls,
materials = ViewModel.MaterialStocks.Count, materialTotal = Runtime.Player.Materials.Sum(x => x.Count), modules = ViewModel.EngineeringModules.Count,
module = ViewModel.EngineeringModule?.Slot, blueprint = ViewModel.EngineeringBlueprint, result = ViewModel.EngineeringResult, rolls = ViewModel.EngineeringRolls.Count, ingredients = ViewModel.EngineeringNeeds.Count };
var sourceModule = ViewModel.RefitSlot?.Module;
var sourceSlot = ViewModel.RefitSlot?.Slot;
if (sourceModule is not null && ViewModel.RefitEngineer is not null && ViewModel.RefitBlueprint is not null)
{
ViewModel.RefitEditEnabled = true; ViewModel.RefitGrade = 5; ViewModel.RefitQuality = 100;
var other = ViewModel.RefitSlots.FirstOrDefault(s => s.Slot != sourceSlot && s.Module is not null && Core.Engineering.EngineeringCatalog.ModuleType(s.Module.Item) == Core.Engineering.EngineeringCatalog.ModuleType(sourceModule.Item));
if (other is not null) { ViewModel.RefitSlot = ViewModel.RefitVisibleSlots.First(s=>s.Slot==other.Slot); ViewModel.RefitEditEnabled = true; ViewModel.RefitGrade = 5; }
await ViewModel.CalculateRefitCommand.ExecuteAsync(null);
}
var refit = new { source = ViewModel.RefitSourceShip?.Type, slots = ViewModel.RefitSlots.Count, jobs = ViewModel.RefitModuleResults.Count, ingredients = ViewModel.RefitNeeds.Count,
targets = ViewModel.RefitDocument?.Slots.Where(s => s.Target is not null).Select(s => new { s.Slot, s.Target }),
result = ViewModel.RefitResult, sourceUnchanged = sourceModule is null || Runtime.Player.Ships.FirstOrDefault(s => s.ShipId == ViewModel.RefitSourceShip?.ShipId)?.Modules.FirstOrDefault(m => m.Slot == sourceSlot) == sourceModule,
gameEditsInvoked = false };
var plugin = new { status = ViewModel.PluginStatusText, installed = ViewModel.PluginInstalled, tradeReady = ViewModel.PluginTradeReady, installationInvoked = false };
await ViewModel.ExportRefitCommand.ExecuteAsync(null);
var refitExport=Directory.Exists(Path.Combine(Runtime.DataDirectory,"refits"))
? Directory.GetFiles(Path.Combine(Runtime.DataDirectory,"refits"),"refit-*.json").OrderByDescending(File.GetLastWriteTimeUtc).FirstOrDefault() : null;
var acceptanceReport = ViewModel.ReadReportPath;
var refitWorkbench = await RunRefitWorkbenchProbeAsync(reportPath);
var incremental = await RunIncrementalProbeAsync();
var navigation=await RunNavigationProbeAsync();
var upgradeList=await RunUpgradeListProbeAsync();
var galaxyProbe=await RunGalaxyProbeAsync();
foreach (var page in Pages)
{
AppShellPage.Current!.Navigate(page);
await Task.Delay(250);
var root = AppShellPage.Current.CurrentPage;
results.Add(new { page = page.Name, loaded = root?.IsLoaded == true, elements = CountVisuals(root) });
foreach (var pivot in Descendants(root).OfType<Pivot>())
for (var index = 0; index < pivot.Items.Count; index++) { pivot.SelectedIndex = index; await Task.Delay(150); }
}
ViewModel.DemoMode = true;
await ViewModel.RefreshCommand.ExecuteAsync(null);
ViewModel.UsePlayerValuesCommand.Execute(null);
await ViewModel.CalculateCommand.ExecuteAsync(null);
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(reportPath))!);
await File.WriteAllTextAsync(reportPath, JsonSerializer.Serialize(new { pages = results, actualModules, acceptanceReport, engineering, refit, refitExport, refitWorkbench, plugin, incremental, navigation, upgradeList, actualLocalization, galaxyProbe, galaxy=new{systems=ViewModel.GalaxySystems.Count,route=ViewModel.GalaxyRoute.Count,centre=ViewModel.GalaxyCentre,generatedStars=false}, profile = new { avatarPresent=ViewModel.ProfileAvatar is not null, namePresent=ViewModel.ProfileDisplayName.Length>0 }, translation=ViewModel.TranslationStatusText, planLegs = ViewModel.Plan?.Legs.Count, error = ViewModel.ErrorMessage, hotkeyStatus = ViewModel.HotkeyStatus, hotkeyProbe, gameSessionActionsInvoked = false }, new JsonSerializerOptions { WriteIndented = true }));
MainWindow.Close();
}
private static async Task<object> RunNavigationProbeAsync()
{
var route=Runtime.Player.Navigation.Route;
if(route.Count>0)ViewModel.NavigationDestination=route[^1].Name;
AppShellPage.Current!.Navigate(typeof(NavigationPage));await Task.Delay(250);
var loaded=AppShellPage.Current.CurrentPage?.IsLoaded==true;
var start=Descendants(AppShellPage.Current.CurrentPage).OfType<Button>().FirstOrDefault(b=>b.Content as string=="开始自动导航");
var enabled=start?.IsEnabled==true;
var before=ViewModel.NavigationLegs.Select(r=>r.Record).ToArray();var changed=0;
var sample=new Core.Navigation.NavigationLegPreview(1,1,"Isolated UI fixture","K",10,true,false,true,2);
ViewModel.NavigationLegs.Apply([sample]);await Task.Delay(120);
System.Collections.Specialized.NotifyCollectionChangedEventHandler handler=(_,_)=>changed++;
ViewModel.NavigationLegs.CollectionChanged+=handler;
ViewModel.NavigationLegs.Apply([sample with{Completed=true,Next=false}]);await Task.Delay(100);
var updated=Descendants(AppShellPage.Current.CurrentPage).OfType<TextBlock>().Any(t=>t.Text=="已抵达");
ViewModel.NavigationLegs.CollectionChanged-=handler;ViewModel.NavigationLegs.Apply(before);
return new{pageLoaded=loaded,actualRouteWaypoints=route.Count,previewRows=before.Length,startRespondsWithPreflight=enabled,readinessHint=ViewModel.NavigationStartHint,incrementalStateBound=updated,collectionEvents=changed,gameFlightCommandsInvoked=false};
}
private static async Task<object> RunIncrementalProbeAsync()
{
AppShellPage.Current!.Navigate(typeof(MaterialsPage));await Task.Delay(250);
var materials=ViewModel.MaterialStocks.Select(r=>r.Record).ToArray();var materialRow=ViewModel.MaterialStocks.FirstOrDefault();
var materialEvents=0;var materialProperties=new List<string>();var materialBound=false;
if(materialRow is not null)
{
System.Collections.Specialized.NotifyCollectionChangedEventHandler collection=(_,_)=>materialEvents++;
System.ComponentModel.PropertyChangedEventHandler property=(_,e)=>materialProperties.Add(e.PropertyName??"");
ViewModel.MaterialStocks.CollectionChanged+=collection;materialRow.PropertyChanged+=property;
var text=Descendants(AppShellPage.Current.CurrentPage).OfType<TextBlock>().FirstOrDefault(t=>ReferenceEquals(t.DataContext,materialRow)&&t.Tag as string=="material-quantity");
ViewModel.MaterialStocks.Apply(materials.Select(m=>m.Key==materialRow.Key?m with{Owned=m.Owned+1}:m));
await Task.Delay(80);materialBound=text?.Text==materialRow.Quantity;
ViewModel.MaterialStocks.CollectionChanged-=collection;materialRow.PropertyChanged-=property;
ViewModel.MaterialStocks.Apply(materials);
}
AppShellPage.Current.Navigate(typeof(MarketPage));await Task.Delay(250);
var markets=ViewModel.MarketRows.Select(r=>r.Record).ToArray();var marketRow=ViewModel.MarketRows.FirstOrDefault();var marketEvents=0;var marketProperties=new List<string>();var marketBound=false;
if(marketRow is not null)
{
System.Collections.Specialized.NotifyCollectionChangedEventHandler collection=(_,_)=>marketEvents++;
System.ComponentModel.PropertyChangedEventHandler property=(_,e)=>marketProperties.Add(e.PropertyName??"");
ViewModel.MarketRows.CollectionChanged+=collection;marketRow.PropertyChanged+=property;
var text=Descendants(AppShellPage.Current.CurrentPage).OfType<TextBlock>().FirstOrDefault(t=>ReferenceEquals(t.DataContext,marketRow)&&Grid.GetColumn(t)==2);
ViewModel.MarketRows.Apply(markets.Select((m,i)=>i==0?m with{Quote=m.Quote with{BuyPrice=m.Quote.BuyPrice+1}}:m));
await Task.Delay(80);marketBound=text?.Text==marketRow.Buy;
ViewModel.MarketRows.CollectionChanged-=collection;marketRow.PropertyChanged-=property;
ViewModel.MarketRows.Apply(markets);
}
return new{materialCollectionEvents=materialEvents,materialProperties,materialTextUpdated=materialBound,marketCollectionEvents=marketEvents,marketProperties,marketTextUpdated=marketBound};
}
private static int CountVisuals(DependencyObject? node)
{
if (node is null) return 0;
var count = 1;
for (var i = 0; i < Microsoft.UI.Xaml.Media.VisualTreeHelper.GetChildrenCount(node); i++) count += CountVisuals(Microsoft.UI.Xaml.Media.VisualTreeHelper.GetChild(node, i));
return count;
}
private static IEnumerable<DependencyObject> Descendants(DependencyObject? node)
{
if (node is null) yield break;
yield return node;
for (var i = 0; i < Microsoft.UI.Xaml.Media.VisualTreeHelper.GetChildrenCount(node); i++)
foreach (var child in Descendants(Microsoft.UI.Xaml.Media.VisualTreeHelper.GetChild(node, i))) yield return child;
}
}