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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

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