diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b43ddf6..8c2061c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,7 +8,7 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "daily" + interval: "weekly" groups: actions-deps: patterns: @@ -19,7 +19,7 @@ updates: - package-ecosystem: "nuget" directory: "/" schedule: - interval: "daily" + interval: "weekly" target-branch: "develop" open-pull-requests-limit: 1 groups: diff --git a/.gitignore b/.gitignore index 8a30d25..7d20657 100644 --- a/.gitignore +++ b/.gitignore @@ -396,3 +396,6 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml +.idea + +.local_deploy \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index d475e7f..5110cdb 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -12,7 +12,7 @@ ModVerify Alamo Engine Tools and Contributors - Copyright © 2025 Alamo Engine Tools and contributors. All rights reserved. + Copyright © 2026 Alamo Engine Tools and contributors. All rights reserved. https://github.com/AlamoEngine-Tools/ModVerify $(RepoRootPath)LICENSE MIT @@ -33,7 +33,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/LICENSE b/LICENSE index 7159126..23fe869 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Alamo Engine Tools +Copyright (c) 2026 Alamo Engine Tools Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/deploy-local.ps1 b/deploy-local.ps1 new file mode 100644 index 0000000..740c579 --- /dev/null +++ b/deploy-local.ps1 @@ -0,0 +1,75 @@ +# Local deployment script for ModVerify to test the update feature. +# This script builds the application, creates an update manifest, and "deploys" it to a local directory. + +$ErrorActionPreference = "Stop" + +$root = $PSScriptRoot +if ([string]::IsNullOrEmpty($root)) { $root = Get-Location } + +$deployRoot = Join-Path $root ".local_deploy" +$stagingDir = Join-Path $deployRoot "staging" +$serverDir = Join-Path $deployRoot "server" +$installDir = Join-Path $deployRoot "install" + +$toolProj = Join-Path $root "src\ModVerify.CliApp\ModVerify.CliApp.csproj" +$creatorProj = Join-Path $root "modules\ModdingToolBase\src\AnakinApps\ApplicationManifestCreator\ApplicationManifestCreator.csproj" +$uploaderProj = Join-Path $root "modules\ModdingToolBase\src\AnakinApps\FtpUploader\FtpUploader.csproj" + +$toolExe = "ModVerify.exe" +$updaterExe = "AnakinRaW.ExternalUpdater.exe" +$manifestCreatorDll = "AnakinRaW.ApplicationManifestCreator.dll" +$uploaderDll = "AnakinRaW.FtpUploader.dll" + +# 1. Clean and Create directories +if (Test-Path $deployRoot) { Remove-Item -Recurse -Force $deployRoot } +New-Item -ItemType Directory -Path $stagingDir | Out-Null +New-Item -ItemType Directory -Path $serverDir | Out-Null +New-Item -ItemType Directory -Path $installDir | Out-Null + +Write-Host "--- Building ModVerify (net481) ---" -ForegroundColor Cyan +dotnet build $toolProj --configuration Release -f net481 --output "$deployRoot\bin\tool" /p:DebugType=None /p:DebugSymbols=false + +Write-Host "--- Building Manifest Creator ---" -ForegroundColor Cyan +dotnet build $creatorProj --configuration Release --output "$deployRoot\bin\creator" + +Write-Host "--- Building Local Uploader ---" -ForegroundColor Cyan +dotnet build $uploaderProj --configuration Release --output "$deployRoot\bin\uploader" + +# 2. Prepare staging +Write-Host "--- Preparing Staging ---" -ForegroundColor Cyan +Copy-Item "$deployRoot\bin\tool\$toolExe" $stagingDir +Copy-Item "$deployRoot\bin\tool\$updaterExe" $stagingDir + +# 3. Create Manifest +# Origin must be an absolute URI for the manifest creator. +# Using 127.0.0.1 and file:// is tricky with Flurl/DownloadManager sometimes. +# We'll use the local path and ensure it's formatted correctly. +$serverPath = (Resolve-Path $serverDir).Path +$serverUri = "file:///$($serverPath.Replace('\', '/'))" +# If we have 3 slashes, Flurl/DownloadManager might still fail on Windows if it expects a certain format. +# However, the ManifestCreator just needs a valid URI for the 'Origin' field in the manifest. +Write-Host "--- Creating Manifest (Origin: $serverUri) ---" -ForegroundColor Cyan +dotnet "$deployRoot\bin\creator\$manifestCreatorDll" ` + -a "$stagingDir\$toolExe" ` + --appDataFiles "$stagingDir\$updaterExe" ` + --origin "$serverUri" ` + -o "$stagingDir" ` + -b "beta" + +# 4. "Deploy" to server using the local uploader +Write-Host "--- Deploying to Local Server ---" -ForegroundColor Cyan +dotnet "$deployRoot\bin\uploader\$uploaderDll" local --base "$serverDir" --source "$stagingDir" + +# 5. Setup a "test" installation +Write-Host "--- Setting up Test Installation ---" -ForegroundColor Cyan +Copy-Item "$deployRoot\bin\tool\*" $installDir -Recurse + +Write-Host "`nLocal deployment complete!" -ForegroundColor Green +Write-Host "Server directory: $serverDir" +Write-Host "Install directory: $installDir" +Write-Host "`nTo test the update:" +Write-Host "1. (Optional) Modify the version in version.json and run this script again to 'push' a new version to the local server." +Write-Host "2. Run ModVerify from the install directory with the following command:" +Write-Host " cd '$installDir'" +Write-Host " .\ModVerify.exe updateApplication --updateManifestUrl '$serverUri'" +Write-Host "`n Note: You can also specify a different branch using --updateBranch if needed." diff --git a/modules/ModdingToolBase b/modules/ModdingToolBase index 479a088..18cfb7a 160000 --- a/modules/ModdingToolBase +++ b/modules/ModdingToolBase @@ -1 +1 @@ -Subproject commit 479a088a2b26dd4a3e2342b2e34f5359b0252e88 +Subproject commit 18cfb7a31a091ebc57708ca394b0d2e406343b31 diff --git a/src/ModVerify.CliApp/App/CreateBaselineAction.cs b/src/ModVerify.CliApp/App/CreateBaselineAction.cs new file mode 100644 index 0000000..b0eb880 --- /dev/null +++ b/src/ModVerify.CliApp/App/CreateBaselineAction.cs @@ -0,0 +1,56 @@ +using AET.ModVerify.App.Reporting; +using AET.ModVerify.App.Settings; +using AET.ModVerify.App.Utilities; +using AET.ModVerify.Reporting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Threading.Tasks; + +namespace AET.ModVerify.App; + +internal sealed class CreateBaselineAction(AppBaselineSettings settings, IServiceProvider serviceProvider) + : ModVerifyApplicationAction(settings, serviceProvider) +{ + private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); + + protected override void PrintAction(VerificationTarget target) + { + Console.WriteLine(); + Console.ForegroundColor = ConsoleColor.DarkGreen; + Console.WriteLine($"Creating baseline for {target.Name}..."); + Console.WriteLine(); + ModVerifyConsoleUtilities.WriteSelectedTarget(target); + Console.WriteLine(); + } + + protected override async Task ProcessVerifyFindings( + VerificationTarget verificationTarget, + IReadOnlyCollection allErrors) + { + var baselineFactory = ServiceProvider.GetRequiredService(); + var baseline = baselineFactory.CreateBaseline(verificationTarget, Settings, allErrors); + + var fullPath = _fileSystem.Path.GetFullPath(Settings.NewBaselinePath); + Logger?.LogInformation(ModVerifyConstants.ConsoleEventId, + "Writing Baseline to '{FullPath}' with {Number} findings", fullPath, allErrors.Count); + + await baselineFactory.WriteBaselineAsync(baseline, Settings.NewBaselinePath); + + Logger?.LogDebug("Baseline successfully created."); + + Console.WriteLine(); + Console.ForegroundColor = ConsoleColor.DarkGreen; + Console.WriteLine($"Baseline for {verificationTarget.Name} created."); + Console.ResetColor(); + + return ModVerifyConstants.Success; + } + + protected override VerificationBaseline GetBaseline(VerificationTarget verificationTarget) + { + return VerificationBaseline.Empty; + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/App/IModVerifyAppAction.cs b/src/ModVerify.CliApp/App/IModVerifyAppAction.cs new file mode 100644 index 0000000..1a75b84 --- /dev/null +++ b/src/ModVerify.CliApp/App/IModVerifyAppAction.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace AET.ModVerify.App; + +internal interface IModVerifyAppAction +{ + Task ExecuteAsync(); +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/App/ModVerifyApplication.cs b/src/ModVerify.CliApp/App/ModVerifyApplication.cs new file mode 100644 index 0000000..eae7851 --- /dev/null +++ b/src/ModVerify.CliApp/App/ModVerifyApplication.cs @@ -0,0 +1,68 @@ +using AET.ModVerify.App.Settings; +using AnakinRaW.ApplicationBase; +using AnakinRaW.ApplicationBase.Utilities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Serilog; +using System; +using System.Threading.Tasks; +using ILogger = Microsoft.Extensions.Logging.ILogger; + +namespace AET.ModVerify.App; + +internal sealed class ModVerifyApplication(AppSettingsBase settings, IServiceProvider services) +{ + private readonly ILogger? _logger = services.GetService()?.CreateLogger(typeof(ModVerifyApplication)); + + public async Task RunAsync() + { + using (new UnhandledExceptionHandler(services)) + using (new UnobservedTaskExceptionHandler(services)) + return await RunCoreAsync().ConfigureAwait(false); + } + + private async Task RunCoreAsync() + { + _logger?.LogDebug("Raw command line: {CommandLine}", Environment.CommandLine); + + try + { + var action = CreateAppAction(); + return await action.ExecuteAsync().ConfigureAwait(false); + } + catch (Exception e) + { + _logger?.LogCritical(e, e.Message); + ConsoleUtilities.WriteApplicationFatalError(ModVerifyConstants.AppNameString, e); + return e.HResult; + } + finally + { +#if NET + await Log.CloseAndFlushAsync(); +#else + Log.CloseAndFlush(); +#endif + if (settings is AppVerifySettings { IsInteractive: true }) + { + Console.WriteLine(); + ConsoleUtilities.WriteHorizontalLine('-'); + Console.WriteLine("Press any key to exit"); + Console.ReadLine(); + } + } + } + + private IModVerifyAppAction CreateAppAction() + { + switch (settings) + { + case AppVerifySettings verifySettings: + return new VerifyAction(verifySettings, services); + case AppBaselineSettings baselineSettings: + return new CreateBaselineAction(baselineSettings, services); + default: + throw new InvalidOperationException("Unknown settings"); + } + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/App/ModVerifyApplicationAction.cs b/src/ModVerify.CliApp/App/ModVerifyApplicationAction.cs new file mode 100644 index 0000000..e10bfde --- /dev/null +++ b/src/ModVerify.CliApp/App/ModVerifyApplicationAction.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Threading.Tasks; +using AET.ModVerify.App.GameFinder; +using AET.ModVerify.App.Reporting; +using AET.ModVerify.App.Settings; +using AET.ModVerify.App.TargetSelectors; +using AET.ModVerify.Pipeline; +using AET.ModVerify.Reporting; +using AnakinRaW.ApplicationBase; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace AET.ModVerify.App; + +internal abstract class ModVerifyApplicationAction : IModVerifyAppAction where T : AppSettingsBase +{ + private readonly ModVerifyAppEnvironment _appEnvironment; + private readonly IFileSystem _fileSystem; + + protected T Settings { get; } + + protected IServiceProvider ServiceProvider { get; } + + protected ILogger? Logger { get; } + + protected ModVerifyApplicationAction(T settings, IServiceProvider serviceProvider) + { + Settings = settings ?? throw new ArgumentNullException(nameof(settings)); + ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + Logger = serviceProvider.GetService()?.CreateLogger(GetType()); + _appEnvironment = ServiceProvider.GetRequiredService(); + _fileSystem = ServiceProvider.GetRequiredService(); + } + + protected virtual void PrintAction(VerificationTarget target) + { + } + + public async Task ExecuteAsync() + { + VerificationTarget verificationTarget; + try + { + var targetSettings = Settings.VerificationTargetSettings; + verificationTarget = new VerificationTargetSelectorFactory(ServiceProvider) + .CreateSelector(targetSettings) + .Select(targetSettings); + } + catch (ArgumentException ex) + { + ConsoleUtilities.WriteApplicationFatalError(_appEnvironment.ApplicationName, + $"The specified arguments are not correct: {ex.Message}"); + Logger?.LogError(ex, "Invalid application arguments: {Message}", ex.Message); + return ex.HResult; + } + catch (TargetNotFoundException ex) + { + ConsoleUtilities.WriteApplicationFatalError(_appEnvironment.ApplicationName, ex.Message); + Logger?.LogError(ex, ex.Message); + return ex.HResult; + } + catch (GameNotFoundException ex) + { + ConsoleUtilities.WriteApplicationFatalError(_appEnvironment.ApplicationName, + "Unable to find an installation of Empire at War or Forces of Corruption."); + Logger?.LogError(ex, "Game not found: {Message}", ex.Message); + return ex.HResult; + } + + PrintAction(verificationTarget); + + var allErrors = await VerifyTargetAsync(verificationTarget) + .ConfigureAwait(false); + + return await ProcessVerifyFindings(verificationTarget, allErrors); + } + + protected abstract Task ProcessVerifyFindings( + VerificationTarget verificationTarget, + IReadOnlyCollection allErrors); + + protected abstract VerificationBaseline GetBaseline(VerificationTarget verificationTarget); + + private async Task> VerifyTargetAsync(VerificationTarget verificationTarget) + { + var progressReporter = new VerifyConsoleProgressReporter(verificationTarget.Name, Settings.ReportSettings); + + var baseline = GetBaseline(verificationTarget); + var suppressions = GetSuppressions(); + + using var verifyPipeline = new GameVerifyPipeline( + verificationTarget, + Settings.VerifyPipelineSettings, + progressReporter, + new EngineInitializeProgressReporter(verificationTarget.Engine), + baseline, + suppressions, + ServiceProvider); + + try + { + Logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Verifying '{Target}'...", verificationTarget.Name); + await verifyPipeline.RunAsync().ConfigureAwait(false); + progressReporter.Report(string.Empty, 1.0); + } + catch (OperationCanceledException) + { + Logger?.LogWarning(ModVerifyConstants.ConsoleEventId, "Verification stopped due to enabled failFast setting."); + } + catch (Exception e) + { + progressReporter.ReportError("Verification failed!", e.Message); + Logger?.LogError(e, "Verification failed: {Message}", e.Message); + throw; + } + finally + { + progressReporter.Dispose(); + } + + Logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Finished verification"); + return verifyPipeline.FilteredErrors; + } + + private SuppressionList GetSuppressions() + { + var suppressionsFile = Settings.ReportSettings.SuppressionsPath; + SuppressionList suppressions; + if (string.IsNullOrEmpty(suppressionsFile)) + suppressions = SuppressionList.Empty; + else + { + using var fileStream = _fileSystem.File.OpenRead(suppressionsFile); + suppressions = SuppressionList.FromJson(fileStream); + if (suppressions.Count > 0) + Logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Using suppressions from '{Suppressions}'", suppressionsFile); + } + return suppressions; + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/App/VerifyAction.cs b/src/ModVerify.CliApp/App/VerifyAction.cs new file mode 100644 index 0000000..0cfea0e --- /dev/null +++ b/src/ModVerify.CliApp/App/VerifyAction.cs @@ -0,0 +1,60 @@ +using AET.ModVerify.App.Reporting; +using AET.ModVerify.App.Settings; +using AET.ModVerify.Reporting; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AET.ModVerify.App.Utilities; + +namespace AET.ModVerify.App; + +internal sealed class VerifyAction(AppVerifySettings settings, IServiceProvider services) + : ModVerifyApplicationAction(settings, services) +{ + protected override void PrintAction(VerificationTarget target) + { + Console.WriteLine(); + Console.ForegroundColor = ConsoleColor.DarkGreen; + Console.WriteLine($"Verifying {target.Name} for issues..."); + Console.WriteLine(); + ModVerifyConsoleUtilities.WriteSelectedTarget(target); + Console.WriteLine(); + } + + protected override async Task ProcessVerifyFindings( + VerificationTarget verificationTarget, + IReadOnlyCollection allErrors) + { + Logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Reporting Errors..."); + var reportBroker = new VerificationReportBroker(ServiceProvider); + await reportBroker.ReportAsync(allErrors); + + if (Settings.AppFailsOnMinimumSeverity.HasValue && + allErrors.Any(x => x.Severity >= Settings.AppFailsOnMinimumSeverity)) + { + Logger?.LogInformation(ModVerifyConstants.ConsoleEventId, + "The verification of {Target} completed with findings of the specified failure severity {Severity}", + verificationTarget.Name, Settings.AppFailsOnMinimumSeverity); + + return ModVerifyConstants.CompletedWithFindings; + } + + return ModVerifyConstants.Success; + } + + protected override VerificationBaseline GetBaseline(VerificationTarget verificationTarget) + { + var baselineSelector = new BaselineSelector(Settings, ServiceProvider); + var baseline = baselineSelector.SelectBaseline(verificationTarget, out var baselinePath); + if (!baseline.IsEmpty) + { + Console.WriteLine(); + ModVerifyConsoleUtilities.WriteBaselineInfo(baseline, baselinePath); + Logger?.LogDebug("Using baseline {Baseline} from location '{Path}'", baseline.ToString(), baselinePath); + Console.WriteLine(); + } + return baseline; + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/AppArgumentException.cs b/src/ModVerify.CliApp/AppArgumentException.cs new file mode 100644 index 0000000..8ead9ed --- /dev/null +++ b/src/ModVerify.CliApp/AppArgumentException.cs @@ -0,0 +1,5 @@ +using System; + +namespace AET.ModVerify.App; + +internal class AppArgumentException(string message) : ArgumentException(message); \ No newline at end of file diff --git a/src/ModVerify.CliApp/GameFinder/GameFinderService.cs b/src/ModVerify.CliApp/GameFinder/GameFinderService.cs index a88ebd9..0ac8346 100644 --- a/src/ModVerify.CliApp/GameFinder/GameFinderService.cs +++ b/src/ModVerify.CliApp/GameFinder/GameFinderService.cs @@ -1,11 +1,17 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO.Abstractions; +using System.Runtime.InteropServices; +using AET.ModVerify.App.Utilities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine; using PG.StarWarsGame.Infrastructure.Clients.Steam; using PG.StarWarsGame.Infrastructure.Games; +using PG.StarWarsGame.Infrastructure.Games.Registry; using PG.StarWarsGame.Infrastructure.Mods; using PG.StarWarsGame.Infrastructure.Services; using PG.StarWarsGame.Infrastructure.Services.Detection; @@ -29,7 +35,7 @@ public GameFinderService(IServiceProvider serviceProvider) _logger = _serviceProvider.GetService()?.CreateLogger(GetType()); } - public GameFinderResult FindGames() + public GameFinderResult FindGames(GameFinderSettings settings) { var detectors = new List { @@ -37,10 +43,47 @@ public GameFinderResult FindGames() new SteamPetroglyphStarWarsGameDetector(_serviceProvider), }; - return FindGames(detectors); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var registryFactory = _serviceProvider.GetRequiredService(); + detectors.Add(new RegistryGameDetector( + registryFactory.CreateRegistry(GameType.Eaw), + registryFactory.CreateRegistry(GameType.Foc), + false, _serviceProvider)); + } + + return FindGames(detectors, settings); + } + + public IGame FindGame(string gamePath, GameFinderSettings settings) + { + var detectors = new List + { + new DirectoryGameDetector(_fileSystem.DirectoryInfo.New(gamePath), _serviceProvider), + }; + return FindGames(detectors, settings).Game; } - public GameFinderResult FindGamesFromPathOrGlobal(string path) + public bool TryFindGame(string gamePath, GameFinderSettings settings, [NotNullWhen(true)]out IGame? game) + { + var detectors = new List + { + new DirectoryGameDetector(_fileSystem.DirectoryInfo.New(gamePath), _serviceProvider), + }; + + try + { + game = FindGames(detectors, settings).Game; + return true; + } + catch (GameNotFoundException) + { + game = null; + return false; + } + } + + public GameFinderResult FindGamesFromPathOrGlobal(string path, GameFinderSettings settings) { // There are four common situations: // 1. path points to the actual game directory @@ -49,87 +92,136 @@ public GameFinderResult FindGamesFromPathOrGlobal(string path) // 4. path points to a "detached mod" at a completely different location var givenDirectory = _fileSystem.DirectoryInfo.New(path); var possibleGameDir = givenDirectory.Parent?.Parent; + + + // We need to check the local paths first, before falling back to global detectors, + // to ensure that we always find the correct game installation, + // especially if the user did not request a specific engine. - var detectors = new List + var localDetectors = new List { - // Case 1 - new DirectoryGameDetector(givenDirectory, _serviceProvider) + new DirectoryGameDetector(givenDirectory, _serviceProvider), }; + if (possibleGameDir is not null) + localDetectors.Add(new DirectoryGameDetector(possibleGameDir, _serviceProvider)); - // Case 2 - if (possibleGameDir is not null) - detectors.Add(new DirectoryGameDetector(possibleGameDir, _serviceProvider)); + // Case 1 & 2 + if (TryFindGames(localDetectors, settings, out var finderResult)) + return finderResult; + + + // There is the rare scenario where the user specified a specific engine, + // but path points to a game with the opposite engine. + // This does not make sense and the global detectors can not handle this. + // Thus, we need to check against this. + if (settings.Engine is not null) + { + if (TryFindGame(path, new GameFinderSettings + { + Engine = settings.Engine.Value.Opposite(), + InitMods = false, + SearchFallbackGame = false + }, out _)) + { + var e = new GameNotFoundException( + $"The specified game engine '{settings.Engine.Value}' does not match engine of the specified path '{path}'."); + _logger?.LogTrace(e, e.Message); + throw e; + } + } // Cases 3 & 4 - detectors.Add(new SteamPetroglyphStarWarsGameDetector(_serviceProvider)); - return FindGames(detectors); + return FindGames(CreateGlobalDetectors(), settings); } - private bool TryDetectGame(GameType gameType, IList detectors, out GameDetectionResult result) + private bool TryFindGames(IList detectors, GameFinderSettings settings, + [NotNullWhen(true)] out GameFinderResult? finderResult) { - var gd = new CompositeGameDetector(detectors, _serviceProvider); - try { - result = gd.Detect(gameType); - if (result.GameLocation is null) - return false; + finderResult = FindGames(detectors, settings); return true; } - catch (Exception e) + catch (GameNotFoundException) { - result = GameDetectionResult.NotInstalled(gameType); - _logger?.LogTrace("Unable to find game installation: {Message}", e.Message); + finderResult = null; return false; } } - private GameFinderResult FindGames(IList detectors) + private GameFinderResult FindGames(IList detectors, GameFinderSettings settings) { - // FoC needs to be tried first - if (!TryDetectGame(GameType.Foc, detectors, out var result)) + GameDetectionResult? detectionResult = null; + if (settings.Engine is GameEngineType.Eaw) { + _logger?.LogTrace("Trying to find requested EaW installation."); + if (!TryDetectGame(GameType.Eaw, detectors, out detectionResult)) + { + var e = new GameNotFoundException($"Unable to find requested game installation '{settings.Engine}'. Wrong install path?"); + _logger?.LogTrace(e, e.Message); + throw e; + } + } + + if (detectionResult is null && !TryDetectGame(GameType.Foc, detectors, out detectionResult)) + { + if (settings.Engine is GameEngineType.Foc) + { + var e = new GameNotFoundException($"Unable to find requested game installation '{settings.Engine}'. Wrong install path?"); + _logger?.LogTrace(e, e.Message); + throw e; + } + + // If the engine is unspecified, we also need to check for EaW. _logger?.LogTrace("Unable to find FoC installation. Trying again with EaW..."); - if (!TryDetectGame(GameType.Eaw, detectors, out result)) + if (!TryDetectGame(GameType.Eaw, detectors, out detectionResult)) throw new GameNotFoundException("Unable to find game installation: Wrong install path?"); } - if (result.GameLocation is null) - throw new GameNotFoundException("Unable to find game installation: Wrong install path?"); + Debug.Assert(detectionResult.GameLocation is not null); - _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, - "Found game installation: {ResultGameIdentity} at {GameLocationFullName}", result.GameIdentity, result.GameLocation.FullName); + _logger?.LogDebug("Found game installation: {ResultGameIdentity} at {GameLocationFullName}", + detectionResult.GameIdentity, detectionResult.GameLocation!.FullName); - var game = _gameFactory.CreateGame(result, CultureInfo.InvariantCulture); - - SetupMods(game); + var game = _gameFactory.CreateGame(detectionResult, CultureInfo.InvariantCulture); + if (settings.InitMods) + SetupMods(game); IGame? fallbackGame = null; - // If the game is Foc we want to set up Eaw as well as the fallbackGame - if (game.Type == GameType.Foc) + if (SearchForFallbackGame(settings, detectionResult)) { - var fallbackDetectors = new List(); - - if (game.Platform == GamePlatform.SteamGold) - fallbackDetectors.Add(new SteamPetroglyphStarWarsGameDetector(_serviceProvider)); - else - throw new NotImplementedException("Searching fallback game for non-Steam games is currently is not yet implemented."); - - if (!TryDetectGame(GameType.Eaw, fallbackDetectors, out var fallbackResult) || fallbackResult.GameLocation is null) + if (!TryDetectGame(GameType.Eaw, CreateGlobalDetectors(), out var fallbackResult)) throw new GameNotFoundException("Unable to find fallback game installation: Wrong install path?"); - _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, - "Found fallback game installation: {FallbackResultGameIdentity} at {GameLocationFullName}", fallbackResult.GameIdentity, fallbackResult.GameLocation.FullName); + _logger?.LogDebug("Found fallback game installation: {FallbackResultGameIdentity} at {GameLocationFullName}", + fallbackResult.GameIdentity, fallbackResult.GameLocation!.FullName); fallbackGame = _gameFactory.CreateGame(fallbackResult, CultureInfo.InvariantCulture); - SetupMods(fallbackGame); + if (settings.InitMods) + SetupMods(fallbackGame); } return new GameFinderResult(game, fallbackGame); } + private static bool SearchForFallbackGame(GameFinderSettings settings, GameDetectionResult? foundGame) + { + if (settings.Engine is GameEngineType.Eaw) + return false; + if (foundGame is { Installed: true, GameIdentity.Type: GameType.Eaw }) + return false; + return settings.SearchFallbackGame; + } + + private bool TryDetectGame(GameType gameType, IList detectors, out GameDetectionResult result) + { + var gd = new CompositeGameDetector(detectors, _serviceProvider, true); + result = gd.Detect(gameType); + return result.GameLocation is not null; + } + private void SetupMods(IGame game) { var modFinder = _serviceProvider.GetRequiredService(); @@ -152,4 +244,21 @@ private void SetupMods(IGame game) mod.ResolveDependencies(); } } + + private IList CreateGlobalDetectors() + { + var detectors = new List + { + new SteamPetroglyphStarWarsGameDetector(_serviceProvider), + }; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var registryFactory = _serviceProvider.GetRequiredService(); + detectors.Add(new RegistryGameDetector( + registryFactory.CreateRegistry(GameType.Eaw), + registryFactory.CreateRegistry(GameType.Foc), + false, _serviceProvider)); + } + return detectors; + } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/GameFinder/GameFinderSettings.cs b/src/ModVerify.CliApp/GameFinder/GameFinderSettings.cs new file mode 100644 index 0000000..fc57b24 --- /dev/null +++ b/src/ModVerify.CliApp/GameFinder/GameFinderSettings.cs @@ -0,0 +1,14 @@ +using PG.StarWarsGame.Engine; + +namespace AET.ModVerify.App.GameFinder; + +internal sealed class GameFinderSettings +{ + internal static readonly GameFinderSettings Default = new(); + + public bool InitMods { get; init; } = true; + + public bool SearchFallbackGame { get; init; } = true; + + public GameEngineType? Engine { get; init; } = null; +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectors/AutomaticModSelector.cs b/src/ModVerify.CliApp/ModSelectors/AutomaticModSelector.cs deleted file mode 100644 index 717db7b..0000000 --- a/src/ModVerify.CliApp/ModSelectors/AutomaticModSelector.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; -using System.Globalization; -using System.IO.Abstractions; -using System.Linq; -using AET.ModVerify.App.GameFinder; -using AET.ModVerify.App.Settings; -using AET.ModVerify.App.Utilities; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Infrastructure; -using PG.StarWarsGame.Infrastructure.Games; -using PG.StarWarsGame.Infrastructure.Mods; -using PG.StarWarsGame.Infrastructure.Services; -using PG.StarWarsGame.Infrastructure.Services.Detection; - -namespace AET.ModVerify.App.ModSelectors; - -internal class AutomaticModSelector(IServiceProvider serviceProvider) : ModSelectorBase(serviceProvider) -{ - private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); - - public override GameLocations? Select( - GameInstallationsSettings settings, - out IPhysicalPlayableObject? targetObject, - out GameEngineType? actualEngineType) - { - var pathToVerify = settings.AutoPath; - if (pathToVerify is null) - throw new InvalidOperationException("path to verify cannot be null."); - - actualEngineType = settings.EngineType; - - GameFinderResult finderResult; - try - { - finderResult = GameFinderService.FindGamesFromPathOrGlobal(pathToVerify); - } - catch (GameNotFoundException) - { - Logger?.LogError(ModVerifyConstants.ConsoleEventId, "Unable to find games based of the given location '{SettingsGamePath}'. Consider specifying all paths manually.", settings.GamePath); - targetObject = null!; - return null; - } - - - var modOrGame = GetAttachedModOrGame(finderResult, actualEngineType, pathToVerify); - - if (modOrGame is not null) - { - var actualType = modOrGame.Game.Type.ToEngineType(); - actualEngineType ??= actualType; - if (actualEngineType != actualType) - throw new ArgumentException($"The specified game type '{actualEngineType}' does not match the actual type of the game or mod to verify."); - - targetObject = modOrGame; - return GetLocations(targetObject, finderResult, settings.AdditionalFallbackPaths); - } - - if (!settings.EngineType.HasValue) - throw new ArgumentException("Unable to determine game type. Use --type argument to set the game type."); - - Logger?.LogDebug("The requested mod at '{PathToVerify}' is detached from its games.", pathToVerify); - - // The path is a detached mod, that exists on a different location than the game. - var result = GetDetachedModLocations(pathToVerify, finderResult, settings, out var mod); - targetObject = mod; - return result; - } - - private IPhysicalPlayableObject? GetAttachedModOrGame(GameFinderResult finderResult, GameEngineType? requestedEngineType, string searchPath) - { - var fullSearchPath = _fileSystem.Path.GetFullPath(searchPath); - - if (finderResult.Game.Directory.FullName.Equals(fullSearchPath, StringComparison.OrdinalIgnoreCase)) - { - if (finderResult.Game.Type.ToEngineType() != requestedEngineType) - throw new ArgumentException($"The specified game type '{requestedEngineType}' does not match the actual type of the game '{searchPath}' to verify."); - return finderResult.Game; - } - - if (finderResult.FallbackGame is not null && - finderResult.FallbackGame.Directory.FullName.Equals(fullSearchPath, StringComparison.OrdinalIgnoreCase)) - { - if (finderResult.FallbackGame.Type.ToEngineType() != requestedEngineType) - throw new ArgumentException($"The specified game type '{requestedEngineType}' does not match the actual type of the game '{searchPath}' to verify."); - return finderResult.FallbackGame; - } - - return GetMatchingModFromGame(finderResult.Game, requestedEngineType, fullSearchPath) ?? - GetMatchingModFromGame(finderResult.FallbackGame, requestedEngineType, fullSearchPath); - } - - private GameLocations GetDetachedModLocations(string modPath, GameFinderResult gameResult, GameInstallationsSettings settings, out IPhysicalMod mod) - { - IGame game = null!; - - if (gameResult.Game.Type.ToEngineType() == settings.EngineType) - game = gameResult.Game; - if (gameResult.FallbackGame is not null && gameResult.FallbackGame.Type.ToEngineType() == settings.EngineType) - game = gameResult.FallbackGame; - - if (game is null) - throw new GameNotFoundException($"Unable to find game of type '{settings.EngineType}'"); - - var modFinder = ServiceProvider.GetRequiredService(); - var modRef = modFinder.FindMods(game, _fileSystem.DirectoryInfo.New(modPath)).FirstOrDefault(); - - if (modRef is null) - throw new NotSupportedException($"The mod at '{modPath}' is not compatible to the found game '{game}'."); - - var modFactory = ServiceProvider.GetRequiredService(); - mod = modFactory.CreatePhysicalMod(game, modRef, CultureInfo.InvariantCulture); - - game.AddMod(mod); - - mod.ResolveDependencies(); - - return GetLocations(mod, gameResult, settings.AdditionalFallbackPaths); - } - - private static IPhysicalMod? GetMatchingModFromGame(IGame? game, GameEngineType? requestedEngineType, string modPath) - { - if (game is null) - return null; - - var isGameSupported = !requestedEngineType.HasValue || game.Type.ToEngineType() == requestedEngineType; - foreach (var mod in game.Game.Mods) - { - if (mod is IPhysicalMod physicalMod) - { - if (physicalMod.Directory.FullName.Equals(modPath, StringComparison.OrdinalIgnoreCase)) - { - if (!isGameSupported) - throw new ArgumentException($"The specified game type '{requestedEngineType}' does not match the actual type of the mod '{modPath}' to verify."); - return physicalMod; - } - } - } - - return null; - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectors/IModSelector.cs b/src/ModVerify.CliApp/ModSelectors/IModSelector.cs deleted file mode 100644 index a04858c..0000000 --- a/src/ModVerify.CliApp/ModSelectors/IModSelector.cs +++ /dev/null @@ -1,13 +0,0 @@ -using AET.ModVerify.App.Settings; -using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Infrastructure; - -namespace AET.ModVerify.App.ModSelectors; - -internal interface IModSelector -{ - GameLocations? Select( - GameInstallationsSettings settings, - out IPhysicalPlayableObject? targetObject, - out GameEngineType? actualEngineType); -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectors/ManualModSelector.cs b/src/ModVerify.CliApp/ModSelectors/ManualModSelector.cs deleted file mode 100644 index 34cf39d..0000000 --- a/src/ModVerify.CliApp/ModSelectors/ManualModSelector.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using AET.ModVerify.App.Settings; -using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Infrastructure; - -namespace AET.ModVerify.App.ModSelectors; - -internal class ManualModSelector(IServiceProvider serviceProvider) : ModSelectorBase(serviceProvider) -{ - public override GameLocations Select( - GameInstallationsSettings settings, - out IPhysicalPlayableObject? targetObject, - out GameEngineType? actualEngineType) - { - actualEngineType = settings.EngineType; - targetObject = null; - - if (!actualEngineType.HasValue) - throw new ArgumentException("Unable to determine game type. Use --type argument to set the game type."); - - if (string.IsNullOrEmpty(settings.GamePath)) - throw new ArgumentException("Argument --game must be set."); - - return new GameLocations( - settings.ModPaths, - settings.GamePath!, - GetFallbackPaths(settings.FallbackGamePath, settings.AdditionalFallbackPaths)); - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectors/ModSelectorBase.cs b/src/ModVerify.CliApp/ModSelectors/ModSelectorBase.cs deleted file mode 100644 index 8dd1d90..0000000 --- a/src/ModVerify.CliApp/ModSelectors/ModSelectorBase.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using AET.ModVerify.App.GameFinder; -using AET.ModVerify.App.Settings; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Infrastructure; -using PG.StarWarsGame.Infrastructure.Mods; -using PG.StarWarsGame.Infrastructure.Services.Dependencies; - -namespace AET.ModVerify.App.ModSelectors; - -internal abstract class ModSelectorBase : IModSelector -{ - protected readonly ILogger? Logger; - protected readonly GameFinderService GameFinderService; - protected readonly IServiceProvider ServiceProvider; - - protected ModSelectorBase(IServiceProvider serviceProvider) - { - ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - Logger = serviceProvider.GetService()?.CreateLogger(GetType()); - GameFinderService = new GameFinderService(serviceProvider); - } - - public abstract GameLocations? Select( - GameInstallationsSettings settings, - out IPhysicalPlayableObject? targetObject, - out GameEngineType? actualEngineType); - - protected GameLocations GetLocations(IPhysicalPlayableObject playableObject, GameFinderResult finderResult, IList additionalFallbackPaths) - { - var fallbacks = GetFallbackPaths(finderResult, playableObject, additionalFallbackPaths); - var modPaths = GetModPaths(playableObject); - return new GameLocations(modPaths, playableObject.Game.Directory.FullName, fallbacks); - } - - private static IList GetFallbackPaths(GameFinderResult finderResult, IPlayableObject gameOrMod, IList additionalFallbackPaths) - { - var coercedFallbackGame = finderResult.FallbackGame; - if (gameOrMod.Equals(finderResult.FallbackGame)) - coercedFallbackGame = null; - else if (gameOrMod.Game.Equals(finderResult.FallbackGame)) - coercedFallbackGame = null; - - return GetFallbackPaths(coercedFallbackGame?.Directory.FullName, additionalFallbackPaths); - } - - - protected static IList GetFallbackPaths(string? fallbackGame, IList additionalFallbackPaths) - { - var fallbacks = new List(); - if (fallbackGame is not null) - fallbacks.Add(fallbackGame); - foreach (var fallback in additionalFallbackPaths) - fallbacks.Add(fallback); - - return fallbacks; - } - - - private IList GetModPaths(IPhysicalPlayableObject modOrGame) - { - if (modOrGame is not IMod mod) - return Array.Empty(); - - var traverser = ServiceProvider.GetRequiredService(); - return traverser.Traverse(mod) - .OfType().Select(x => x.Directory.FullName) - .ToList(); - } - -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectors/ModSelectorFactory.cs b/src/ModVerify.CliApp/ModSelectors/ModSelectorFactory.cs deleted file mode 100644 index 07ef263..0000000 --- a/src/ModVerify.CliApp/ModSelectors/ModSelectorFactory.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using AET.ModVerify.App.Settings; - -namespace AET.ModVerify.App.ModSelectors; - -internal class ModSelectorFactory(IServiceProvider serviceProvider) -{ - public IModSelector CreateSelector(GameInstallationsSettings settings) - { - if (settings.Interactive) - return new ConsoleModSelector(serviceProvider); - if (settings.UseAutoDetection) - return new AutomaticModSelector(serviceProvider); - if (settings.ManualSetup) - return new ManualModSelector(serviceProvider); - throw new ArgumentException("Unknown option configuration provided."); - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectors/SettingsBasedModSelector.cs b/src/ModVerify.CliApp/ModSelectors/SettingsBasedModSelector.cs deleted file mode 100644 index 221bb9e..0000000 --- a/src/ModVerify.CliApp/ModSelectors/SettingsBasedModSelector.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Linq; -using AET.ModVerify.App.GameFinder; -using AET.ModVerify.App.Settings; -using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Infrastructure; - -namespace AET.ModVerify.App.ModSelectors; - -internal class SettingsBasedModSelector(IServiceProvider serviceProvider) -{ - public VerifyInstallationData CreateInstallationDataFromSettings(GameInstallationsSettings settings) - { - var gameLocations = new ModSelectorFactory(serviceProvider) - .CreateSelector(settings) - .Select(settings, out var targetObject, out var engineType); - - if (gameLocations is null) - throw new GameNotFoundException("Unable to get game locations"); - - if (engineType is null) - throw new InvalidOperationException("Engine type not specified."); - - return new VerifyInstallationData - { - EngineType = engineType.Value, - GameLocations = gameLocations, - Name = GetNameFromGameLocations(targetObject, gameLocations, engineType.Value) - }; - } - - private static string GetNameFromGameLocations(IPlayableObject? targetObject, GameLocations gameLocations, GameEngineType engineType) - { - if (targetObject is not null) - return targetObject.Name; - - var mod = gameLocations.ModPaths.FirstOrDefault(); - return mod ?? gameLocations.GamePath; - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectors/VerifyInstallationData.cs b/src/ModVerify.CliApp/ModSelectors/VerifyInstallationData.cs deleted file mode 100644 index 1a1fcd2..0000000 --- a/src/ModVerify.CliApp/ModSelectors/VerifyInstallationData.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Text; -using PG.StarWarsGame.Engine; - -namespace AET.ModVerify.App.ModSelectors; - -internal sealed class VerifyInstallationData -{ - public required string Name { get; init; } - - public required GameEngineType EngineType { get; init; } - - public required GameLocations GameLocations { get; init; } - - public override string ToString() - { - var sb = new StringBuilder(); - - sb.AppendLine($"ObjectToVerify={Name};EngineType={EngineType};Locations=["); - if (GameLocations.ModPaths.Count > 0) - sb.AppendLine($"Mods=[{string.Join(";", GameLocations.ModPaths)}];"); - sb.AppendLine($"Game=[{GameLocations.GamePath}];"); - if (GameLocations.FallbackPaths.Count > 0) - sb.AppendLine($"Fallbacks=[{string.Join(";", GameLocations.FallbackPaths)}];"); - sb.AppendLine("]"); - - return sb.ToString(); - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModVerify.CliApp.csproj b/src/ModVerify.CliApp/ModVerify.CliApp.csproj index 0073b2b..2301a28 100644 --- a/src/ModVerify.CliApp/ModVerify.CliApp.csproj +++ b/src/ModVerify.CliApp/ModVerify.CliApp.csproj @@ -21,6 +21,10 @@ en + + true + + @@ -30,18 +34,18 @@ - - + + - + - - - - - + + + + + @@ -51,10 +55,6 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - all @@ -64,9 +64,18 @@ - - true - + + + + compile + runtime; build; native; contentfiles; analyzers; buildtransitive + + + compile + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/src/ModVerify.CliApp/ModVerify.CliApp.csproj.DotSettings b/src/ModVerify.CliApp/ModVerify.CliApp.csproj.DotSettings new file mode 100644 index 0000000..0bcf4ed --- /dev/null +++ b/src/ModVerify.CliApp/ModVerify.CliApp.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModVerifyAppEnvironment.cs b/src/ModVerify.CliApp/ModVerifyAppEnvironment.cs index 86bfc40..90a41db 100644 --- a/src/ModVerify.CliApp/ModVerifyAppEnvironment.cs +++ b/src/ModVerify.CliApp/ModVerifyAppEnvironment.cs @@ -1,4 +1,5 @@ -using System.IO.Abstractions; +using System.IO; +using System.IO.Abstractions; using System.Reflection; using AnakinRaW.ApplicationBase.Environment; #if !NET @@ -26,11 +27,17 @@ internal sealed class ModVerifyAppEnvironment(Assembly assembly, IFileSystem fil public override ICollection UpdateMirrors { get; } = new List { #if DEBUG - new("C:\\Test\\ModVerify"), + new(CreateDebugPath()), #endif new($"https://republicatwar.com/downloads/{ModVerifyConstants.ModVerifyToolPath}") }; + private static string CreateDebugPath() + { + var dir = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "../../../../..")); + return Path.Combine(dir, ".local_deploy/server"); + } + public override string UpdateRegistryPath => $@"SOFTWARE\{ModVerifyConstants.ModVerifyToolPath}\Update"; protected override UpdateConfiguration CreateUpdateConfiguration() diff --git a/src/ModVerify.CliApp/ModVerifyApplication.cs b/src/ModVerify.CliApp/ModVerifyApplication.cs deleted file mode 100644 index 7ea461c..0000000 --- a/src/ModVerify.CliApp/ModVerifyApplication.cs +++ /dev/null @@ -1,251 +0,0 @@ -using AET.ModVerify.App.ModSelectors; -using AET.ModVerify.App.Reporting; -using AET.ModVerify.App.Settings; -using AET.ModVerify.Pipeline; -using AET.ModVerify.Reporting; -using AET.ModVerify.Reporting.Settings; -using AnakinRaW.ApplicationBase; -using AnakinRaW.ApplicationBase.Utilities; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using PG.StarWarsGame.Engine; -using Serilog; -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using AET.ModVerify.App.GameFinder; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace AET.ModVerify.App; - -internal sealed class ModVerifyApplication(ModVerifyAppSettings settings, IServiceProvider services) -{ - private readonly ILogger? _logger = services.GetService()?.CreateLogger(typeof(ModVerifyApplication)); - private readonly IFileSystem _fileSystem = services.GetRequiredService(); - private readonly ModVerifyAppEnvironment _appEnvironment = services.GetRequiredService(); - - public async Task Run() - { - using (new UnhandledExceptionHandler(services)) - using (new UnobservedTaskExceptionHandler(services)) - return await RunCore().ConfigureAwait(false); - } - - private async Task RunCore() - { - _logger?.LogDebug("Raw command line: {CommandLine}", Environment.CommandLine); - - var interactive = settings.Interactive; - try - { - return await RunVerify().ConfigureAwait(false); - } - catch (Exception e) - { - _logger?.LogCritical(e, e.Message); - ConsoleUtilities.WriteApplicationFatalError(ModVerifyConstants.AppNameString, e); - return e.HResult; - } - finally - { -#if NET - await Log.CloseAndFlushAsync(); -#else - Log.CloseAndFlush(); -#endif - if (interactive) - { - Console.WriteLine(); - ConsoleUtilities.WriteHorizontalLine('-'); - Console.WriteLine("Press any key to exit"); - Console.ReadLine(); - } - } - } - - - private async Task RunVerify() - { - VerifyInstallationData installData; - try - { - installData = new SettingsBasedModSelector(services) - .CreateInstallationDataFromSettings(settings.GameInstallationsSettings); - } - catch (GameNotFoundException ex) - { - ConsoleUtilities.WriteApplicationFatalError(_appEnvironment.ApplicationName, - "Unable to find an installation of Empire at War or Forces of Corruption."); - _logger?.LogError(ex, "Game not found: {Message}", ex.Message); - return ex.HResult; - } - - var reportSettings = CreateGlobalReportSettings(installData); - - _logger?.LogDebug("Verify install data: {InstallData}", installData); - _logger?.LogTrace("Verify settings: {Settings}", settings); - - var allErrors = await Verify(installData, reportSettings) - .ConfigureAwait(false); - - try - { - await ReportErrors(allErrors).ConfigureAwait(false); - } - catch (GameVerificationException e) - { - return e.HResult; - } - - if (!settings.CreateNewBaseline) - return 0; - - await WriteBaseline(reportSettings, allErrors, settings.NewBaselinePath).ConfigureAwait(false); - _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Baseline successfully created."); - - return 0; - } - - private async Task> Verify( - VerifyInstallationData installData, - GlobalVerifyReportSettings reportSettings) - { - var gameEngineService = services.GetRequiredService(); - var engineErrorReporter = new ConcurrentGameEngineErrorReporter(); - - IStarWarsGameEngine gameEngine; - - try - { - var initProgress = new Progress(); - var initProgressReporter = new EngineInitializeProgressReporter(initProgress); - - try - { - _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Creating Game Engine '{Engine}'", installData.EngineType); - gameEngine = await gameEngineService.InitializeAsync( - installData.EngineType, - installData.GameLocations, - engineErrorReporter, - initProgress, - false, - CancellationToken.None).ConfigureAwait(false); - _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Game Engine created"); - } - finally - { - initProgressReporter.Dispose(); - } - } - catch (Exception e) - { - _logger?.LogError(e, "Creating game engine failed: {Message}", e.Message); - throw; - } - - var progressReporter = new VerifyConsoleProgressReporter(installData.Name); - - using var verifyPipeline = new GameVerifyPipeline( - gameEngine, - engineErrorReporter, - settings.VerifyPipelineSettings, - reportSettings, - progressReporter, - services); - - try - { - try - { - _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Verifying '{Target}'...", installData.Name); - await verifyPipeline.RunAsync().ConfigureAwait(false); - progressReporter.Report(string.Empty, 1.0); - } - catch - { - progressReporter.ReportError("Verification failed", null); - throw; - } - finally - { - progressReporter.Dispose(); - } - } - catch (OperationCanceledException) - { - _logger?.LogWarning(ModVerifyConstants.ConsoleEventId, "Verification stopped due to enabled failFast setting."); - } - catch (Exception e) - { - _logger?.LogError(e, "Verification failed: {Message}", e.Message); - throw; - } - - _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Finished verification"); - return verifyPipeline.FilteredErrors; - } - - private async Task ReportErrors(IReadOnlyCollection errors) - { - _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Reporting Errors..."); - - var reportBroker = new VerificationReportBroker(services); - - await reportBroker.ReportAsync(errors); - - if (errors.Any(x => x.Severity >= settings.AppThrowsOnMinimumSeverity)) - throw new GameVerificationException(errors); - } - - private async Task WriteBaseline( - GlobalVerifyReportSettings reportSettings, - IEnumerable errors, - string baselineFile) - { - var baseline = new VerificationBaseline(reportSettings.MinimumReportSeverity, errors); - - var fullPath = _fileSystem.Path.GetFullPath(baselineFile); - _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Writing Baseline to '{FullPath}'", fullPath); - -#if NET - await -#endif - using var fs = _fileSystem.FileStream.New(fullPath, FileMode.Create, FileAccess.Write, FileShare.None); - await baseline.ToJsonAsync(fs); - } - - private GlobalVerifyReportSettings CreateGlobalReportSettings(VerifyInstallationData installData) - { - var baselineSelector = new BaselineSelector(settings, services); - var baseline = baselineSelector.SelectBaseline(installData, out var baselinePath); - - if (baseline.Count > 0) - _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Using baseline '{Baseline}'", baselinePath); - - var suppressionsFile = settings.ReportSettings.SuppressionsPath; - SuppressionList suppressions; - - if (string.IsNullOrEmpty(suppressionsFile)) - suppressions = SuppressionList.Empty; - else - { - using var fs = _fileSystem.File.OpenRead(suppressionsFile); - suppressions = SuppressionList.FromJson(fs); - - if (suppressions.Count > 0) - _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Using suppressions from '{Suppressions}'", suppressionsFile); - } - - - return new GlobalVerifyReportSettings - { - Baseline = baseline, - Suppressions = suppressions, - MinimumReportSeverity = settings.ReportSettings.MinimumReportSeverity, - }; - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModVerifyConstants.cs b/src/ModVerify.CliApp/ModVerifyConstants.cs index 6b60f06..041a4a5 100644 --- a/src/ModVerify.CliApp/ModVerifyConstants.cs +++ b/src/ModVerify.CliApp/ModVerifyConstants.cs @@ -9,5 +9,9 @@ internal static class ModVerifyConstants public const string ModVerifyToolPath = "ModVerify"; public const int ConsoleEventIdValue = 1138; + public const int Success = 0; + public const int CompletedWithFindings = 1; + public const int ErrorBadArguments = 0xA0; + public static readonly EventId ConsoleEventId = new(ConsoleEventIdValue, "LogToConsole"); } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Program.cs b/src/ModVerify.CliApp/Program.cs index cd4747b..2d23b3b 100644 --- a/src/ModVerify.CliApp/Program.cs +++ b/src/ModVerify.CliApp/Program.cs @@ -40,6 +40,7 @@ using System.IO.Abstractions; using System.Runtime.InteropServices; using System.Threading.Tasks; +using AET.ModVerify.App.Reporting; using Testably.Abstractions; using ILogger = Serilog.ILogger; @@ -61,21 +62,22 @@ internal class Program : SelfUpdateableAppLifecycle private static readonly string ModVerifyRootNameSpace = typeof(Program).Namespace!; private static readonly CompiledExpression PrintToConsoleExpression = SerilogExpression.Compile($"EventId.Id = {ModVerifyConstants.ConsoleEventIdValue}"); - private static ModVerifyOptionsContainer _optionsContainer = null!; - - protected override async Task InitializeAppAsync(IReadOnlyList args) + private AppSettingsBase? _modVerifyAppSettings; + private ApplicationUpdateOptions _updateOptions = new(); + private bool _offlineMode; + private bool _verboseMode; + private bool _isLaunchedWithoutArguments; + + protected override async Task InitializeAppAsync(IReadOnlyList args, IServiceProvider bootstrapServices) { + await base.InitializeAppAsync(args, bootstrapServices); + ModVerifyConsoleUtilities.WriteHeader(ApplicationEnvironment.AssemblyInfo.InformationalVersion); - await base.InitializeAppAsync(args); - + ModVerifyOptionsContainer parsedOptions; try { - var settings = new ModVerifyOptionsParser(ApplicationEnvironment, BootstrapLoggerFactory).Parse(args); - if (!settings.HasOptions) - return 0xA0; - _optionsContainer = settings; - return 0; + parsedOptions = new ModVerifyOptionsParser(ApplicationEnvironment, BootstrapLoggerFactory).Parse(args); } catch (Exception e) { @@ -83,6 +85,41 @@ protected override async Task InitializeAppAsync(IReadOnlyList args ConsoleUtilities.WriteApplicationFatalError(ModVerifyConstants.AppNameString, e); return e.HResult; } + + if (!parsedOptions.HasOptions) + return ModVerifyConstants.ErrorBadArguments; + + if (parsedOptions.UpdateOptions is not null) + _updateOptions = parsedOptions.UpdateOptions; + + if (parsedOptions.ModVerifyOptions?.Verbose is true || parsedOptions.UpdateOptions?.Verbose is true) + _verboseMode = true; + + if (parsedOptions.ModVerifyOptions is null) + return ModVerifyConstants.Success; + + _offlineMode = parsedOptions.ModVerifyOptions.OfflineMode; + _isLaunchedWithoutArguments = parsedOptions.ModVerifyOptions.LaunchedWithoutArguments(); + + try + { + _modVerifyAppSettings = new SettingsBuilder(bootstrapServices) + .BuildSettings(parsedOptions.ModVerifyOptions); + } + catch (AppArgumentException e) + { + Logger?.LogCritical(e, "Invalid arguments specified by the user: {Message}", e.Message); + ConsoleUtilities.WriteApplicationFatalError(ModVerifyConstants.AppNameString, e.Message); + return e.HResult; + } + catch (Exception e) + { + Logger?.LogCritical(e, "Failed to create settings form commandline arguments: {Message}", e.Message); + ConsoleUtilities.WriteApplicationFatalError(ModVerifyConstants.AppNameString, e); + return e.HResult; + } + + return ModVerifyConstants.Success; } protected override void CreateAppServices(IServiceCollection services, IReadOnlyList args) @@ -106,8 +143,8 @@ protected override void CreateAppServices(IServiceCollection services, IReadOnly sp => new CosturaApplicationProductService(ApplicationEnvironment, sp), sp => new JsonManifestLoader(sp)); } - - if (_optionsContainer.ModVerifyOptions is null) + + if (_modVerifyAppSettings is null) return; SteamAbstractionLayer.InitializeServices(services); @@ -122,10 +159,11 @@ protected override void CreateAppServices(IServiceCollection services, IReadOnly PetroglyphEngineServiceContribution.ContributeServices(services); services.RegisterVerifierCache(); - + services.AddSingleton(sp => new BaselineFactory(sp)); + SetupVerifyReporting(services); - if (_optionsContainer.ModVerifyOptions.OfflineMode) + if (_offlineMode) { services.AddSingleton(sp => new OfflineModNameResolver(sp)); services.AddSingleton(sp => new OfflineModGameTypeResolver(sp)); @@ -157,59 +195,39 @@ protected override IRegistry CreateRegistry() protected override async Task RunAppAsync(string[] args, IServiceProvider appServiceProvider) { var result = await HandleUpdate(appServiceProvider); - if (result != 0 || _optionsContainer.ModVerifyOptions is null) + if (result != 0 || _modVerifyAppSettings is null) return result; - - ModVerifyAppSettings modVerifySettings; - - try - { - modVerifySettings = new SettingsBuilder(appServiceProvider).BuildSettings(_optionsContainer.ModVerifyOptions); - } - catch (Exception e) - { - Logger?.LogCritical(e, "Failed to create settings form commandline arguments: {EMessage}", e.Message); - ConsoleUtilities.WriteApplicationFatalError(ModVerifyConstants.AppNameString, e); - return e.HResult; - } - - return await new ModVerifyApplication(modVerifySettings, appServiceProvider).Run().ConfigureAwait(false); + return await new ModVerifyApplication(_modVerifyAppSettings, appServiceProvider).RunAsync().ConfigureAwait(false); } private void SetupVerifyReporting(IServiceCollection serviceCollection) { - var options = _optionsContainer.ModVerifyOptions; - Debug.Assert(options is not null); - + Debug.Assert(_modVerifyAppSettings is not null); - var verifyVerb = options as VerifyVerbOption; + var verifySettings = _modVerifyAppSettings as AppVerifySettings; - // Console should be in minimal summary mode if we are not in verify mode. - var printOnlySummary = verifyVerb is null; - - serviceCollection.RegisterConsoleReporter(new VerifyReportSettings + // Console should be in minimal summary mode if we are in a different mode than verify. + serviceCollection.RegisterConsoleReporter(new ReporterSettings { - MinimumReportSeverity = VerificationSeverity.Error - }, printOnlySummary); + MinimumReportSeverity = verifySettings?.VerifyPipelineSettings.FailFastSettings.IsFailFast is true + ? VerificationSeverity.Information + : VerificationSeverity.Error + }, summaryOnly: verifySettings is null); - if (verifyVerb == null) + if (verifySettings == null) return; - var outputDirectory = Environment.CurrentDirectory; - - if (!string.IsNullOrEmpty(verifyVerb.OutputDirectory)) - outputDirectory = FileSystem.Path.GetFullPath(FileSystem.Path.Combine(Environment.CurrentDirectory, verifyVerb.OutputDirectory!)); - + var outputDirectory = verifySettings.ReportDirectory; serviceCollection.RegisterJsonReporter(new JsonReporterSettings { OutputDirectory = outputDirectory!, - MinimumReportSeverity = options.MinimumSeverity + MinimumReportSeverity = _modVerifyAppSettings.ReportSettings.MinimumReportSeverity }); serviceCollection.RegisterTextFileReporter(new TextFileReporterSettings { OutputDirectory = outputDirectory!, - MinimumReportSeverity = options.MinimumSeverity + MinimumReportSeverity = _modVerifyAppSettings.ReportSettings.MinimumReportSeverity }); } @@ -224,7 +242,7 @@ private void ConfigureLogging(ILoggingBuilder loggingBuilder) loggingBuilder.AddDebug(); #endif - if (_optionsContainer.ModVerifyOptions?.Verbose == true || _optionsContainer.UpdateOptions?.Verbose == true) + if (_verboseMode) { logLevel = LogEventLevel.Verbose; loggingBuilder.AddDebug(); @@ -297,31 +315,30 @@ static bool IsXmlParserLogging(LogEvent logEvent) private async Task HandleUpdate(IServiceProvider serviceProvider) { - var updateOptions = _optionsContainer.UpdateOptions ?? new ApplicationUpdateOptions(); + if (_offlineMode) + { + Logger?.LogTrace("Running in offline mode. There will be nothing to update."); + return ModVerifyConstants.Success; + } + ModVerifyUpdateMode updateMode; - if (_optionsContainer.ModVerifyOptions is not null) + if (_isLaunchedWithoutArguments) + updateMode = ModVerifyUpdateMode.InteractiveUpdate; + else { - if (_optionsContainer.ModVerifyOptions.OfflineMode) - { - Logger?.LogTrace("Running in offline mode. There will be nothing to update."); - return 0; - } - - updateMode = _optionsContainer.ModVerifyOptions.LaunchedWithoutArguments() - ? ModVerifyUpdateMode.InteractiveUpdate - : ModVerifyUpdateMode.CheckOnly; + updateMode = _modVerifyAppSettings is not null + ? ModVerifyUpdateMode.CheckOnly + : ModVerifyUpdateMode.AutoUpdate; } - else - updateMode = ModVerifyUpdateMode.AutoUpdate; - + try { Logger?.LogDebug("Running update with mode '{ModVerifyUpdateMode}'", updateMode); var modVerifyUpdater = new ModVerifyUpdater(serviceProvider); - await modVerifyUpdater.RunUpdateProcedure(updateOptions, updateMode).ConfigureAwait(false); + await modVerifyUpdater.RunUpdateProcedure(_updateOptions, updateMode).ConfigureAwait(false); Logger?.LogDebug("Update procedure completed successfully."); - return 0; + return ModVerifyConstants.Success; } catch (Exception e) { diff --git a/src/ModVerify.CliApp/Properties/launchSettings.json b/src/ModVerify.CliApp/Properties/launchSettings.json index e47583a..299ce46 100644 --- a/src/ModVerify.CliApp/Properties/launchSettings.json +++ b/src/ModVerify.CliApp/Properties/launchSettings.json @@ -1,21 +1,20 @@ { "profiles": { - "Run": { + "Verify": { "commandName": "Project", "commandLineArgs": "" }, - "Interactive Verify": { + "Verify (Interactive)": { "commandName": "Project", - "commandLineArgs": "verify -o verifyResults --minFailSeverity Information --offline" + "commandLineArgs": "verify -o verifyResults --offline --minFailSeverity Information" }, - "Interactive Baseline": { + "Verify (Automatic Target Selection)": { "commandName": "Project", - "commandLineArgs": "createBaseline -o focBaseline.json --offline" + "commandLineArgs": "verify -o verifyResults --path \"C:\\Program Files (x86)\\Steam\\steamapps\\common\\Star Wars Empire at War\\corruption\"" }, - - "FromModPath": { + "Create Baseline Interactive": { "commandName": "Project", - "commandLineArgs": "-o verifyResults --baseline focBaseline.json --path C:/test --type Foc" + "commandLineArgs": "createBaseline -o baseline.json --offline --skipLocation" } } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Reporting/BaselineFactory.cs b/src/ModVerify.CliApp/Reporting/BaselineFactory.cs index 9fcc911..a83cea9 100644 --- a/src/ModVerify.CliApp/Reporting/BaselineFactory.cs +++ b/src/ModVerify.CliApp/Reporting/BaselineFactory.cs @@ -1,24 +1,31 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.IO.Abstractions; +using AET.ModVerify.App.Settings; using AET.ModVerify.Reporting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using System.Threading.Tasks; +using PG.StarWarsGame.Engine; +using AET.ModVerify.App.Utilities; namespace AET.ModVerify.App.Reporting; -internal sealed class BaselineFactory(IServiceProvider serviceProvider) +internal sealed class BaselineFactory(IServiceProvider serviceProvider) : IBaselineFactory { private readonly ILogger? _logger = serviceProvider.GetService()?.CreateLogger(typeof(BaselineFactory)); private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); - public bool TryCreateBaseline( + public bool TryFindBaselineInDirectory( string directory, - out VerificationBaseline baseline, + Predicate baselineSelector, + [NotNullWhen(true)] out VerificationBaseline? baseline, [NotNullWhen(true)] out string? path) { - baseline = VerificationBaseline.Empty; + baseline = null; path = null; if (!_fileSystem.Directory.Exists(directory)) @@ -42,9 +49,17 @@ public bool TryCreateBaseline( { try { - baseline = CreateBaselineFromFilePath(jsonFile); - path = jsonFile; - _logger?.LogDebug("Create baseline from file: {JsonFile}", jsonFile); + var parsedBaseline = CreateBaselineFromFilePath(jsonFile); + if (!baselineSelector(parsedBaseline)) + { + _logger?.LogDebug("Baseline '{JsonFile}' was denied by selector.", jsonFile); + continue; + } + + baseline = parsedBaseline; + path = _fileSystem.Path.GetFullPath(jsonFile); + + _logger?.LogDebug("Create baseline from file '{JsonFile}'", jsonFile); return true; } catch (InvalidBaselineException e) @@ -54,15 +69,50 @@ public bool TryCreateBaseline( } } + baseline = null; path = null; return false; } - public VerificationBaseline CreateBaseline(string filePath) + public VerificationBaseline ParseBaseline(string filePath) { return CreateBaselineFromFilePath(filePath); } + public VerificationBaseline CreateBaseline( + VerificationTarget target, + AppBaselineSettings settings, + IEnumerable errors) + { + var baselineTarget = new BaselineVerificationTarget + { + Engine = target.Engine, + Name = target.Name, + Version = target.Version, + Location = settings.WriteLocations ? MaskUsername(target.Location) : null, + IsGame = target.IsGame, + }; + + return new VerificationBaseline(settings.ReportSettings.MinimumReportSeverity, errors, baselineTarget); + } + + private static GameLocations MaskUsername(GameLocations targetLocation) + { + return new GameLocations( + targetLocation.ModPaths.Select(PathUtilities.MaskUsername).ToList(), + PathUtilities.MaskUsername(targetLocation.GamePath), + targetLocation.FallbackPaths.Select(PathUtilities.MaskUsername).ToList()); + } + + public async Task WriteBaselineAsync(VerificationBaseline baseline, string filePath) + { +#if NET + await +#endif + using var fs = _fileSystem.FileStream.New(filePath, FileMode.Create, FileAccess.Write, FileShare.None); + await baseline.ToJsonAsync(fs); + } + private VerificationBaseline CreateBaselineFromFilePath(string baselineFile) { using var fs = _fileSystem.FileStream.New(baselineFile, FileMode.Open, FileAccess.Read); diff --git a/src/ModVerify.CliApp/Reporting/BaselineSelector.cs b/src/ModVerify.CliApp/Reporting/BaselineSelector.cs index 95953f1..791eaae 100644 --- a/src/ModVerify.CliApp/Reporting/BaselineSelector.cs +++ b/src/ModVerify.CliApp/Reporting/BaselineSelector.cs @@ -1,5 +1,4 @@ -using AET.ModVerify.App.ModSelectors; -using AET.ModVerify.App.Resources.Baselines; +using AET.ModVerify.App.Resources.Baselines; using AET.ModVerify.App.Settings; using AET.ModVerify.Reporting; using AnakinRaW.ApplicationBase; @@ -8,15 +7,17 @@ using PG.StarWarsGame.Engine; using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text; namespace AET.ModVerify.App.Reporting; -internal sealed class BaselineSelector(ModVerifyAppSettings settings, IServiceProvider services) +internal sealed class BaselineSelector(AppVerifySettings settings, IServiceProvider services) { private readonly ILogger? _logger = services.GetService()?.CreateLogger(typeof(ModVerifyApplication)); - private readonly BaselineFactory _baselineFactory = new(services); + private readonly IBaselineFactory _baselineFactory = services.GetRequiredService(); - public VerificationBaseline SelectBaseline(VerifyInstallationData installationData, out string? usedBaselinePath) + public VerificationBaseline SelectBaseline(VerificationTarget verificationTarget, out string? usedBaselinePath) { var baselinePath = settings.ReportSettings.BaselinePath; if (!string.IsNullOrEmpty(baselinePath)) @@ -24,7 +25,7 @@ public VerificationBaseline SelectBaseline(VerifyInstallationData installationDa try { usedBaselinePath = baselinePath; - return _baselineFactory.CreateBaseline(baselinePath!); + return _baselineFactory.ParseBaseline(baselinePath); } catch (InvalidBaselineException e) { @@ -43,20 +44,21 @@ public VerificationBaseline SelectBaseline(VerifyInstallationData installationDa if (!settings.ReportSettings.SearchBaselineLocally) { - _logger?.LogDebug(ModVerifyConstants.ConsoleEventId, "No baseline path specified and local search is not enabled. Using empty baseline."); + _logger?.LogDebug(ModVerifyConstants.ConsoleEventId, + "No baseline path specified and local search is not enabled. Using empty baseline."); usedBaselinePath = null; return VerificationBaseline.Empty; } - if (settings.Interactive) - return FindBaselineInteractive(installationData, out usedBaselinePath); + if (settings.IsInteractive) + return FindBaselineInteractive(verificationTarget, out usedBaselinePath); // If the application is not interactive, we only use a baseline file present in the directory of the verification target. - return FindBaselineNonInteractive(installationData.GameLocations.TargetPath, out usedBaselinePath); + return FindBaselineNonInteractive(verificationTarget, out usedBaselinePath); } - private VerificationBaseline FindBaselineInteractive(VerifyInstallationData installationData, out string? baselinePath) + private VerificationBaseline FindBaselineInteractive(VerificationTarget verificationTarget, out string? baselinePath) { // The application is in interactive mode. We apply the following lookup: // 1. Use a baseline found in the directory of the verification target. @@ -66,48 +68,52 @@ private VerificationBaseline FindBaselineInteractive(VerifyInstallationData inst _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Searching for local baseline files..."); - if (!_baselineFactory.TryCreateBaseline(installationData.GameLocations.TargetPath, out var baseline, + if (!_baselineFactory.TryFindBaselineInDirectory( + verificationTarget.Location.TargetPath, + b => IsBaselineCompatible(b, verificationTarget), + out var baseline, out baselinePath)) { - if (!_baselineFactory.TryCreateBaseline("./", out baseline, out baselinePath)) + if (!_baselineFactory.TryFindBaselineInDirectory( + Environment.CurrentDirectory, + b => IsBaselineCompatible(b, verificationTarget), + out baseline, + out baselinePath)) { - // It does not make sense to load the game's default baselines if the user wants to verify the game, - // as the verification result would always be empty (at least in a non-development scenario) - if (installationData.GameLocations.ModPaths.Count == 0) - { - _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "No local baseline file found."); - return VerificationBaseline.Empty; - } - + Console.ForegroundColor = ConsoleColor.Cyan; Console.WriteLine("No baseline found locally."); - return TryGetDefaultBaseline(installationData.EngineType, out baselinePath); + Console.ResetColor(); + baselinePath = null; + TryGetDefaultBaseline(verificationTarget.Engine, out baseline); + return baseline ?? VerificationBaseline.Empty; } } - Debug.Assert(baselinePath is not null); + Debug.Assert(baselinePath is not null && baseline is not null); - return ConsoleUtilities.UserYesNoQuestion($"ModVerify found the baseline file '{baselinePath}'. Do you want to use it?") - ? baseline + return ShouldUseBaseline(baseline, baselinePath) + ? baseline : VerificationBaseline.Empty; } - private VerificationBaseline TryGetDefaultBaseline(GameEngineType engineType, out string? baselinePath) + private static bool TryGetDefaultBaseline( + GameEngineType engineType, + [NotNullWhen(true)] out VerificationBaseline? baseline) { - baselinePath = null; + baseline = null; if (engineType == GameEngineType.Eaw) { // TODO: EAW currently not implemented - return VerificationBaseline.Empty; + return false; } if (!ConsoleUtilities.UserYesNoQuestion($"Do you want to load the default baseline for game engine '{engineType}'?")) - return VerificationBaseline.Empty; - - baselinePath = $"{engineType} (Default)"; + return false; try { - return LoadEmbeddedBaseline(engineType); + baseline = LoadEmbeddedBaseline(engineType); + return true; } catch (InvalidBaselineException) { @@ -116,7 +122,7 @@ private VerificationBaseline TryGetDefaultBaseline(GameEngineType engineType, ou } } - internal VerificationBaseline LoadEmbeddedBaseline(GameEngineType engineType) + internal static VerificationBaseline LoadEmbeddedBaseline(GameEngineType engineType) { var baselineFileName = $"baseline-{engineType.ToString().ToLower()}.json"; var resourcePath = $"{typeof(BaselineResources).Namespace}.{baselineFileName}"; @@ -125,15 +131,39 @@ internal VerificationBaseline LoadEmbeddedBaseline(GameEngineType engineType) return VerificationBaseline.FromJson(baselineStream); } - private VerificationBaseline FindBaselineNonInteractive(string targetPath, out string? usedPath) + private VerificationBaseline FindBaselineNonInteractive(VerificationTarget target, out string? usedPath) { - if (_baselineFactory.TryCreateBaseline(targetPath, out var baseline, out usedPath)) + if (_baselineFactory.TryFindBaselineInDirectory( + target.Location.TargetPath, + b => IsBaselineCompatible(b, target), + out var baseline, + out usedPath)) { _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Automatically applying local baseline file '{Path}'.", usedPath); return baseline; } - _logger?.LogTrace("No baseline file found in taget path '{TargetPath}'.", targetPath); + _logger?.LogTrace("No baseline file found in taget path '{TargetPath}'.", target.Location.TargetPath); usedPath = null; return VerificationBaseline.Empty; } + + + private static bool IsBaselineCompatible(VerificationBaseline baseline, VerificationTarget target) + { + return baseline.Target?.Engine == target.Engine; + } + + private static bool ShouldUseBaseline(VerificationBaseline baseline, string baselinePath) + { + var sb = new StringBuilder("Found baseline "); + if (baseline.Target is not null) + sb.Append($"for '{baseline.Target.Name}' "); + + sb.Append($"at '{baselinePath}'."); + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine(sb.ToString()); + + return ConsoleUtilities.UserYesNoQuestion("Do you want to use it?"); + } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Reporting/EngineInitializeProgressReporter.cs b/src/ModVerify.CliApp/Reporting/EngineInitializeProgressReporter.cs index b994e97..d93462f 100644 --- a/src/ModVerify.CliApp/Reporting/EngineInitializeProgressReporter.cs +++ b/src/ModVerify.CliApp/Reporting/EngineInitializeProgressReporter.cs @@ -1,30 +1,25 @@ using System; +using PG.StarWarsGame.Engine; namespace AET.ModVerify.App.Reporting; -internal sealed class EngineInitializeProgressReporter : IDisposable -{ - private Progress? _progress; - - public EngineInitializeProgressReporter(Progress? progress) +internal sealed class EngineInitializeProgressReporter(GameEngineType engine) : IGameEngineInitializationReporter +{ + public void ReportProgress(string message) { - if (progress is null) - return; - progress.ProgressChanged += OnProgress; + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine(message); + Console.ResetColor(); } - private void OnProgress(object sender, string e) + public void ReportStarted() { - Console.ForegroundColor = ConsoleColor.DarkGray; - Console.WriteLine(e); - Console.ResetColor(); + Console.WriteLine($"Initializing game engine '{engine}'..."); } - public void Dispose() + public void ReportFinished() { + Console.WriteLine($"Game engine initialized."); Console.WriteLine(); - if (_progress is not null) - _progress.ProgressChanged -= OnProgress; - _progress = null; } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Reporting/IBaselineFactory.cs b/src/ModVerify.CliApp/Reporting/IBaselineFactory.cs new file mode 100644 index 0000000..721a7ce --- /dev/null +++ b/src/ModVerify.CliApp/Reporting/IBaselineFactory.cs @@ -0,0 +1,26 @@ +using AET.ModVerify.App.Settings; +using AET.ModVerify.Reporting; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +namespace AET.ModVerify.App.Reporting; + +internal interface IBaselineFactory +{ + bool TryFindBaselineInDirectory( + string directory, + Predicate baselineSelector, + [NotNullWhen(true)] out VerificationBaseline? baseline, + [NotNullWhen(true)] out string? path); + + VerificationBaseline ParseBaseline(string filePath); + + Task WriteBaselineAsync(VerificationBaseline baseline, string filePath); + + VerificationBaseline CreateBaseline( + VerificationTarget target, + AppBaselineSettings settings, + IEnumerable errors); +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs b/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs index 4700457..b2ce170 100644 --- a/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs +++ b/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs @@ -1,5 +1,6 @@ using System; using System.Threading; +using AET.ModVerify.App.Settings; using AET.ModVerify.Pipeline.Progress; using AnakinRaW.CommonUtilities; using AnakinRaW.CommonUtilities.SimplePipeline.Progress; @@ -7,7 +8,8 @@ namespace AET.ModVerify.App.Reporting; -public sealed class VerifyConsoleProgressReporter(string toVerifyName) : DisposableObject, IVerifyProgressReporter +public sealed class VerifyConsoleProgressReporter(string toVerifyName, AppReportSettings reportSettings) + : DisposableObject, IVerifyProgressReporter { private static readonly ProgressBarOptions ProgressBarOptions = new() { @@ -17,6 +19,7 @@ public sealed class VerifyConsoleProgressReporter(string toVerifyName) : Disposa WriteQueuedMessage = WriteQueuedMessage, }; + private readonly bool _verbose = reportSettings.Verbose; private ProgressBar? _progressBar; public void ReportError(string message, string? errorLine) @@ -38,8 +41,8 @@ public void Report(double progress, string? progressText, ProgressType type, Ver var progressBar = EnsureProgressBar(); - // TODO: Only recognize detailed mode - progressBar.Message = progressText; + if (detailedProgress.IsDetailed) + progressBar.Message = progressText; if (progress >= 1.0) progressBar.Message = $"Verified '{toVerifyName}'"; @@ -47,8 +50,8 @@ public void Report(double progress, string? progressText, ProgressType type, Ver var cpb = progressBar.AsProgress(); cpb.Report(progress); - // TODO: Only in verbose mode - //progressBar.WriteLine(progressText); + if (_verbose) + progressBar.WriteLine(progressText); } protected override void DisposeResources() diff --git a/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json b/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json index c94d121..ce70f8a 100644 --- a/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json +++ b/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json @@ -1,5 +1,11 @@ { - "version": "2.0", + "version": "2.1", + "target": { + "name": "Forces of Corruption (SteamGold)", + "engine": "Foc", + "isGame": true, + "version": "1.121.13.7360" + }, "minSeverity": "Information", "errors": [ { @@ -36,12 +42,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_SKIPRAY.ALO\u0027.", + "message": "Unable to find .ALO file \u0027CIN_Reb_CelebHall.alo\u0027", "severity": "Error", - "context": [ - "DATA\\ART\\MODELS\\UV_SKIPRAY.ALO" - ], - "asset": "Default.fx" + "context": [], + "asset": "CIN_Reb_CelebHall.alo" }, { "id": "FILE00", @@ -49,36 +53,26 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_PRISON.ALO\u0027", + "message": "Proxy particle \u0027p_ssd_debris\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_ECLIPSE_UC_DC.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\NB_PRISON.ALO" - ], - "asset": "p_smoke_small_thin2" - }, - { - "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + "DATA\\ART\\MODELS\\UV_ECLIPSE_UC_DC.ALO" ], - "message": "Unable to find .ALO file \u0027W_Kamino_Reflect.ALO\u0027", - "severity": "Error", - "context": [], - "asset": "W_Kamino_Reflect.ALO" + "asset": "p_ssd_debris" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" ], - "message": "Proxy particle \u0027p_ssd_debris\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_ECLIPSE_UC_DC.ALO\u0027", + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall.tga\u0027 for context: [W_SITH_LEFTHALL.ALO].", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\UV_ECLIPSE_UC_DC.ALO" + "W_SITH_LEFTHALL.ALO" ], - "asset": "p_ssd_debris" + "asset": "Cin_Reb_CelebHall_Wall.tga" }, { "id": "FILE00", @@ -86,10 +80,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_p_proton_torpedo.alo\u0027", + "message": "Unable to find .ALO file \u0027Cin_ImperialCraft.alo\u0027", "severity": "Error", "context": [], - "asset": "CIN_p_proton_torpedo.alo" + "asset": "Cin_ImperialCraft.alo" }, { "id": "FILE00", @@ -97,10 +91,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_DStar_LeverPanel.alo\u0027", + "message": "Unable to find .ALO file \u0027Cin_Officer.alo\u0027", "severity": "Error", "context": [], - "asset": "Cin_DStar_LeverPanel.alo" + "asset": "Cin_Officer.alo" }, { "id": "FILE00", @@ -108,12 +102,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027lookat\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_ECLIPSE.ALO\u0027", + "message": "Unable to find .ALO file \u0027Cin_DStar_protons.alo\u0027", "severity": "Error", - "context": [ - "DATA\\ART\\MODELS\\UV_ECLIPSE.ALO" - ], - "asset": "lookat" + "context": [], + "asset": "Cin_DStar_protons.alo" }, { "id": "FILE00", @@ -122,12 +114,12 @@ "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", "AET.ModVerify.Verifiers.Commons.TextureVeifier" ], - "message": "Could not find texture \u0027Cin_DeathStar.tga\u0027 for context: [ALTTEST.ALO].", + "message": "Could not find texture \u0027w_grenade.tga\u0027 for context: [W_GRENADE.ALO].", "severity": "Error", "context": [ - "ALTTEST.ALO" + "W_GRENADE.ALO" ], - "asset": "Cin_DeathStar.tga" + "asset": "w_grenade.tga" }, { "id": "FILE00", @@ -135,12 +127,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_prison_light\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_PRISON.ALO\u0027", + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_03_STATION_D.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\NB_PRISON.ALO" + "DATA\\ART\\MODELS\\UB_03_STATION_D.ALO" ], - "asset": "p_prison_light" + "asset": "p_uwstation_death" }, { "id": "FILE00", @@ -148,26 +140,21 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\EV_MDU_SENSORNODE.ALO\u0027.", + "message": "Unable to find .ALO file \u0027Cin_EI_Vader.alo\u0027", "severity": "Error", - "context": [ - "DATA\\ART\\MODELS\\EV_MDU_SENSORNODE.ALO" - ], - "asset": "Default.fx" + "context": [], + "asset": "Cin_EI_Vader.alo" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", - "AET.ModVerify.Verifiers.Commons.TextureVeifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Could not find texture \u0027w_grenade.tga\u0027 for context: [W_GRENADE.ALO].", + "message": "Unable to find .ALO file \u0027MODELS\u0027", "severity": "Error", - "context": [ - "W_GRENADE.ALO" - ], - "asset": "w_grenade.tga" + "context": [], + "asset": "MODELS" }, { "id": "FILE00", @@ -175,10 +162,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_Rbel_NavyRow.alo\u0027", + "message": "Unable to find .ALO file \u0027Cin_DeathStar_Wall.alo\u0027", "severity": "Error", "context": [], - "asset": "CIN_Rbel_NavyRow.alo" + "asset": "Cin_DeathStar_Wall.alo" }, { "id": "FILE00", @@ -186,10 +173,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_Planet_Alderaan_High.alo\u0027", + "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_PRISON.ALO\u0027", "severity": "Error", - "context": [], - "asset": "Cin_Planet_Alderaan_High.alo" + "context": [ + "DATA\\ART\\MODELS\\NB_PRISON.ALO" + ], + "asset": "p_smoke_small_thin2" }, { "id": "FILE00", @@ -197,12 +186,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027lookat\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_ECLIPSE_UC.ALO\u0027", + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_01_STATION_D.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\UV_ECLIPSE_UC.ALO" + "DATA\\ART\\MODELS\\UB_01_STATION_D.ALO" ], - "asset": "lookat" + "asset": "p_uwstation_death" }, { "id": "FILE00", @@ -210,12 +199,23 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\EI_MARAJADE.ALO\u0027", + "message": "Unable to find .ALO file \u0027CIN_Officer_Row.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Officer_Row.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_MEDIUM.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\EI_MARAJADE.ALO" + "DATA\\ART\\MODELS\\W_STARS_MEDIUM.ALO" ], - "asset": "p_desert_ground_dust" + "asset": "Lensflare0" }, { "id": "FILE00", @@ -223,10 +223,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027p_splash_wake_lava.alo\u0027", + "message": "Unable to find .ALO file \u0027CIN_DeathStar_Hangar.alo\u0027", "severity": "Error", "context": [], - "asset": "p_splash_wake_lava.alo" + "asset": "CIN_DeathStar_Hangar.alo" }, { "id": "FILE00", @@ -234,10 +234,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_rv_XWingProp.alo\u0027", + "message": "Unable to find .ALO file \u0027Cin_EV_lambdaShuttle_150.alo\u0027", "severity": "Error", "context": [], - "asset": "Cin_rv_XWingProp.alo" + "asset": "Cin_EV_lambdaShuttle_150.alo" }, { "id": "FILE00", @@ -245,10 +245,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_Fire_Huge.alo\u0027", + "message": "Proxy particle \u0027p_smoke_small_thin4\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_PRISON.ALO\u0027", "severity": "Error", - "context": [], - "asset": "CIN_Fire_Huge.alo" + "context": [ + "DATA\\ART\\MODELS\\NB_PRISON.ALO" + ], + "asset": "p_smoke_small_thin4" }, { "id": "FILE00", @@ -256,10 +258,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_Probe_Droid.alo\u0027", + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_CINE.ALO\u0027", "severity": "Error", - "context": [], - "asset": "CIN_Probe_Droid.alo" + "context": [ + "DATA\\ART\\MODELS\\W_STARS_CINE.ALO" + ], + "asset": "Lensflare0" }, { "id": "FILE00", @@ -267,12 +271,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_05_STATION_D.ALO\u0027", + "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_SKIPRAY.ALO\u0027.", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\UB_05_STATION_D.ALO" + "DATA\\ART\\MODELS\\UV_SKIPRAY.ALO" ], - "asset": "p_uwstation_death" + "asset": "Default.fx" }, { "id": "FILE00", @@ -280,10 +284,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\RI_KYLEKATARN.ALO\u0027", + "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\UI_IG88.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\RI_KYLEKATARN.ALO" + "DATA\\ART\\MODELS\\UI_IG88.ALO" ], "asset": "p_desert_ground_dust" }, @@ -293,26 +297,34 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_steam_small\u0027 not found for model \u0027DATA\\ART\\MODELS\\RB_HEAVYVEHICLEFACTORY.ALO\u0027", + "message": "Unable to find .ALO file \u0027CIN_p_proton_torpedo.alo\u0027", "severity": "Error", - "context": [ - "DATA\\ART\\MODELS\\RB_HEAVYVEHICLEFACTORY.ALO" + "context": [], + "asset": "CIN_p_proton_torpedo.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "asset": "p_steam_small" + "message": "Unable to find .ALO file \u0027CIN_Fire_Huge.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Fire_Huge.alo" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", - "AET.ModVerify.Verifiers.Commons.TextureVeifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Could not find texture \u0027p_particle_master\u0027 for context: [P_DIRT_EMITTER_TEST1.ALO].", + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_04_STATION_D.ALO\u0027", "severity": "Error", "context": [ - "P_DIRT_EMITTER_TEST1.ALO" + "DATA\\ART\\MODELS\\UB_04_STATION_D.ALO" ], - "asset": "p_particle_master" + "asset": "p_uwstation_death" }, { "id": "FILE00", @@ -320,12 +332,23 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_smoke_small_thin4\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_PRISON.ALO\u0027", + "message": "Unable to find .ALO file \u0027RV_nebulonb_D_death_00.ALO\u0027", + "severity": "Error", + "context": [], + "asset": "RV_nebulonb_D_death_00.ALO" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_02_STATION_D.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\NB_PRISON.ALO" + "DATA\\ART\\MODELS\\UB_02_STATION_D.ALO" ], - "asset": "p_smoke_small_thin4" + "asset": "p_uwstation_death" }, { "id": "FILE00", @@ -333,10 +356,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_EI_Vader.alo\u0027", + "message": "Unable to find .ALO file \u0027W_Kamino_Reflect.ALO\u0027", "severity": "Error", "context": [], - "asset": "Cin_EI_Vader.alo" + "asset": "W_Kamino_Reflect.ALO" }, { "id": "FILE00", @@ -344,10 +367,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027DATA\\ART\\MODELS\\RB_HYPERVELOCITYGUN.ALO\u0027", + "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_MONCAL_BUILDING.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\RB_HYPERVELOCITYGUN.ALO" + "DATA\\ART\\MODELS\\NB_MONCAL_BUILDING.ALO" ], "asset": "p_smoke_small_thin2" }, @@ -357,12 +380,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\EV_TIE_LANCET.ALO\u0027.", + "message": "Proxy particle \u0027p_steam_small\u0027 not found for model \u0027DATA\\ART\\MODELS\\RB_HEAVYVEHICLEFACTORY.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\EV_TIE_LANCET.ALO" + "DATA\\ART\\MODELS\\RB_HEAVYVEHICLEFACTORY.ALO" ], - "asset": "Default.fx" + "asset": "p_steam_small" }, { "id": "FILE00", @@ -370,10 +393,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_DeathStar_Wall.alo\u0027", + "message": "Unable to find .ALO file \u0027Cin_EV_Stardestroyer_Warp.alo\u0027", "severity": "Error", "context": [], - "asset": "Cin_DeathStar_Wall.alo" + "asset": "Cin_EV_Stardestroyer_Warp.alo" }, { "id": "FILE00", @@ -381,10 +404,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027W_droid_steam.alo\u0027", + "message": "Unable to find .ALO file \u0027Cin_DStar_TurretLasers.alo\u0027", "severity": "Error", "context": [], - "asset": "W_droid_steam.alo" + "asset": "Cin_DStar_TurretLasers.alo" }, { "id": "FILE00", @@ -392,10 +415,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_DeathStar_High.alo\u0027", + "message": "Unable to find .ALO file \u0027CIN_Rbel_GreyGroup.alo\u0027", "severity": "Error", "context": [], - "asset": "Cin_DeathStar_High.alo" + "asset": "CIN_Rbel_GreyGroup.alo" }, { "id": "FILE00", @@ -403,10 +426,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027MODELS\u0027", + "message": "Unable to find .ALO file \u0027Cin_Planet_Hoth_High.alo\u0027", "severity": "Error", "context": [], - "asset": "MODELS" + "asset": "Cin_Planet_Hoth_High.alo" }, { "id": "FILE00", @@ -414,10 +437,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027W_AllShaders.ALO\u0027", + "message": "Unable to find .ALO file \u0027CIN_Trooper_Row.alo\u0027", "severity": "Error", "context": [], - "asset": "W_AllShaders.ALO" + "asset": "CIN_Trooper_Row.alo" }, { "id": "FILE00", @@ -425,12 +448,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_bomb_spin\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_THERMAL_DETONATOR_EMPIRE.ALO\u0027", + "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\UI_SABOTEUR.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\W_THERMAL_DETONATOR_EMPIRE.ALO" + "DATA\\ART\\MODELS\\UI_SABOTEUR.ALO" ], - "asset": "p_bomb_spin" + "asset": "p_desert_ground_dust" }, { "id": "FILE00", @@ -438,12 +461,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_03_STATION_D.ALO\u0027", + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_CINE_LUA.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\UB_03_STATION_D.ALO" + "DATA\\ART\\MODELS\\W_STARS_CINE_LUA.ALO" ], - "asset": "p_uwstation_death" + "asset": "Lensflare0" }, { "id": "FILE00", @@ -451,10 +474,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_Rbel_GreyGroup.alo\u0027", + "message": "Unable to find .ALO file \u0027W_AllShaders.ALO\u0027", "severity": "Error", "context": [], - "asset": "CIN_Rbel_GreyGroup.alo" + "asset": "W_AllShaders.ALO" }, { "id": "FILE00", @@ -462,12 +485,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_cold_tiny01\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_SCH.ALO\u0027", + "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\EV_TIE_LANCET.ALO\u0027.", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\NB_SCH.ALO" + "DATA\\ART\\MODELS\\EV_TIE_LANCET.ALO" ], - "asset": "p_cold_tiny01" + "asset": "Default.fx" }, { "id": "FILE00", @@ -486,34 +509,38 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_hp_archammer-damage\u0027 not found for model \u0027DATA\\ART\\MODELS\\EV_ARCHAMMER.ALO\u0027", + "message": "Unable to find .ALO file \u0027Cin_EI_Palpatine.alo\u0027", "severity": "Error", - "context": [ - "DATA\\ART\\MODELS\\EV_ARCHAMMER.ALO" - ], - "asset": "p_hp_archammer-damage" - }, + "context": [], + "asset": "Cin_EI_Palpatine.alo" + }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" ], - "message": "Unable to find .ALO file \u0027CIN_Rbel_grey.alo\u0027", + "message": "Could not find texture \u0027Cin_DeathStar.tga\u0027 for context: [ALTTEST.ALO].", "severity": "Error", - "context": [], - "asset": "CIN_Rbel_grey.alo" + "context": [ + "ALTTEST.ALO" + ], + "asset": "Cin_DeathStar.tga" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" ], - "message": "Unable to find .ALO file \u0027CIN_Reb_CelebHall.alo\u0027", + "message": "Could not find texture \u0027UB_girder_B.tga\u0027 for context: [UV_MDU_CAGE.ALO].", "severity": "Error", - "context": [], - "asset": "CIN_Reb_CelebHall.alo" + "context": [ + "UV_MDU_CAGE.ALO" + ], + "asset": "UB_girder_B.tga" }, { "id": "FILE00", @@ -534,10 +561,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_ImperialCraft.alo\u0027", + "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\EV_MDU_SENSORNODE.ALO\u0027.", "severity": "Error", - "context": [], - "asset": "Cin_ImperialCraft.alo" + "context": [ + "DATA\\ART\\MODELS\\EV_MDU_SENSORNODE.ALO" + ], + "asset": "Default.fx" }, { "id": "FILE00", @@ -545,10 +574,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_DStar_Dish_close.alo\u0027", + "message": "Unable to find .ALO file \u0027Cin_Planet_Alderaan_High.alo\u0027", "severity": "Error", "context": [], - "asset": "Cin_DStar_Dish_close.alo" + "asset": "Cin_Planet_Alderaan_High.alo" }, { "id": "FILE00", @@ -556,12 +585,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027pe_bwing_yellow\u0027 not found for model \u0027DATA\\ART\\MODELS\\RV_BWING.ALO\u0027", + "message": "Proxy particle \u0027p_hp_archammer-damage\u0027 not found for model \u0027DATA\\ART\\MODELS\\EV_ARCHAMMER.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\RV_BWING.ALO" + "DATA\\ART\\MODELS\\EV_ARCHAMMER.ALO" ], - "asset": "pe_bwing_yellow" + "asset": "p_hp_archammer-damage" }, { "id": "FILE00", @@ -569,10 +598,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_bridge.alo\u0027", + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_05_STATION_D.ALO\u0027", "severity": "Error", - "context": [], - "asset": "Cin_bridge.alo" + "context": [ + "DATA\\ART\\MODELS\\UB_05_STATION_D.ALO" + ], + "asset": "p_uwstation_death" }, { "id": "FILE00", @@ -580,12 +611,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\UI_SABOTEUR.ALO\u0027", + "message": "Proxy particle \u0027p_explosion_smoke_small_thin5\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_NOGHRI_HUT.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\UI_SABOTEUR.ALO" + "DATA\\ART\\MODELS\\NB_NOGHRI_HUT.ALO" ], - "asset": "p_desert_ground_dust" + "asset": "p_explosion_smoke_small_thin5" }, { "id": "FILE00", @@ -593,10 +624,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_Trooper_Row.alo\u0027", + "message": "Unable to find .ALO file \u0027CIN_Probe_Droid.alo\u0027", "severity": "Error", "context": [], - "asset": "CIN_Trooper_Row.alo" + "asset": "CIN_Probe_Droid.alo" }, { "id": "FILE00", @@ -604,10 +635,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_EV_TieAdvanced.alo\u0027", + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_HIGH.ALO\u0027", "severity": "Error", - "context": [], - "asset": "Cin_EV_TieAdvanced.alo" + "context": [ + "DATA\\ART\\MODELS\\W_STARS_HIGH.ALO" + ], + "asset": "Lensflare0" }, { "id": "FILE00", @@ -615,38 +648,58 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027w_sith_arch.alo\u0027", + "message": "Unable to find .ALO file \u0027W_Volcano_Rock02.ALO\u0027", "severity": "Error", "context": [], - "asset": "w_sith_arch.alo" + "asset": "W_Volcano_Rock02.ALO" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", - "AET.ModVerify.Verifiers.Commons.TextureVeifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Could not find texture \u0027NB_YsalamiriTree_B.tga\u0027 for context: [UV_MDU_CAGE.ALO].", + "message": "Proxy particle \u0027lookat\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_ECLIPSE.ALO\u0027", "severity": "Error", "context": [ - "UV_MDU_CAGE.ALO" + "DATA\\ART\\MODELS\\UV_ECLIPSE.ALO" ], - "asset": "NB_YsalamiriTree_B.tga" + "asset": "lookat" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", - "AET.ModVerify.Verifiers.Commons.TextureVeifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Could not find texture \u0027W_TE_Rock_f_02_b.tga\u0027 for context: [EV_TIE_PHANTOM.ALO].", + "message": "Unable to find .ALO file \u0027Cin_Shuttle_Tyderium.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_Shuttle_Tyderium.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027W_SwampGasEmit.ALO\u0027", + "severity": "Error", + "context": [], + "asset": "W_SwampGasEmit.ALO" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027P_heat_small01\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_VCH.ALO\u0027", "severity": "Error", "context": [ - "EV_TIE_PHANTOM.ALO" + "DATA\\ART\\MODELS\\NB_VCH.ALO" ], - "asset": "W_TE_Rock_f_02_b.tga" + "asset": "P_heat_small01" }, { "id": "FILE00", @@ -654,10 +707,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_EI_Palpatine.alo\u0027", + "message": "Unable to find .ALO file \u0027CIN_Rbel_NavyRow.alo\u0027", "severity": "Error", "context": [], - "asset": "Cin_EI_Palpatine.alo" + "asset": "CIN_Rbel_NavyRow.alo" }, { "id": "FILE00", @@ -665,10 +718,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_Rbel_Soldier.alo\u0027", + "message": "Unable to find .ALO file \u0027CIN_Fire_Medium.alo\u0027", "severity": "Error", "context": [], - "asset": "CIN_Rbel_Soldier.alo" + "asset": "CIN_Fire_Medium.alo" }, { "id": "FILE00", @@ -676,12 +729,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_MONCAL_BUILDING.ALO\u0027", + "message": "Proxy particle \u0027p_ewok_drag_dirt\u0027 not found for model \u0027DATA\\ART\\MODELS\\UI_EWOK_HANDLER.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\NB_MONCAL_BUILDING.ALO" + "DATA\\ART\\MODELS\\UI_EWOK_HANDLER.ALO" ], - "asset": "p_smoke_small_thin2" + "asset": "p_ewok_drag_dirt" }, { "id": "FILE00", @@ -700,10 +753,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_Officer_Row.alo\u0027", + "message": "Unable to find .ALO file \u0027W_droid_steam.alo\u0027", "severity": "Error", "context": [], - "asset": "CIN_Officer_Row.alo" + "asset": "W_droid_steam.alo" }, { "id": "FILE00", @@ -711,26 +764,21 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_HIGH.ALO\u0027", + "message": "Unable to find .ALO file \u0027CIN_Biker_Row.alo\u0027", "severity": "Error", - "context": [ - "DATA\\ART\\MODELS\\W_STARS_HIGH.ALO" - ], - "asset": "Lensflare0" + "context": [], + "asset": "CIN_Biker_Row.alo" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", - "AET.ModVerify.Verifiers.Commons.TextureVeifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall_B.tga\u0027 for context: [W_SITH_LEFTHALL.ALO].", + "message": "Unable to find .ALO file \u0027w_planet_volcanic.alo\u0027", "severity": "Error", - "context": [ - "W_SITH_LEFTHALL.ALO" - ], - "asset": "Cin_Reb_CelebHall_Wall_B.tga" + "context": [], + "asset": "w_planet_volcanic.alo" }, { "id": "FILE00", @@ -738,10 +786,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_Coruscant.alo\u0027", + "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027DATA\\ART\\MODELS\\RB_HYPERVELOCITYGUN.ALO\u0027", "severity": "Error", - "context": [], - "asset": "Cin_Coruscant.alo" + "context": [ + "DATA\\ART\\MODELS\\RB_HYPERVELOCITYGUN.ALO" + ], + "asset": "p_smoke_small_thin2" }, { "id": "FILE00", @@ -749,12 +799,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_ewok_drag_dirt\u0027 not found for model \u0027DATA\\ART\\MODELS\\UI_EWOK_HANDLER.ALO\u0027", + "message": "Proxy particle \u0027lookat\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_ECLIPSE_UC.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\UI_EWOK_HANDLER.ALO" + "DATA\\ART\\MODELS\\UV_ECLIPSE_UC.ALO" ], - "asset": "p_ewok_drag_dirt" + "asset": "lookat" }, { "id": "FILE00", @@ -762,12 +812,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027P_heat_small01\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_VCH.ALO\u0027", + "message": "Unable to find .ALO file \u0027CIN_REb_CelebCharacters.alo\u0027", "severity": "Error", - "context": [ - "DATA\\ART\\MODELS\\NB_VCH.ALO" - ], - "asset": "P_heat_small01" + "context": [], + "asset": "CIN_REb_CelebCharacters.alo" }, { "id": "FILE00", @@ -775,36 +823,38 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027W_Vol_Steam01.ALO\u0027", + "message": "Unable to find .ALO file \u0027Cin_DeathStar_High.alo\u0027", "severity": "Error", "context": [], - "asset": "W_Vol_Steam01.ALO" + "asset": "Cin_DeathStar_High.alo" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" ], - "message": "Proxy particle \u0027p_explosion_smoke_small_thin5\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_NOGHRI_HUT.ALO\u0027", + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall_B.tga\u0027 for context: [W_SITH_LEFTHALL.ALO].", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\NB_NOGHRI_HUT.ALO" + "W_SITH_LEFTHALL.ALO" ], - "asset": "p_explosion_smoke_small_thin5" + "asset": "Cin_Reb_CelebHall_Wall_B.tga" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" ], - "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_MEDIUM.ALO\u0027", + "message": "Could not find texture \u0027NB_YsalamiriTree_B.tga\u0027 for context: [UV_MDU_CAGE.ALO].", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\W_STARS_MEDIUM.ALO" + "UV_MDU_CAGE.ALO" ], - "asset": "Lensflare0" + "asset": "NB_YsalamiriTree_B.tga" }, { "id": "FILE00", @@ -812,12 +862,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_02_STATION_D.ALO\u0027", + "message": "Unable to find .ALO file \u0027Cin_Coruscant.alo\u0027", "severity": "Error", - "context": [ - "DATA\\ART\\MODELS\\UB_02_STATION_D.ALO" - ], - "asset": "p_uwstation_death" + "context": [], + "asset": "Cin_Coruscant.alo" }, { "id": "FILE00", @@ -825,10 +873,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_Officer.alo\u0027", + "message": "Proxy particle \u0027p_prison_light\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_PRISON.ALO\u0027", "severity": "Error", - "context": [], - "asset": "Cin_Officer.alo" + "context": [ + "DATA\\ART\\MODELS\\NB_PRISON.ALO" + ], + "asset": "p_prison_light" }, { "id": "FILE00", @@ -836,12 +886,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_04_STATION_D.ALO\u0027", + "message": "Proxy particle \u0027p_cold_tiny01\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_SCH.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\UB_04_STATION_D.ALO" + "DATA\\ART\\MODELS\\NB_SCH.ALO" ], - "asset": "p_uwstation_death" + "asset": "p_cold_tiny01" }, { "id": "FILE00", @@ -849,10 +899,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_Lambda_Head.alo\u0027", + "message": "Unable to find .ALO file \u0027CIN_NavyTrooper_Row.alo\u0027", "severity": "Error", "context": [], - "asset": "CIN_Lambda_Head.alo" + "asset": "CIN_NavyTrooper_Row.alo" }, { "id": "FILE00", @@ -860,10 +910,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_Biker_Row.alo\u0027", + "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_CRUSADERCLASSCORVETTE.ALO\u0027.", "severity": "Error", - "context": [], - "asset": "CIN_Biker_Row.alo" + "context": [ + "DATA\\ART\\MODELS\\UV_CRUSADERCLASSCORVETTE.ALO" + ], + "asset": "Default.fx" }, { "id": "FILE00", @@ -871,10 +923,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_DStar_protons.alo\u0027", + "message": "Unable to find .ALO file \u0027CIN_Rbel_Soldier.alo\u0027", "severity": "Error", "context": [], - "asset": "Cin_DStar_protons.alo" + "asset": "CIN_Rbel_Soldier.alo" }, { "id": "FILE00", @@ -882,10 +934,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\UI_IG88.ALO\u0027", + "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\RI_KYLEKATARN.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\UI_IG88.ALO" + "DATA\\ART\\MODELS\\RI_KYLEKATARN.ALO" ], "asset": "p_desert_ground_dust" }, @@ -904,12 +956,15 @@ "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" ], - "message": "Unable to find .ALO file \u0027Cin_Planet_Hoth_High.alo\u0027", + "message": "Could not find texture \u0027p_particle_master\u0027 for context: [P_DIRT_EMITTER_TEST1.ALO].", "severity": "Error", - "context": [], - "asset": "Cin_Planet_Hoth_High.alo" + "context": [ + "P_DIRT_EMITTER_TEST1.ALO" + ], + "asset": "p_particle_master" }, { "id": "FILE00", @@ -917,12 +972,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_LOW.ALO\u0027", + "message": "Unable to find .ALO file \u0027Cin_bridge.alo\u0027", "severity": "Error", - "context": [ - "DATA\\ART\\MODELS\\W_STARS_LOW.ALO" - ], - "asset": "Lensflare0" + "context": [], + "asset": "Cin_bridge.alo" }, { "id": "FILE00", @@ -930,10 +983,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_NavyTrooper_Row.alo\u0027", + "message": "Unable to find .ALO file \u0027W_Vol_Steam01.ALO\u0027", "severity": "Error", "context": [], - "asset": "CIN_NavyTrooper_Row.alo" + "asset": "W_Vol_Steam01.ALO" }, { "id": "FILE00", @@ -941,10 +994,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_DStar_TurretLasers.alo\u0027", + "message": "Unable to find .ALO file \u0027CIN_Rbel_grey.alo\u0027", "severity": "Error", "context": [], - "asset": "Cin_DStar_TurretLasers.alo" + "asset": "CIN_Rbel_grey.alo" }, { "id": "FILE00", @@ -952,10 +1005,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027W_SwampGasEmit.ALO\u0027", + "message": "Unable to find .ALO file \u0027w_sith_arch.alo\u0027", "severity": "Error", "context": [], - "asset": "W_SwampGasEmit.ALO" + "asset": "w_sith_arch.alo" }, { "id": "FILE00", @@ -963,10 +1016,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_Shuttle_Tyderium.alo\u0027", + "message": "Unable to find .ALO file \u0027Cin_rv_XWingProp.alo\u0027", "severity": "Error", "context": [], - "asset": "Cin_Shuttle_Tyderium.alo" + "asset": "Cin_rv_XWingProp.alo" }, { "id": "FILE00", @@ -974,23 +1027,24 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_EV_Stardestroyer_Warp.alo\u0027", + "message": "Unable to find .ALO file \u0027Cin_DStar_Dish_close.alo\u0027", "severity": "Error", "context": [], - "asset": "Cin_EV_Stardestroyer_Warp.alo" + "asset": "Cin_DStar_Dish_close.alo" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" ], - "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_CINE_LUA.ALO\u0027", + "message": "Could not find texture \u0027W_TE_Rock_f_02_b.tga\u0027 for context: [EV_TIE_PHANTOM.ALO].", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\W_STARS_CINE_LUA.ALO" + "EV_TIE_PHANTOM.ALO" ], - "asset": "Lensflare0" + "asset": "W_TE_Rock_f_02_b.tga" }, { "id": "FILE00", @@ -998,10 +1052,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_DeathStar_Hangar.alo\u0027", + "message": "Proxy particle \u0027pe_bwing_yellow\u0027 not found for model \u0027DATA\\ART\\MODELS\\RV_BWING.ALO\u0027", "severity": "Error", - "context": [], - "asset": "CIN_DeathStar_Hangar.alo" + "context": [ + "DATA\\ART\\MODELS\\RV_BWING.ALO" + ], + "asset": "pe_bwing_yellow" }, { "id": "FILE00", @@ -1009,10 +1065,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_Fire_Medium.alo\u0027", + "message": "Unable to find .ALO file \u0027CIN_Lambda_Head.alo\u0027", "severity": "Error", "context": [], - "asset": "CIN_Fire_Medium.alo" + "asset": "CIN_Lambda_Head.alo" }, { "id": "FILE00", @@ -1020,12 +1076,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_CINE.ALO\u0027", + "message": "Proxy particle \u0027p_explosion_small_delay00\u0027 not found for model \u0027DATA\\ART\\MODELS\\EB_COMMANDCENTER.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\W_STARS_CINE.ALO" + "DATA\\ART\\MODELS\\EB_COMMANDCENTER.ALO" ], - "asset": "Lensflare0" + "asset": "p_explosion_small_delay00" }, { "id": "FILE00", @@ -1033,10 +1089,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_Rbel_Soldier_Group.alo\u0027", + "message": "Unable to find .ALO file \u0027Cin_DStar_LeverPanel.alo\u0027", "severity": "Error", "context": [], - "asset": "CIN_Rbel_Soldier_Group.alo" + "asset": "Cin_DStar_LeverPanel.alo" }, { "id": "FILE00", @@ -1044,10 +1100,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027RV_nebulonb_D_death_00.ALO\u0027", + "message": "Unable to find .ALO file \u0027p_splash_wake_lava.alo\u0027", "severity": "Error", "context": [], - "asset": "RV_nebulonb_D_death_00.ALO" + "asset": "p_splash_wake_lava.alo" }, { "id": "FILE00", @@ -1055,10 +1111,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027W_Volcano_Rock02.ALO\u0027", + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_LOW.ALO\u0027", "severity": "Error", - "context": [], - "asset": "W_Volcano_Rock02.ALO" + "context": [ + "DATA\\ART\\MODELS\\W_STARS_LOW.ALO" + ], + "asset": "Lensflare0" }, { "id": "FILE00", @@ -1066,10 +1124,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027w_planet_volcanic.alo\u0027", + "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\EI_MARAJADE.ALO\u0027", "severity": "Error", - "context": [], - "asset": "w_planet_volcanic.alo" + "context": [ + "DATA\\ART\\MODELS\\EI_MARAJADE.ALO" + ], + "asset": "p_desert_ground_dust" }, { "id": "FILE00", @@ -1077,10 +1137,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_REb_CelebCharacters.alo\u0027", + "message": "Proxy particle \u0027p_bomb_spin\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_THERMAL_DETONATOR_EMPIRE.ALO\u0027", "severity": "Error", - "context": [], - "asset": "CIN_REb_CelebCharacters.alo" + "context": [ + "DATA\\ART\\MODELS\\W_THERMAL_DETONATOR_EMPIRE.ALO" + ], + "asset": "p_bomb_spin" }, { "id": "FILE00", @@ -1088,257 +1150,249 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_CRUSADERCLASSCORVETTE.ALO\u0027.", + "message": "Unable to find .ALO file \u0027Cin_EV_TieAdvanced.alo\u0027", "severity": "Error", - "context": [ - "DATA\\ART\\MODELS\\UV_CRUSADERCLASSCORVETTE.ALO" - ], - "asset": "Default.fx" + "context": [], + "asset": "Cin_EV_TieAdvanced.alo" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", - "AET.ModVerify.Verifiers.Commons.TextureVeifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall.tga\u0027 for context: [W_SITH_LEFTHALL.ALO].", + "message": "Unable to find .ALO file \u0027CIN_Rbel_Soldier_Group.alo\u0027", "severity": "Error", - "context": [ - "W_SITH_LEFTHALL.ALO" - ], - "asset": "Cin_Reb_CelebHall_Wall.tga" + "context": [], + "asset": "CIN_Rbel_Soldier_Group.alo" }, { "id": "FILE00", "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_EV_lambdaShuttle_150.alo\u0027", + "message": "Audio file \u0027U000_LEI0213_ENG.WAV\u0027 could not be found.", "severity": "Error", - "context": [], - "asset": "Cin_EV_lambdaShuttle_150.alo" + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0213_ENG.WAV" }, { "id": "FILE00", "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Proxy particle \u0027p_explosion_small_delay00\u0027 not found for model \u0027DATA\\ART\\MODELS\\EB_COMMANDCENTER.ALO\u0027", + "message": "Audio file \u0027U000_LEI0113_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\EB_COMMANDCENTER.ALO" + "Unit_Select_Leia" ], - "asset": "p_explosion_small_delay00" + "asset": "U000_LEI0113_ENG.WAV" }, { "id": "FILE00", "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", - "AET.ModVerify.Verifiers.Commons.TextureVeifier" + "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Could not find texture \u0027UB_girder_B.tga\u0027 for context: [UV_MDU_CAGE.ALO].", + "message": "Audio file \u0027U000_LEI0603_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "UV_MDU_CAGE.ALO" + "Unit_Increase_Production_Leia" ], - "asset": "UB_girder_B.tga" + "asset": "U000_LEI0603_ENG.WAV" }, { "id": "FILE00", "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_01_STATION_D.ALO\u0027", + "message": "Audio file \u0027U000_LEI0309_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\UB_01_STATION_D.ALO" + "Unit_Attack_Leia" ], - "asset": "p_uwstation_death" + "asset": "U000_LEI0309_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0206_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0212_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ "Unit_Move_Leia" ], - "asset": "U000_LEI0206_ENG.WAV" + "asset": "U000_LEI0212_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0204_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_MAL0503_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Move_Leia" + "Unit_Assist_Move_Missile_Launcher" ], - "asset": "U000_LEI0204_ENG.WAV" + "asset": "U000_MAL0503_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0102_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_MCF1601_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Select_Leia" + "Unit_StarDest_MC30_Frigate" ], - "asset": "U000_LEI0102_ENG.WAV" + "asset": "U000_MCF1601_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0215_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0111_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Move_Leia" + "Unit_Select_Leia" ], - "asset": "U000_LEI0215_ENG.WAV" + "asset": "U000_LEI0111_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0107_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_ARC3106_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Select_Leia" + "Unit_Complete_Troops_Arc_Hammer" ], - "asset": "U000_LEI0107_ENG.WAV" + "asset": "U000_ARC3106_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0504_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0303_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Remove_Corruption_Leia" + "Unit_Attack_Leia" ], - "asset": "U000_LEI0504_ENG.WAV" + "asset": "U000_LEI0303_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027AMB_DES_CLEAR_LOOP_1.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0404_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Weather_Ambient_Clear_Sandstorm_Loop" + "Unit_Guard_Leia" ], - "asset": "AMB_DES_CLEAR_LOOP_1.WAV" + "asset": "U000_LEI0404_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0105_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027TESTUNITMOVE_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Select_Leia" + "Unit_Move_Gneneric_Test" ], - "asset": "U000_LEI0105_ENG.WAV" + "asset": "TESTUNITMOVE_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0213_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0401_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Move_Leia" + "Unit_Guard_Leia" ], - "asset": "U000_LEI0213_ENG.WAV" + "asset": "U000_LEI0401_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0201_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027EGL_STAR_VIPER_SPINNING_1.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Move_Leia" + "Unit_Star_Viper_Spinning_By" ], - "asset": "U000_LEI0201_ENG.WAV" + "asset": "EGL_STAR_VIPER_SPINNING_1.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0303_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_TMC0212_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Attack_Leia" + "Unit_Move_Tie_Mauler" ], - "asset": "U000_LEI0303_ENG.WAV" + "asset": "U000_TMC0212_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0103_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0110_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ "Unit_Select_Leia" ], - "asset": "U000_LEI0103_ENG.WAV" + "asset": "U000_LEI0110_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0207_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0314_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Move_Leia" + "Unit_Attack_Leia" ], - "asset": "U000_LEI0207_ENG.WAV" + "asset": "U000_LEI0314_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_DEF3006_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0305_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Corrupt_Sabateur" + "Unit_Attack_Leia" ], - "asset": "U000_DEF3006_ENG.WAV" + "asset": "U000_LEI0305_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0309_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0112_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Attack_Leia" + "Unit_Select_Leia" ], - "asset": "U000_LEI0309_ENG.WAV" + "asset": "U000_LEI0112_ENG.WAV" }, { "id": "FILE00", @@ -1357,120 +1411,120 @@ "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_DEF3106_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0211_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Weaken_Sabateur" + "Unit_Move_Leia" ], - "asset": "U000_DEF3106_ENG.WAV" + "asset": "U000_LEI0211_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0503_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0205_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Remove_Corruption_Leia" + "Unit_Move_Leia" ], - "asset": "U000_LEI0503_ENG.WAV" + "asset": "U000_LEI0205_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0502_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0115_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Remove_Corruption_Leia" + "Unit_Select_Leia" ], - "asset": "U000_LEI0502_ENG.WAV" + "asset": "U000_LEI0115_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0212_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0604_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Move_Leia" + "Unit_Increase_Production_Leia" ], - "asset": "U000_LEI0212_ENG.WAV" + "asset": "U000_LEI0604_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027AMB_URB_CLEAR_LOOP_1.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0602_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Weather_Ambient_Clear_Urban_Loop" + "Unit_Increase_Production_Leia" ], - "asset": "AMB_URB_CLEAR_LOOP_1.WAV" + "asset": "U000_LEI0602_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0311_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0315_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ "Unit_Attack_Leia" ], - "asset": "U000_LEI0311_ENG.WAV" + "asset": "U000_LEI0315_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0115_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_DEF3006_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Select_Leia" + "Unit_Corrupt_Sabateur" ], - "asset": "U000_LEI0115_ENG.WAV" + "asset": "U000_DEF3006_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0101_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0210_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Select_Leia" + "Unit_Move_Leia" ], - "asset": "U000_LEI0101_ENG.WAV" + "asset": "U000_LEI0210_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0401_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0105_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Guard_Leia" + "Unit_Select_Leia" ], - "asset": "U000_LEI0401_ENG.WAV" + "asset": "U000_LEI0105_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0315_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0208_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Attack_Leia" + "Unit_Move_Leia" ], - "asset": "U000_LEI0315_ENG.WAV" + "asset": "U000_LEI0208_ENG.WAV" }, { "id": "FILE00", @@ -1489,593 +1543,1505 @@ "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0603_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0202_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Increase_Production_Leia" + "Unit_Move_Leia" ], - "asset": "U000_LEI0603_ENG.WAV" + "asset": "U000_LEI0202_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0104_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0306_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Select_Leia" + "Unit_Attack_Leia" ], - "asset": "U000_LEI0104_ENG.WAV" + "asset": "U000_LEI0306_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0501_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0101_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Remove_Corruption_Leia" + "Unit_Select_Leia" ], - "asset": "U000_LEI0501_ENG.WAV" + "asset": "U000_LEI0101_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027TESTUNITMOVE_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027C000_DST0102_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Move_Gneneric_Test" + "EHD_Death_Star_Activate" ], - "asset": "TESTUNITMOVE_ENG.WAV" + "asset": "C000_DST0102_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0108_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0103_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ "Unit_Select_Leia" ], - "asset": "U000_LEI0108_ENG.WAV" + "asset": "U000_LEI0103_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_MCF1601_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0403_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_StarDest_MC30_Frigate" + "Unit_Guard_Leia" ], - "asset": "U000_MCF1601_ENG.WAV" + "asset": "U000_LEI0403_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0111_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027FS_BEETLE_2.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Select_Leia" + "SFX_Anim_Beetle_Footsteps" ], - "asset": "U000_LEI0111_ENG.WAV" + "asset": "FS_BEETLE_2.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0211_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0201_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ "Unit_Move_Leia" ], - "asset": "U000_LEI0211_ENG.WAV" + "asset": "U000_LEI0201_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0110_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0203_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Select_Leia" + "Unit_Move_Leia" ], - "asset": "U000_LEI0110_ENG.WAV" + "asset": "U000_LEI0203_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0403_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0114_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Guard_Leia" + "Unit_Select_Leia" ], - "asset": "U000_LEI0403_ENG.WAV" + "asset": "U000_LEI0114_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0306_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027FS_BEETLE_1.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Attack_Leia" + "SFX_Anim_Beetle_Footsteps" ], - "asset": "U000_LEI0306_ENG.WAV" + "asset": "FS_BEETLE_1.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0308_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0304_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ "Unit_Attack_Leia" ], - "asset": "U000_LEI0308_ENG.WAV" + "asset": "U000_LEI0304_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0112_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0301_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Select_Leia" + "Unit_Attack_Leia" ], - "asset": "U000_LEI0112_ENG.WAV" + "asset": "U000_LEI0301_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0301_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0503_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Attack_Leia" + "Unit_Remove_Corruption_Leia" ], - "asset": "U000_LEI0301_ENG.WAV" + "asset": "U000_LEI0503_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0404_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0109_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Guard_Leia" + "Unit_Select_Leia" ], - "asset": "U000_LEI0404_ENG.WAV" + "asset": "U000_LEI0109_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_TMC0212_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0308_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Move_Tie_Mauler" + "Unit_Attack_Leia" ], - "asset": "U000_TMC0212_ENG.WAV" + "asset": "U000_LEI0308_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027EGL_STAR_VIPER_SPINNING_1.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0402_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Star_Viper_Spinning_By" + "Unit_Guard_Leia" ], - "asset": "EGL_STAR_VIPER_SPINNING_1.WAV" + "asset": "U000_LEI0402_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0208_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0108_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Move_Leia" + "Unit_Select_Leia" ], - "asset": "U000_LEI0208_ENG.WAV" + "asset": "U000_LEI0108_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0604_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0307_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Increase_Production_Leia" + "Unit_Attack_Leia" ], - "asset": "U000_LEI0604_ENG.WAV" + "asset": "U000_LEI0307_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027FS_BEETLE_3.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0311_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "SFX_Anim_Beetle_Footsteps" + "Unit_Attack_Leia" ], - "asset": "FS_BEETLE_3.WAV" + "asset": "U000_LEI0311_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0109_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0102_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ "Unit_Select_Leia" ], - "asset": "U000_LEI0109_ENG.WAV" + "asset": "U000_LEI0102_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0202_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0104_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Move_Leia" + "Unit_Select_Leia" ], - "asset": "U000_LEI0202_ENG.WAV" + "asset": "U000_LEI0104_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0602_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027FS_BEETLE_3.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Increase_Production_Leia" + "SFX_Anim_Beetle_Footsteps" ], - "asset": "U000_LEI0602_ENG.WAV" + "asset": "FS_BEETLE_3.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0305_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0313_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ "Unit_Attack_Leia" ], - "asset": "U000_LEI0305_ENG.WAV" + "asset": "U000_LEI0313_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_MAL0503_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0206_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Assist_Move_Missile_Launcher" + "Unit_Move_Leia" ], - "asset": "U000_MAL0503_ENG.WAV" + "asset": "U000_LEI0206_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0601_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_ARC3104_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Increase_Production_Leia" + "Unit_Produce_Troops_Arc_Hammer" ], - "asset": "U000_LEI0601_ENG.WAV" + "asset": "U000_ARC3104_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_ARC3106_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0312_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Complete_Troops_Arc_Hammer" + "Unit_Attack_Leia" ], - "asset": "U000_ARC3106_ENG.WAV" + "asset": "U000_LEI0312_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027FS_BEETLE_4.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0215_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "SFX_Anim_Beetle_Footsteps" + "Unit_Move_Leia" ], - "asset": "FS_BEETLE_4.WAV" + "asset": "U000_LEI0215_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027FS_BEETLE_1.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0107_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "SFX_Anim_Beetle_Footsteps" + "Unit_Select_Leia" ], - "asset": "FS_BEETLE_1.WAV" + "asset": "U000_LEI0107_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0205_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0501_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Move_Leia" + "Unit_Remove_Corruption_Leia" ], - "asset": "U000_LEI0205_ENG.WAV" + "asset": "U000_LEI0501_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0113_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027AMB_DES_CLEAR_LOOP_1.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Select_Leia" + "Weather_Ambient_Clear_Sandstorm_Loop" ], - "asset": "U000_LEI0113_ENG.WAV" + "asset": "AMB_DES_CLEAR_LOOP_1.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0314_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0504_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Attack_Leia" + "Unit_Remove_Corruption_Leia" ], - "asset": "U000_LEI0314_ENG.WAV" + "asset": "U000_LEI0504_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0304_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0502_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Attack_Leia" + "Unit_Remove_Corruption_Leia" ], - "asset": "U000_LEI0304_ENG.WAV" + "asset": "U000_LEI0502_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0203_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_DEF3106_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Move_Leia" + "Unit_Weaken_Sabateur" ], - "asset": "U000_LEI0203_ENG.WAV" + "asset": "U000_DEF3106_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027C000_DST0102_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_ARC3105_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "EHD_Death_Star_Activate" + "Unit_Complete_Troops_Arc_Hammer" ], - "asset": "C000_DST0102_ENG.WAV" + "asset": "U000_ARC3105_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0114_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0601_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Select_Leia" + "Unit_Increase_Production_Leia" ], - "asset": "U000_LEI0114_ENG.WAV" + "asset": "U000_LEI0601_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_ARC3104_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0204_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Produce_Troops_Arc_Hammer" + "Unit_Move_Leia" ], - "asset": "U000_ARC3104_ENG.WAV" + "asset": "U000_LEI0204_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027FS_BEETLE_2.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0207_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "SFX_Anim_Beetle_Footsteps" + "Unit_Move_Leia" ], - "asset": "FS_BEETLE_2.WAV" + "asset": "U000_LEI0207_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_ARC3105_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027FS_BEETLE_4.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Complete_Troops_Arc_Hammer" + "SFX_Anim_Beetle_Footsteps" ], - "asset": "U000_ARC3105_ENG.WAV" + "asset": "FS_BEETLE_4.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0307_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027AMB_URB_CLEAR_LOOP_1.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Attack_Leia" + "Weather_Ambient_Clear_Urban_Loop" ], - "asset": "U000_LEI0307_ENG.WAV" + "asset": "AMB_URB_CLEAR_LOOP_1.WAV" }, { "id": "FILE00", "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" + "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" ], - "message": "Audio file \u0027U000_LEI0402_ENG.WAV\u0027 could not be found.", + "message": "Could not find GUI texture \u0027i_dialogue_button_large_middle_off.tga\u0027 at location \u0027Repository\u0027.", "severity": "Error", "context": [ - "Unit_Guard_Leia" + "IDC_PLAY_FACTION_B_BUTTON_BIG", + "Repository" ], - "asset": "U000_LEI0402_ENG.WAV" + "asset": "i_dialogue_button_large_middle_off.tga" }, { "id": "FILE00", "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" + "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" ], - "message": "Audio file \u0027U000_LEI0312_ENG.WAV\u0027 could not be found.", + "message": "Could not find GUI texture \u0027underworld_logo_selected.tga\u0027 at location \u0027MegaTexture\u0027.", "severity": "Error", "context": [ - "Unit_Attack_Leia" + "IDC_PLAY_FACTION_A_BUTTON_BIG", + "MegaTexture" ], - "asset": "U000_LEI0312_ENG.WAV" + "asset": "underworld_logo_selected.tga" }, { "id": "FILE00", "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" + "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" ], - "message": "Audio file \u0027U000_LEI0210_ENG.WAV\u0027 could not be found.", + "message": "Could not find GUI texture \u0027underworld_logo_off.tga\u0027 at location \u0027MegaTexture\u0027.", "severity": "Error", "context": [ - "Unit_Move_Leia" + "IDC_PLAY_FACTION_A_BUTTON_BIG", + "MegaTexture" ], - "asset": "U000_LEI0210_ENG.WAV" + "asset": "underworld_logo_off.tga" }, { "id": "FILE00", "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" + "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" ], - "message": "Audio file \u0027U000_LEI0313_ENG.WAV\u0027 could not be found.", + "message": "Could not find GUI texture \u0027i_button_petro_sliver.tga\u0027 at location \u0027MegaTexture\u0027.", "severity": "Error", "context": [ - "Unit_Attack_Leia" + "IDC_MENU_PETRO_LOGO", + "MegaTexture" ], - "asset": "U000_LEI0313_ENG.WAV" + "asset": "i_button_petro_sliver.tga" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" ], - "message": "Could not find GUI texture \u0027underworld_logo_selected.tga\u0027 at location \u0027MegaTexture\u0027.", + "message": "Could not find GUI texture \u0027underworld_logo_rollover.tga\u0027 at location \u0027MegaTexture\u0027.", "severity": "Error", "context": [ "IDC_PLAY_FACTION_A_BUTTON_BIG", "MegaTexture" ], - "asset": "underworld_logo_selected.tga" + "asset": "underworld_logo_rollover.tga" }, { - "id": "FILE00", + "id": "CMDBAR05", "verifiers": [ - "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" + "AET.ModVerify.Verifiers.CommandBarVerifier" ], - "message": "Could not find GUI texture \u0027i_button_petro_sliver.tga\u0027 at location \u0027MegaTexture\u0027.", - "severity": "Error", - "context": [ - "IDC_MENU_PETRO_LOGO", - "MegaTexture" + "message": "The CommandBar component \u0027g_planet_land_forces\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_planet_land_forces" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" ], - "asset": "i_button_petro_sliver.tga" + "message": "The CommandBar component \u0027g_ground_sell\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_ground_sell" }, { - "id": "FILE00", + "id": "CMDBAR05", "verifiers": [ - "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" + "AET.ModVerify.Verifiers.CommandBarVerifier" ], - "message": "Could not find GUI texture \u0027i_dialogue_button_large_middle_off.tga\u0027 at location \u0027Repository\u0027.", - "severity": "Error", - "context": [ - "IDC_PLAY_FACTION_B_BUTTON_BIG", - "Repository" + "message": "The CommandBar component \u0027st_bracket_medium\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_bracket_medium" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" ], - "asset": "i_dialogue_button_large_middle_off.tga" + "message": "The CommandBar component \u0027b_planet_right\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "b_planet_right" }, { - "id": "FILE00", + "id": "CMDBAR04", "verifiers": [ - "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" + "AET.ModVerify.Verifiers.CommandBarVerifier" ], - "message": "Could not find GUI texture \u0027underworld_logo_rollover.tga\u0027 at location \u0027MegaTexture\u0027.", - "severity": "Error", - "context": [ - "IDC_PLAY_FACTION_A_BUTTON_BIG", - "MegaTexture" + "message": "The CommandBar component \u0027g_credit_bar\u0027 is not supported by the game.", + "severity": "Information", + "context": [], + "asset": "g_credit_bar" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" ], - "asset": "underworld_logo_rollover.tga" + "message": "The CommandBar component \u0027zoomed_header_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "zoomed_header_text" }, { - "id": "FILE00", + "id": "CMDBAR05", "verifiers": [ - "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" + "AET.ModVerify.Verifiers.CommandBarVerifier" ], - "message": "Could not find GUI texture \u0027underworld_logo_off.tga\u0027 at location \u0027MegaTexture\u0027.", - "severity": "Error", - "context": [ - "IDC_PLAY_FACTION_A_BUTTON_BIG", - "MegaTexture" + "message": "The CommandBar component \u0027g_space_level_pips\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_space_level_pips" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" ], - "asset": "underworld_logo_off.tga" + "message": "The CommandBar component \u0027bribed_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "bribed_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027encyclopedia_header_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "encyclopedia_header_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tutorial_text_back\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tutorial_text_back" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027encyclopedia_back\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "encyclopedia_back" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027encyclopedia_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "encyclopedia_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027balance_pip\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "balance_pip" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027objective_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "objective_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027skirmish_upgrade\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "skirmish_upgrade" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027surface_mod_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "surface_mod_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_hero_health\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_hero_health" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027bribe_display\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "bribe_display" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_build\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_build" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027garrison_slot_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "garrison_slot_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_conflict\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_conflict" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tooltip_name\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tooltip_name" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027garrison_respawn_counter\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "garrison_respawn_counter" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_ability_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_ability_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_shields_medium\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_shields_medium" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_health\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_health" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_weather\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_weather" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_health_medium\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_health_medium" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_power\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_power" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_ground_level_pips\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_ground_level_pips" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027zoomed_cost_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "zoomed_cost_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027bm_title_4011\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "bm_title_4011" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_planet_name\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_planet_name" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_shields_large\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_shields_large" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_hero_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_hero_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027generic_flytext\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "generic_flytext" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027reinforcement_counter\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "reinforcement_counter" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_planet_value\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_planet_value" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027radar_blip\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "radar_blip" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_political_control\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_political_control" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_planet_ring\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_planet_ring" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_garrison_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_garrison_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027encyclopedia_right_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "encyclopedia_right_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027b_quick_ref\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "b_quick_ref" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027objective_back\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "objective_back" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027encyclopedia_center_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "encyclopedia_center_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_shields\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_shields" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_grab_bar\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_grab_bar" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_smuggler\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_smuggler" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_enemy_hero\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_enemy_hero" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027cs_ability_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "cs_ability_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027encyclopedia_cost_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "encyclopedia_cost_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_planet_ability\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_planet_ability" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_control_group\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_control_group" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027gui_dialog_tooltip\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "gui_dialog_tooltip" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027remote_bomb_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "remote_bomb_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tutorial_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tutorial_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_space_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_space_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_bracket_large\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_bracket_large" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027zoomed_back\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "zoomed_back" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027encyclopedia_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "encyclopedia_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027zoomed_right_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "zoomed_right_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027b_beacon_t\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "b_beacon_t" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_bounty_hunter\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_bounty_hunter" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_credit_bar\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_credit_bar" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_hero\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_hero" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027bm_title_4010\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "bm_title_4010" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_planet_fleet\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_planet_fleet" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_corruption_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_corruption_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_smuggled\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_smuggled" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027help_back\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "help_back" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_bracket_small\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_bracket_small" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027objective_header_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "objective_header_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_corruption_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_corruption_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_ground_level\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_ground_level" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027lt_weather_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "lt_weather_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027cs_ability_button\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "cs_ability_button" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_radar_view\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_radar_view" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027objective_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "objective_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tooltip_back\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tooltip_back" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027zoomed_center_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "zoomed_center_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_health_bar\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_health_bar" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027zoomed_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "zoomed_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027generic_collision\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "generic_collision" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tooltip_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tooltip_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_radar_blip\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_radar_blip" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_hero_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_hero_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_space_level\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_space_level" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027b_planet_left\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "b_planet_left" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tooltip_icon_land\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tooltip_icon_land" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_ground_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_ground_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tooltip_price\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tooltip_price" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tooltip_left_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tooltip_left_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_health_large\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_health_large" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tactical_sell\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tactical_sell" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_special_ability\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_special_ability" } ] } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/CommandLine/BaseModVerifyOptions.cs b/src/ModVerify.CliApp/Settings/CommandLine/BaseModVerifyOptions.cs index 0e5e203..36c8509 100644 --- a/src/ModVerify.CliApp/Settings/CommandLine/BaseModVerifyOptions.cs +++ b/src/ModVerify.CliApp/Settings/CommandLine/BaseModVerifyOptions.cs @@ -21,9 +21,9 @@ internal abstract class BaseModVerifyOptions public string? Suppressions { get; init; } [Option("path", SetName = "autoDetection", Required = false, Default = null, - HelpText = "Specifies the path to verify. The path may be a game or mod. The application will try to find all necessary sub-mods or base games itself. " + + HelpText = "Specifies the path to verify. The path may be a game or mod. The application will try to find all necessary sub-mods and base games itself. " + "The argument cannot be combined with any of --mods, --game or --fallbackGame")] - public string? AutoPath { get; init; } + public string? TargetPath { get; init; } [Option("mods", SetName = "manualPaths", Required = false, Default = null, Separator = ';', HelpText = "The path of the mod to verify. To support submods, multiple paths can be separated using the ';' (semicolon) character. " + @@ -41,10 +41,10 @@ internal abstract class BaseModVerifyOptions public string? FallbackGamePath { get; init; } - [Option("type", Required = false, Default = null, - HelpText = "The game type of the mod that shall be verified. Skip this value to auto-determine the type. Valid values are 'Eaw' and 'Foc'. " + + [Option("engine", Required = false, Default = null, + HelpText = "The game engine of the target that shall be verified. Skip this value to auto-determine the type. Valid values are 'Eaw' and 'Foc'. " + "This argument is required, if the first mod of '--mods' points to a directory outside of the common folder hierarchy (e.g, /MODS/MOD_NAME or /32470/WORKSHOP_ID")] - public GameEngineType? GameType { get; init; } + public GameEngineType? Engine { get; init; } [Option("additionalFallbackPaths", Required = false, Separator = ';', diff --git a/src/ModVerify.CliApp/Settings/CommandLine/CreateBaselineVerbOption.cs b/src/ModVerify.CliApp/Settings/CommandLine/CreateBaselineVerbOption.cs index dc60a73..6593245 100644 --- a/src/ModVerify.CliApp/Settings/CommandLine/CreateBaselineVerbOption.cs +++ b/src/ModVerify.CliApp/Settings/CommandLine/CreateBaselineVerbOption.cs @@ -7,4 +7,7 @@ internal sealed class CreateBaselineVerbOption : BaseModVerifyOptions { [Option('o', "outFile", Required = true, HelpText = "The file path of the new baseline file.")] public required string OutputFile { get; init; } + + [Option("skipLocation", Required = false, HelpText = "Skips writing the target location to the baseline.")] + public bool SkipLocation { get; init; } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/CommandLine/VerifyVerbOption.cs b/src/ModVerify.CliApp/Settings/CommandLine/VerifyVerbOption.cs index 97f1536..e3be836 100644 --- a/src/ModVerify.CliApp/Settings/CommandLine/VerifyVerbOption.cs +++ b/src/ModVerify.CliApp/Settings/CommandLine/VerifyVerbOption.cs @@ -16,17 +16,19 @@ internal sealed class VerifyVerbOption : BaseModVerifyOptions public string? OutputDirectory { get; init; } [Option("failFast", Required = false, Default = false, - HelpText = "When set, the application will abort on the first failure. The option also recognized the 'MinimumFailureSeverity' setting.")] + HelpText = "When set, the application will abort on the first failure. " + + "The option requires 'minFailSeverity' to be set.")] public bool FailFast { get; init; } [Option("minFailSeverity", Required = false, Default = null, - HelpText = "When set, the application return with an error, if any finding has at least the specified severity value.")] + HelpText = "When set, the application returns with an error, if any finding has at least the specified severity value.")] public VerificationSeverity? MinimumFailureSeverity { get; set; } [Option("ignoreAsserts", Required = false, HelpText = "When this flag is present, the application will not report engine assertions.")] public bool IgnoreAsserts { get; init; } + [Option("baseline", SetName = "baselineSelection", Required = false, HelpText = "Path to a JSON baseline file. Cannot be used together with --searchBaseline.")] public string? Baseline { get; init; } diff --git a/src/ModVerify.CliApp/Settings/CommandLineHelper.cs b/src/ModVerify.CliApp/Settings/CommandLineHelper.cs new file mode 100644 index 0000000..f799591 --- /dev/null +++ b/src/ModVerify.CliApp/Settings/CommandLineHelper.cs @@ -0,0 +1,18 @@ +using System; +using System.Linq; +using System.Reflection; +using CommandLine; + +namespace AET.ModVerify.App.Settings; + +internal static class CommandLineHelper +{ + public static string GetOptionName(this Type type, string optionPropertyName) + { + var property = type.GetProperties().FirstOrDefault(p => p.Name.Equals(optionPropertyName)); + var optionAttribute = property?.GetCustomAttribute(); + return optionAttribute is null + ? throw new InvalidOperationException($"Unable to get option data for {type}:{optionAttribute}") + : $"--{optionAttribute.LongName}"; + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/GameInstallationsSettings.cs b/src/ModVerify.CliApp/Settings/GameInstallationsSettings.cs deleted file mode 100644 index 00bc4f0..0000000 --- a/src/ModVerify.CliApp/Settings/GameInstallationsSettings.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using PG.StarWarsGame.Engine; - -namespace AET.ModVerify.App.Settings; - -internal sealed record GameInstallationsSettings -{ - public bool Interactive => string.IsNullOrEmpty(AutoPath) && ModPaths.Count == 0 && string.IsNullOrEmpty(GamePath); - - [MemberNotNullWhen(true, nameof(AutoPath))] - public bool UseAutoDetection => !string.IsNullOrEmpty(AutoPath); - - [MemberNotNullWhen(true, nameof(GamePath))] - public bool ManualSetup => !string.IsNullOrEmpty(GamePath); - - public string? AutoPath { get; init; } - - public IList ModPaths { get; init; } = []; - - public string? GamePath { get; init; } - - public string? FallbackGamePath { get; init; } - - public IList AdditionalFallbackPaths { get; init; } = []; - - public GameEngineType? EngineType { get; init; } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/ModVerifyAppSettings.cs b/src/ModVerify.CliApp/Settings/ModVerifyAppSettings.cs index 3376354..e4488a0 100644 --- a/src/ModVerify.CliApp/Settings/ModVerifyAppSettings.cs +++ b/src/ModVerify.CliApp/Settings/ModVerifyAppSettings.cs @@ -1,23 +1,77 @@ -using System.Diagnostics.CodeAnalysis; +using System; using AET.ModVerify.Reporting; using AET.ModVerify.Settings; namespace AET.ModVerify.App.Settings; -internal sealed class ModVerifyAppSettings +public class AppReportSettings { - public bool Interactive => GameInstallationsSettings.Interactive; + public VerificationSeverity MinimumReportSeverity { get; init; } + + public string? SuppressionsPath { get; init; } - public required VerifyPipelineSettings VerifyPipelineSettings { get; init; } + public bool Verbose { get; init; } +} - public required ModVerifyReportSettings ReportSettings { get; init; } +public sealed class VerifyReportSettings : AppReportSettings +{ + public string? BaselinePath { get; init; } + public bool SearchBaselineLocally { get; init; } +} + +internal abstract class AppSettingsBase(AppReportSettings reportSettings) +{ + public bool IsInteractive => VerificationTargetSettings.Interactive; + + public required VerificationTargetSettings VerificationTargetSettings + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } + + public required VerifyPipelineSettings VerifyPipelineSettings + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } - public required GameInstallationsSettings GameInstallationsSettings { get; init; } + public AppReportSettings ReportSettings { get; } = reportSettings ?? throw new ArgumentNullException(nameof(reportSettings)); +} - public VerificationSeverity? AppThrowsOnMinimumSeverity { get; init; } +internal abstract class AppSettingsBase(T reportSettings) : AppSettingsBase(reportSettings) + where T : AppReportSettings +{ + public new T ReportSettings { get; } = reportSettings ?? throw new ArgumentNullException(nameof(reportSettings)); +} + +internal sealed class AppVerifySettings(VerifyReportSettings reportSettings) : AppSettingsBase(reportSettings) +{ + public VerificationSeverity? AppFailsOnMinimumSeverity { get; init; } - [MemberNotNullWhen(true, nameof(NewBaselinePath))] - public bool CreateNewBaseline => !string.IsNullOrEmpty(NewBaselinePath); + public required string ReportDirectory + { + get; + init + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentNullException(nameof(value)); + field = value; + } + } +} + +internal sealed class AppBaselineSettings(AppReportSettings reportSettings) : AppSettingsBase(reportSettings) +{ + public required string NewBaselinePath + { + get; + init + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentNullException(nameof(value)); + field = value; + } + } - public string? NewBaselinePath { get; init; } + public bool WriteLocations { get; init; } = true; } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/ModVerifyReportSettings.cs b/src/ModVerify.CliApp/Settings/ModVerifyReportSettings.cs deleted file mode 100644 index 482b844..0000000 --- a/src/ModVerify.CliApp/Settings/ModVerifyReportSettings.cs +++ /dev/null @@ -1,14 +0,0 @@ -using AET.ModVerify.Reporting; - -namespace AET.ModVerify.App.Settings; - -internal sealed class ModVerifyReportSettings -{ - public VerificationSeverity MinimumReportSeverity { get; init; } - - public string? SuppressionsPath { get; init; } - - public string? BaselinePath { get; init; } - - public bool SearchBaselineLocally { get; init; } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/SettingsBuilder.cs b/src/ModVerify.CliApp/Settings/SettingsBuilder.cs index 3c5535f..23a89ff 100644 --- a/src/ModVerify.CliApp/Settings/SettingsBuilder.cs +++ b/src/ModVerify.CliApp/Settings/SettingsBuilder.cs @@ -1,22 +1,18 @@ -using System; -using System.Collections.Generic; -using System.IO.Abstractions; -using AET.ModVerify.App.Settings.CommandLine; -using AET.ModVerify.App.Utilities; +using AET.ModVerify.App.Settings.CommandLine; using AET.ModVerify.Pipeline; -using AET.ModVerify.Reporting; using AET.ModVerify.Settings; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO.Abstractions; namespace AET.ModVerify.App.Settings; internal sealed class SettingsBuilder(IServiceProvider serviceProvider) { - private readonly ILogger? _logger = serviceProvider.GetService()?.CreateLogger(typeof(SettingsBuilder)); private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); - public ModVerifyAppSettings BuildSettings(BaseModVerifyOptions options) + public AppSettingsBase BuildSettings(BaseModVerifyOptions options) { switch (options) { @@ -28,90 +24,103 @@ public ModVerifyAppSettings BuildSettings(BaseModVerifyOptions options) throw new NotSupportedException($"The option '{options.GetType().Name}' is not supported!"); } - private ModVerifyAppSettings BuildFromVerifyVerb(VerifyVerbOption verifyOptions) + private AppVerifySettings BuildFromVerifyVerb(VerifyVerbOption verifyOptions) { - return new ModVerifyAppSettings + ValidateVerb(); + var failFastSetting = GetFailFastSetting(); + return new AppVerifySettings(BuildReportSettings()) { + ReportDirectory = GetReportDirectory(), VerifyPipelineSettings = new VerifyPipelineSettings { ParallelVerifiers = verifyOptions.Parallel ? 4 : 1, VerifiersProvider = new DefaultGameVerifiersProvider(), - FailFast = verifyOptions.FailFast, + FailFastSettings = failFastSetting, GameVerifySettings = new GameVerifySettings { IgnoreAsserts = verifyOptions.IgnoreAsserts, - ThrowsOnMinimumSeverity = GetVerifierMinimumThrowSeverity() + ThrowsOnMinimumSeverity = failFastSetting.IsFailFast + ? failFastSetting.MinumumSeverity + // The app shall not make a specific verifier throw, but it should always run to completion. + : null } }, - AppThrowsOnMinimumSeverity = verifyOptions.MinimumFailureSeverity, - GameInstallationsSettings = BuildInstallationSettings(verifyOptions), - ReportSettings = BuildReportSettings(verifyOptions), + AppFailsOnMinimumSeverity = verifyOptions.MinimumFailureSeverity, + VerificationTargetSettings = BuildTargetSettings(verifyOptions), }; - VerificationSeverity? GetVerifierMinimumThrowSeverity() + void ValidateVerb() { - var minFailSeverity = verifyOptions.MinimumFailureSeverity; - if (verifyOptions.FailFast) + if (verifyOptions.SearchBaselineLocally && !string.IsNullOrEmpty(verifyOptions.Baseline)) { - if (minFailSeverity == null) - { - _logger?.LogWarning(ModVerifyConstants.ConsoleEventId, - "Verification is configured to fail fast but 'minFailSeverity' is not specified. Using severity '{Info}'.", VerificationSeverity.Information); - minFailSeverity = VerificationSeverity.Information; - } + var searchOption = typeof(VerifyVerbOption).GetOptionName(nameof(VerifyVerbOption.SearchBaselineLocally)); + var baselineOption = typeof(VerifyVerbOption).GetOptionName(nameof(VerifyVerbOption.Baseline)); + throw new AppArgumentException($"Options {searchOption} and {baselineOption} cannot be used together."); + } - return minFailSeverity; + if (verifyOptions is { FailFast: true, MinimumFailureSeverity: null }) + { + var failFast = typeof(VerifyVerbOption).GetOptionName(nameof(VerifyVerbOption.FailFast)); + var minThrowSeverity = typeof(VerifyVerbOption).GetOptionName(nameof(VerifyVerbOption.MinimumFailureSeverity)); + throw new AppArgumentException($"Option {failFast} requires to set {minThrowSeverity}."); } + } + + FailFastSetting GetFailFastSetting() + { + return !verifyOptions.FailFast + ? FailFastSetting.NoFailFast + : new FailFastSetting(verifyOptions.MinimumFailureSeverity!.Value); + } + + string GetReportDirectory() + { + return _fileSystem.Path.GetFullPath(_fileSystem.Path.Combine( + Environment.CurrentDirectory, + verifyOptions.OutputDirectory ?? "ModVerifyResults")); + } - // Only in a failFast scenario we want the verifier to throw. - // In a normal run, the verifier should simply store the error. - return null; + VerifyReportSettings BuildReportSettings() + { + return new VerifyReportSettings + { + BaselinePath = verifyOptions.Baseline, + MinimumReportSeverity = verifyOptions.MinimumSeverity, + SearchBaselineLocally = verifyOptions.SearchBaselineLocally, + SuppressionsPath = verifyOptions.Suppressions, + Verbose = verifyOptions.Verbose + }; } } - private ModVerifyAppSettings BuildFromCreateBaselineVerb(CreateBaselineVerbOption baselineVerb) + private AppBaselineSettings BuildFromCreateBaselineVerb(CreateBaselineVerbOption baselineVerb) { - return new ModVerifyAppSettings + return new AppBaselineSettings(BuildReportSettings()) { VerifyPipelineSettings = new VerifyPipelineSettings { ParallelVerifiers = baselineVerb.Parallel ? 4 : 1, - GameVerifySettings = new GameVerifySettings - { - IgnoreAsserts = false, - ThrowsOnMinimumSeverity = null, - }, VerifiersProvider = new DefaultGameVerifiersProvider(), - FailFast = false, + GameVerifySettings = GameVerifySettings.Default, + FailFastSettings = FailFastSetting.NoFailFast, }, - AppThrowsOnMinimumSeverity = null, - GameInstallationsSettings = BuildInstallationSettings(baselineVerb), - ReportSettings = BuildReportSettings(baselineVerb), + VerificationTargetSettings = BuildTargetSettings(baselineVerb), NewBaselinePath = baselineVerb.OutputFile, + WriteLocations = !baselineVerb.SkipLocation }; - } - private static ModVerifyReportSettings BuildReportSettings(BaseModVerifyOptions options) - { - var baselinePath = (options as VerifyVerbOption)?.Baseline; - - return new ModVerifyReportSettings - { - BaselinePath = baselinePath, - MinimumReportSeverity = options.MinimumSeverity, - SearchBaselineLocally = SearchLocally(options), - SuppressionsPath = options.Suppressions - }; - - static bool SearchLocally(BaseModVerifyOptions o) + AppReportSettings BuildReportSettings() { - if (o is not VerifyVerbOption v) - return false; - return v.SearchBaselineLocally || v.LaunchedWithoutArguments(); + return new AppReportSettings + { + MinimumReportSeverity = baselineVerb.MinimumSeverity, + SuppressionsPath = baselineVerb.Suppressions, + Verbose = baselineVerb.Verbose + }; } } - private GameInstallationsSettings BuildInstallationSettings(BaseModVerifyOptions options) + private VerificationTargetSettings BuildTargetSettings(BaseModVerifyOptions options) { var modPaths = new List(); if (options.ModPaths is not null) @@ -142,18 +151,18 @@ private GameInstallationsSettings BuildInstallationSettings(BaseModVerifyOptions if (!string.IsNullOrEmpty(gamePath) && !string.IsNullOrEmpty(options.FallbackGamePath)) fallbackGamePath = _fileSystem.Path.GetFullPath(options.FallbackGamePath!); - var autoPath = options.AutoPath; - if (!string.IsNullOrEmpty(autoPath)) - autoPath = _fileSystem.Path.GetFullPath(autoPath!); + var targetPath = options.TargetPath; + if (!string.IsNullOrEmpty(targetPath)) + targetPath = _fileSystem.Path.GetFullPath(targetPath!); - return new GameInstallationsSettings + return new VerificationTargetSettings { - AutoPath = autoPath, + TargetPath = targetPath, ModPaths = modPaths, GamePath = gamePath, FallbackGamePath = fallbackGamePath, AdditionalFallbackPaths = fallbackPaths, - EngineType = options.GameType + Engine = options.Engine }; } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/VerificationTargetSettings.cs b/src/ModVerify.CliApp/Settings/VerificationTargetSettings.cs new file mode 100644 index 0000000..b5e1a40 --- /dev/null +++ b/src/ModVerify.CliApp/Settings/VerificationTargetSettings.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using PG.StarWarsGame.Engine; + +namespace AET.ModVerify.App.Settings; + +internal sealed record VerificationTargetSettings +{ + public bool Interactive => string.IsNullOrEmpty(TargetPath) && ModPaths.Count == 0 && string.IsNullOrEmpty(GamePath) && string.IsNullOrEmpty(FallbackGamePath); + + [MemberNotNullWhen(true, nameof(TargetPath))] + public bool UseAutoDetection => !string.IsNullOrEmpty(TargetPath) && ModPaths.Count == 0 && string.IsNullOrEmpty(GamePath) && string.IsNullOrEmpty(FallbackGamePath); + + [MemberNotNullWhen(true, nameof(GamePath))] + public bool ManualSetup => !string.IsNullOrEmpty(GamePath); + + public string? TargetPath { get; init; } + + public IReadOnlyList ModPaths { get; init; } = []; + + public string? GamePath { get; init; } + + public string? FallbackGamePath { get; init; } + + public IReadOnlyList AdditionalFallbackPaths { get; init; } = []; + + public GameEngineType? Engine { get; init; } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/TargetSelectors/AutomaticSelector.cs b/src/ModVerify.CliApp/TargetSelectors/AutomaticSelector.cs new file mode 100644 index 0000000..5250ec4 --- /dev/null +++ b/src/ModVerify.CliApp/TargetSelectors/AutomaticSelector.cs @@ -0,0 +1,163 @@ +using AET.ModVerify.App.GameFinder; +using AET.ModVerify.App.Settings; +using AET.ModVerify.App.Utilities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Infrastructure; +using PG.StarWarsGame.Infrastructure.Games; +using PG.StarWarsGame.Infrastructure.Mods; +using PG.StarWarsGame.Infrastructure.Services; +using PG.StarWarsGame.Infrastructure.Services.Detection; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO.Abstractions; +using System.Linq; +using AnakinRaW.CommonUtilities.FileSystem; + +namespace AET.ModVerify.App.TargetSelectors; + +internal class AutomaticSelector(IServiceProvider serviceProvider) : VerificationTargetSelectorBase(serviceProvider) +{ + private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); + + internal override SelectionResult SelectTarget(VerificationTargetSettings settings) + { + if (!settings.UseAutoDetection) + throw new ArgumentException("wrong settings format provided.", nameof(settings)); + + var targetPath = settings.TargetPath; + if (!_fileSystem.Directory.Exists(targetPath)) + { + Logger?.LogError(ModVerifyConstants.ConsoleEventId, "The specified path '{Path}' does not exist.", targetPath); + throw new TargetNotFoundException(targetPath); + } + + var engine = settings.Engine; + + GameFinderResult finderResult; + try + { + var finderSettings = new GameFinderSettings + { + Engine = engine, + InitMods = true, + SearchFallbackGame = true + }; + finderResult = GameFinderService.FindGamesFromPathOrGlobal(targetPath, finderSettings); + } + catch (GameNotFoundException) + { + Logger?.LogError(ModVerifyConstants.ConsoleEventId, + "Unable to find games based of the specified target path '{Path}'. Consider specifying all paths manually.", targetPath); + throw; + } + + GameLocations locations; + + var targetObject = GetAttachedModOrGame(finderResult, targetPath, engine); + + if (targetObject is not null) + { + var actualType = targetObject.Game.Type; + Debug.Assert(IsEngineTypeSupported(engine, actualType)); + engine ??= actualType.ToEngineType(); + locations = GetLocations(targetObject, finderResult.FallbackGame, settings.AdditionalFallbackPaths); + } + else + { + if (!engine.HasValue) + throw new ArgumentException("Game engine not specified. Use --engine argument to set it."); + + Logger?.LogDebug("The requested mod at '{TargetPath}' is detached from its games.", targetPath); + + // The path is a detached mod, that exists on a different location than the game. + locations = GetDetachedModLocations(targetPath, finderResult, engine.Value, settings.AdditionalFallbackPaths, out var mod); + targetObject = mod; + } + + return new(locations, engine.Value, targetObject); + } + + private IPhysicalPlayableObject? GetAttachedModOrGame(GameFinderResult finderResult, string targetPath, GameEngineType? requestedEngineType) + { + var targetFullPath = _fileSystem.Path.GetFullPath(targetPath); + + IPhysicalPlayableObject? target = null; + + // If the target is the game directory itself. + if (targetFullPath.Equals(finderResult.Game.Directory.FullName, StringComparison.OrdinalIgnoreCase)) + target = finderResult.Game; + + target ??= GetMatchingModFromGame(finderResult.Game, targetFullPath, requestedEngineType) ?? + GetMatchingModFromGame(finderResult.FallbackGame, targetFullPath, requestedEngineType); + + return target; + } + + private GameLocations GetDetachedModLocations( + string modPath, + GameFinderResult gameResult, + GameEngineType requestedGameEngine, + IReadOnlyList additionalFallbackPaths, + out IPhysicalMod mod) + { + // Because requestedGameEngine must be set, GameFinderService already ensures + // gameResult.Game is the correct type. + var game = gameResult.Game; + var modFinder = ServiceProvider.GetRequiredService(); + var modRef = modFinder.FindMods(game, _fileSystem.DirectoryInfo.New(modPath)).FirstOrDefault(); + + if (modRef is null) + ThrowEngineNotSupported(requestedGameEngine, modPath); + + var modFactory = ServiceProvider.GetRequiredService(); + mod = modFactory.CreatePhysicalMod(game, modRef, CultureInfo.InvariantCulture); + + game.AddMod(mod); + + mod.ResolveDependencies(); + + return GetLocations(mod, gameResult.FallbackGame, additionalFallbackPaths); + } + + private IPhysicalMod? GetMatchingModFromGame(IGame? game, string modPath, GameEngineType? requestedEngineType) + { + if (game is null || !IsEngineTypeSupported(requestedEngineType, game.Type)) + return null; + + foreach (var mod in game.Game.Mods) + { + if (mod is not IPhysicalMod physicalMod) + continue; + + if (_fileSystem.Path.AreEqual(modPath, physicalMod.Directory.FullName)) + return physicalMod; + } + + return null; + } + + private static IGame? GetTargetGame(GameFinderResult finderResult, GameEngineType? requestedEngine) + { + if (finderResult.Game.Type.ToEngineType() == requestedEngine) + return finderResult.Game; + if (finderResult.FallbackGame is not null && finderResult.FallbackGame.Type.ToEngineType() == requestedEngine) + return finderResult.FallbackGame; + return null; + } + + private static bool IsEngineTypeSupported([NotNullWhen(false)] GameEngineType? requestedEngineType, GameType actualGameType) + { + return !requestedEngineType.HasValue || actualGameType.ToEngineType() == requestedEngineType; + } + + [DoesNotReturn] + private static void ThrowEngineNotSupported(GameEngineType requested, string targetPath) + { + throw new ArgumentException($"The specified game engine '{requested}' does not match engine of the verification target '{targetPath}'."); + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectors/ConsoleModSelector.cs b/src/ModVerify.CliApp/TargetSelectors/ConsoleSelector.cs similarity index 79% rename from src/ModVerify.CliApp/ModSelectors/ConsoleModSelector.cs rename to src/ModVerify.CliApp/TargetSelectors/ConsoleSelector.cs index c776d6d..17b9791 100644 --- a/src/ModVerify.CliApp/ModSelectors/ConsoleModSelector.cs +++ b/src/ModVerify.CliApp/TargetSelectors/ConsoleSelector.cs @@ -5,22 +5,21 @@ using AET.ModVerify.App.Settings; using AET.ModVerify.App.Utilities; using AnakinRaW.ApplicationBase; -using PG.StarWarsGame.Engine; using PG.StarWarsGame.Infrastructure; using PG.StarWarsGame.Infrastructure.Games; using PG.StarWarsGame.Infrastructure.Mods; -namespace AET.ModVerify.App.ModSelectors; +namespace AET.ModVerify.App.TargetSelectors; -internal class ConsoleModSelector(IServiceProvider serviceProvider) : ModSelectorBase(serviceProvider) +internal class ConsoleSelector(IServiceProvider serviceProvider) : VerificationTargetSelectorBase(serviceProvider) { - public override GameLocations Select(GameInstallationsSettings settings, out IPhysicalPlayableObject targetObject, - out GameEngineType? actualEngineType) + internal override SelectionResult SelectTarget(VerificationTargetSettings settings) { - var gameResult = GameFinderService.FindGames(); - targetObject = SelectPlayableObject(gameResult); - actualEngineType = targetObject.Game.Type.ToEngineType(); - return GetLocations(targetObject, gameResult, settings.AdditionalFallbackPaths); + var gameResult = GameFinderService.FindGames(GameFinderSettings.Default); + var targetObject = SelectPlayableObject(gameResult); + var engine = targetObject.Game.Type.ToEngineType(); + var locations = GetLocations(targetObject, gameResult.FallbackGame, settings.AdditionalFallbackPaths); + return new SelectionResult(locations, engine, targetObject); } private static IPhysicalPlayableObject SelectPlayableObject(GameFinderResult finderResult) @@ -31,6 +30,9 @@ private static IPhysicalPlayableObject SelectPlayableObject(GameFinderResult fin list.Add(finderResult.Game); Console.WriteLine(); + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine("Listing Games and Mods:"); + Console.ResetColor(); ConsoleUtilities.WriteHorizontalLine(); Console.WriteLine($"0: {game.Name}"); diff --git a/src/ModVerify.CliApp/TargetSelectors/IVerificationTargetSelector.cs b/src/ModVerify.CliApp/TargetSelectors/IVerificationTargetSelector.cs new file mode 100644 index 0000000..6d452e8 --- /dev/null +++ b/src/ModVerify.CliApp/TargetSelectors/IVerificationTargetSelector.cs @@ -0,0 +1,8 @@ +using AET.ModVerify.App.Settings; + +namespace AET.ModVerify.App.TargetSelectors; + +internal interface IVerificationTargetSelector +{ + VerificationTarget Select(VerificationTargetSettings settings); +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/TargetSelectors/ManualSelector.cs b/src/ModVerify.CliApp/TargetSelectors/ManualSelector.cs new file mode 100644 index 0000000..f963b4f --- /dev/null +++ b/src/ModVerify.CliApp/TargetSelectors/ManualSelector.cs @@ -0,0 +1,73 @@ +using System; +using System.Globalization; +using System.Linq; +using AET.ModVerify.App.GameFinder; +using AET.ModVerify.App.Settings; +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Infrastructure; +using PG.StarWarsGame.Infrastructure.Games; +using PG.StarWarsGame.Infrastructure.Services; +using PG.StarWarsGame.Infrastructure.Services.Detection; + +namespace AET.ModVerify.App.TargetSelectors; + +internal class ManualSelector(IServiceProvider serviceProvider) : VerificationTargetSelectorBase(serviceProvider) +{ + internal override SelectionResult SelectTarget(VerificationTargetSettings settings) + { + if (string.IsNullOrEmpty(settings.GamePath)) + throw new ArgumentException("Argument --game must be set."); + if (!settings.Engine.HasValue) + throw new ArgumentException("Unable to determine game type. Use --engine argument to set the game type."); + + var engine = settings.Engine.Value; + + var gameLocations = new GameLocations( + settings.ModPaths.ToList(), + settings.GamePath!, + GetFallbackPaths(settings.FallbackGamePath, settings.AdditionalFallbackPaths).ToList()); + + + // For the manual selector the whole game and mod detection is optional. + // This allows user to use the application for unusual scenarios, + // not known to the detection service. + if (!GameFinderService.TryFindGame(gameLocations.GamePath, + new GameFinderSettings { Engine = engine, InitMods = false, SearchFallbackGame = false }, + out var game)) + { + // TODO: Log + } + + // If the fallback game path is specified we simply try to detect the game and report a warning to the user if not found. + var fallbackGamePath = settings.FallbackGamePath; + if (!string.IsNullOrEmpty(fallbackGamePath)) + { + + if (!GameFinderService.TryFindGame(fallbackGamePath, + new GameFinderSettings { InitMods = false, SearchFallbackGame = false }, + out _)) + { + // TODO: Log + } + } + + var target = TryGetPlayableObject(game, gameLocations.ModPaths.FirstOrDefault()); + return new SelectionResult(gameLocations, engine, target); + } + + private IPhysicalPlayableObject? TryGetPlayableObject(IGame? game, string? modPath) + { + if (game is null) + return null; + if (string.IsNullOrEmpty(modPath)) + return game; + + var modFinder = ServiceProvider.GetRequiredService(); + var modFactory = ServiceProvider.GetRequiredService(); + + var mods = modFinder.FindMods(game, FileSystem.DirectoryInfo.New(modPath)); + var mod = modFactory.CreatePhysicalMod(game, mods.First(), CultureInfo.InvariantCulture); + return mod; + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/TargetSelectors/TargetNotFoundException.cs b/src/ModVerify.CliApp/TargetSelectors/TargetNotFoundException.cs new file mode 100644 index 0000000..d6b052e --- /dev/null +++ b/src/ModVerify.CliApp/TargetSelectors/TargetNotFoundException.cs @@ -0,0 +1,6 @@ +using System.IO; + +namespace AET.ModVerify.App.TargetSelectors; + +internal class TargetNotFoundException(string path) + : DirectoryNotFoundException($"The target path '{path}' does not exist"); \ No newline at end of file diff --git a/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorBase.cs b/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorBase.cs new file mode 100644 index 0000000..68f0f39 --- /dev/null +++ b/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorBase.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Linq; +using System.Runtime.InteropServices; +using AET.ModVerify.App.GameFinder; +using AET.ModVerify.App.Settings; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Infrastructure; +using PG.StarWarsGame.Infrastructure.Clients; +using PG.StarWarsGame.Infrastructure.Clients.Utilities; +using PG.StarWarsGame.Infrastructure.Games; +using PG.StarWarsGame.Infrastructure.Mods; +using PG.StarWarsGame.Infrastructure.Services.Dependencies; + +namespace AET.ModVerify.App.TargetSelectors; + +internal abstract class VerificationTargetSelectorBase : IVerificationTargetSelector +{ + internal sealed record SelectionResult( + GameLocations Locations, + GameEngineType Engine, + IPhysicalPlayableObject? Target); + + protected readonly ILogger? Logger; + protected readonly GameFinderService GameFinderService; + protected readonly IServiceProvider ServiceProvider; + protected readonly IFileSystem FileSystem; + + protected VerificationTargetSelectorBase(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + Logger = serviceProvider.GetService()?.CreateLogger(GetType()); + GameFinderService = new GameFinderService(serviceProvider); + FileSystem = serviceProvider.GetRequiredService(); + } + + public VerificationTarget Select(VerificationTargetSettings settings) + { + var selectedTarget = SelectTarget(settings); + + return new VerificationTarget + { + Location = selectedTarget.Locations, + Engine = selectedTarget.Engine, + Name = GetTargetName(selectedTarget.Target, selectedTarget.Locations), + Version = GetTargetVersion(selectedTarget.Target) + }; + } + + + internal abstract SelectionResult SelectTarget(VerificationTargetSettings settings); + + + protected GameLocations GetLocations( + IPhysicalPlayableObject target, + IGame? fallbackGame, + IReadOnlyList additionalFallbackPaths) + { + var fallbacks = GetFallbackPaths(target, fallbackGame, additionalFallbackPaths); + var modPaths = GetModPaths(target); + return new GameLocations(modPaths, target.Game.Directory.FullName, fallbacks); + } + + private static IReadOnlyList GetFallbackPaths(IPhysicalPlayableObject target, IGame? fallbackGame, IReadOnlyList additionalFallbackPaths) + { + var coercedFallbackGame = fallbackGame; + if (target is IGame tGame && tGame.Equals(fallbackGame)) + coercedFallbackGame = null; + else if (target.Game.Equals(fallbackGame)) + coercedFallbackGame = null; + return GetFallbackPaths(coercedFallbackGame?.Directory.FullName, additionalFallbackPaths); + } + + + protected static IReadOnlyList GetFallbackPaths(string? fallbackGame, IReadOnlyList additionalFallbackPaths) + { + var fallbacks = new List(); + if (fallbackGame is not null) + fallbacks.Add(fallbackGame); + foreach (var fallback in additionalFallbackPaths) + fallbacks.Add(fallback); + return fallbacks; + } + + + private IReadOnlyList GetModPaths(IPhysicalPlayableObject modOrGame) + { + if (modOrGame is not IMod mod) + return []; + + var traverser = ServiceProvider.GetRequiredService(); + return traverser.Traverse(mod) + .OfType().Select(x => x.Directory.FullName) + .ToList(); + } + + protected static string GetTargetName(IPhysicalPlayableObject? targetObject, GameLocations gameLocations) + { + if (targetObject is not null) + return targetObject.Name; + + // TODO: Reuse name beautifier from GameInfrastructure lib + var mod = gameLocations.ModPaths.FirstOrDefault(); + return mod ?? gameLocations.GamePath; + } + + protected string? GetTargetVersion(IPhysicalPlayableObject? targetObject) + { + return targetObject switch + { + IMod mod => mod.Version?.ToString(), + IGame game => GetVersionFromGame(game), + _ => null + }; + } + + private string? GetVersionFromGame(IGame game) + { + var exeFile = GameExecutableFileUtilities.GetExecutableForGame(game, GameBuildType.Release); + if (exeFile is null) + { + Logger?.LogWarning(ModVerifyConstants.ConsoleEventId, + "Unable to get game version of target path '{Path}'. Is this a game directory?", + game.Directory.FullName); + return null; + } + + var versionInfo = FileSystem.FileVersionInfo.GetVersionInfo(exeFile.FullName); + var version = + $"{versionInfo.FileMajorPart}.{versionInfo.FileMinorPart}.{versionInfo.FileBuildPart}.{versionInfo.FilePrivatePart}"; + return version; + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorFactory.cs b/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorFactory.cs new file mode 100644 index 0000000..5e106eb --- /dev/null +++ b/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorFactory.cs @@ -0,0 +1,18 @@ +using System; +using AET.ModVerify.App.Settings; + +namespace AET.ModVerify.App.TargetSelectors; + +internal sealed class VerificationTargetSelectorFactory(IServiceProvider serviceProvider) +{ + public IVerificationTargetSelector CreateSelector(VerificationTargetSettings settings) + { + if (settings.Interactive) + return new ConsoleSelector(serviceProvider); + if (settings.UseAutoDetection) + return new AutomaticSelector(serviceProvider); + if (settings.ManualSetup) + return new ManualSelector(serviceProvider); + throw new ArgumentException("Unknown option configuration provided."); + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Utilities/ExtensionMethods.cs b/src/ModVerify.CliApp/Utilities/ExtensionMethods.cs index b2e7ab0..fc07469 100644 --- a/src/ModVerify.CliApp/Utilities/ExtensionMethods.cs +++ b/src/ModVerify.CliApp/Utilities/ExtensionMethods.cs @@ -8,16 +8,23 @@ namespace AET.ModVerify.App.Utilities; internal static class ExtensionMethods { - public static GameEngineType ToEngineType(this GameType type) + extension(GameEngineType type) { - return type == GameType.Foc ? GameEngineType.Foc : GameEngineType.Eaw; + public GameType FromEngineType() + { + return (GameType)(int)type; + } + + public GameEngineType Opposite() + { + return (GameEngineType)((int)type ^ 1); + } } - public static GameType FromEngineType(this GameEngineType type) + public static GameEngineType ToEngineType(this GameType type) { - return type == GameEngineType.Foc ? GameType.Foc : GameType.Eaw; + return (GameEngineType)(int)type; } - extension(ApplicationEnvironment modVerifyEnvironment) { public bool IsUpdatable() diff --git a/src/ModVerify.CliApp/Utilities/ModVerifyConsoleUtilities.cs b/src/ModVerify.CliApp/Utilities/ModVerifyConsoleUtilities.cs index 6e6664a..b9ebff0 100644 --- a/src/ModVerify.CliApp/Utilities/ModVerifyConsoleUtilities.cs +++ b/src/ModVerify.CliApp/Utilities/ModVerifyConsoleUtilities.cs @@ -1,6 +1,8 @@ using AnakinRaW.ApplicationBase; using Figgle; using System; +using System.Collections.Generic; +using AET.ModVerify.Reporting; namespace AET.ModVerify.App.Utilities; @@ -21,10 +23,68 @@ public static void WriteHeader(string? version = null) Console.ResetColor(); Console.WriteLine(); } + ConsoleUtilities.WriteHorizontalLine('*', lineLength); ConsoleUtilities.WriteLineRight(author, lineLength); Console.WriteLine(); Console.WriteLine(); } + + public static void WriteSelectedTarget(VerificationTarget target) + { + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine("Selected Target:"); + Console.ForegroundColor = ConsoleColor.DarkGray; + ConsoleUtilities.PrintAsTable([ + ("Name", target.Name), + ("Type", target.IsGame ? "Game" : "Mod"), + ("Engine", target.Engine), + ("Version", target.Version ?? "n/a"), + ("Location", target.Location.TargetPath), + ], 120); + Console.ResetColor(); + } + + public static void WriteBaselineInfo(VerificationBaseline baseline, string? filePath) + { + if (baseline.IsEmpty) + return; + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine("Using Baseline:"); + Console.ForegroundColor = ConsoleColor.DarkGray; + + IList<(string, object)> baselineData = + [ + ("Version", baseline.Version?.ToString(2) ?? "n/a"), + ("Is Default", filePath is null), + ("Minimum Severity", baseline.MinimumSeverity.ToString()), + ("Entries", baseline.Count.ToString()) + ]; + if (!string.IsNullOrEmpty(filePath)) + baselineData.Add(("File Path", filePath)); + + ConsoleUtilities.PrintAsTable(baselineData, 120); + + if (baseline.Target is not null) + { + Console.ForegroundColor = ConsoleColor.DarkMagenta; + Console.WriteLine("Baseline Target:"); + Console.ForegroundColor = ConsoleColor.DarkGray; + + IList<(string, object)> targetData = [ + ("Name", baseline.Target.Name), + ("Type", baseline.Target.IsGame ? "Game" : "Mod"), + ("Engine", baseline.Target.Engine), + ("Version", baseline.Target.Version ?? "n/a"), + ]; + + if (baseline.Target.Location is not null) + targetData.Add(("Location", baseline.Target.Location.TargetPath)); + + ConsoleUtilities.PrintAsTable(targetData, 120); + } + Console.ResetColor(); + } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Utilities/PathUtilities.cs b/src/ModVerify.CliApp/Utilities/PathUtilities.cs new file mode 100644 index 0000000..561630f --- /dev/null +++ b/src/ModVerify.CliApp/Utilities/PathUtilities.cs @@ -0,0 +1,39 @@ +using System; +using System.Runtime.InteropServices; +using AnakinRaW.CommonUtilities.FileSystem.Normalization; + +namespace AET.ModVerify.App.Utilities; + +internal static class PathUtilities +{ + private static readonly string HomeVariable; + private static readonly string HomePath; + private static readonly StringComparison StringComparer; + + static PathUtilities() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + HomeVariable = "%USERPROFILE%"; + StringComparer = StringComparison.OrdinalIgnoreCase; + } + else + { + HomeVariable = "$HOME"; + StringComparer = StringComparison.Ordinal; + } + + HomePath = PathNormalizer.Normalize( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + PathNormalizeOptions.EnsureTrailingSeparator); + } + + internal static string MaskUsername(string path) + { + if (string.IsNullOrWhiteSpace(path)) + return path; + + var index = path.IndexOf(HomePath, StringComparer); + return index >= 0 ? path.Remove(index, HomePath.Length).Insert(index, HomeVariable) : path; + } +} \ No newline at end of file diff --git a/src/ModVerify/GameVerificationException.cs b/src/ModVerify/GameVerificationException.cs index 1096179..dba312b 100644 --- a/src/ModVerify/GameVerificationException.cs +++ b/src/ModVerify/GameVerificationException.cs @@ -7,23 +7,21 @@ namespace AET.ModVerify; public sealed class GameVerificationException : Exception { - private readonly string? _errorMessage = null; - public IReadOnlyCollection Errors { get; } private string ErrorMessage { get { - if (_errorMessage != null) - return _errorMessage; + if (field != null) + return field; var stringBuilder = new StringBuilder(); foreach (var error in Errors) stringBuilder.AppendLine($"Verification error: {error.Id}: {error.Message};"); return stringBuilder.ToString().TrimEnd(';'); } - } + } = null; /// public override string Message => ErrorMessage; diff --git a/src/ModVerify/ModVerify.csproj b/src/ModVerify/ModVerify.csproj index 427458d..73d45e0 100644 --- a/src/ModVerify/ModVerify.csproj +++ b/src/ModVerify/ModVerify.csproj @@ -25,18 +25,18 @@ - + - + - - - - + + + + @@ -51,8 +51,4 @@ - - - - diff --git a/src/ModVerify/Pipeline/GameVerifierPipelineStep.cs b/src/ModVerify/Pipeline/GameVerifierPipelineStep.cs index 6405dbd..e83cb25 100644 --- a/src/ModVerify/Pipeline/GameVerifierPipelineStep.cs +++ b/src/ModVerify/Pipeline/GameVerifierPipelineStep.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using System; using System.Threading; +using System.Threading.Tasks; using AET.ModVerify.Pipeline.Progress; namespace AET.ModVerify.Pipeline; @@ -22,18 +23,19 @@ public sealed class GameVerifierPipelineStep( public long Size => 1; - protected override void RunCore(CancellationToken token) + protected override Task RunCoreAsync(CancellationToken token) { try { Logger?.LogDebug("Running verifier '{Name}'...", GameVerifier.FriendlyName); ReportProgress(new ProgressEventArgs(0.0, "Started")); - + GameVerifier.Progress += OnVerifyProgress; GameVerifier.Verify(token); Logger?.LogDebug("Finished verifier '{Name}'", GameVerifier.FriendlyName); ReportProgress(new ProgressEventArgs(1.0, "Finished")); + return Task.CompletedTask; } finally { diff --git a/src/ModVerify/Pipeline/GameVerifyPipeline.cs b/src/ModVerify/Pipeline/GameVerifyPipeline.cs index 810651a..493aa8c 100644 --- a/src/ModVerify/Pipeline/GameVerifyPipeline.cs +++ b/src/ModVerify/Pipeline/GameVerifyPipeline.cs @@ -1,7 +1,6 @@ -using AET.ModVerify.Reporting; -using AET.ModVerify.Reporting.Settings; +using AET.ModVerify.Pipeline.Progress; +using AET.ModVerify.Reporting; using AET.ModVerify.Settings; -using AET.ModVerify.Utilities; using AET.ModVerify.Verifiers; using AnakinRaW.CommonUtilities.SimplePipeline; using AnakinRaW.CommonUtilities.SimplePipeline.Runners; @@ -12,113 +11,127 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using AET.ModVerify.Pipeline.Progress; +using AET.ModVerify.Utilities; +using Microsoft.Extensions.DependencyInjection; namespace AET.ModVerify.Pipeline; -public sealed class GameVerifyPipeline : AnakinRaW.CommonUtilities.SimplePipeline.Pipeline +public sealed class GameVerifyPipeline : StepRunnerPipelineBase { - private readonly List _verifiers = new(); - private readonly List _verificationSteps = new(); - private readonly StepRunnerBase _verifyRunner; - - private readonly IStarWarsGameEngine _gameEngine; - private readonly IGameEngineErrorCollection _engineErrors; + private readonly List _verifiers = []; + private readonly List _verificationSteps = []; + private readonly ConcurrentGameEngineErrorReporter _engineErrorReporter = new(); + private readonly VerificationTarget _verificationTarget; private readonly VerifyPipelineSettings _pipelineSettings; - private readonly GlobalVerifyReportSettings _reportSettings; - private readonly IVerifyProgressReporter _progressReporter; - - protected override bool FailFast { get; } + private readonly IGameEngineInitializationReporter? _engineInitializationReporter; + private readonly IPetroglyphStarWarsGameEngineService _gameEngineService; + private readonly ILogger? _logger; + private AggregatedVerifyProgressReporter? _aggregatedVerifyProgressReporter; public IReadOnlyCollection FilteredErrors { get; private set; } = []; - + public VerificationBaseline Baseline { get; } + public SuppressionList Suppressions { get; } + public GameVerifyPipeline( - IStarWarsGameEngine gameEngine, - IGameEngineErrorCollection engineErrors, - VerifyPipelineSettings pipelineSettings, - GlobalVerifyReportSettings reportSettings, + VerificationTarget verificationTarget, + VerifyPipelineSettings pipelineSettings, IVerifyProgressReporter progressReporter, + IGameEngineInitializationReporter? engineInitializationReporter, + VerificationBaseline baseline, + SuppressionList suppressions, IServiceProvider serviceProvider) : base(serviceProvider) { - _gameEngine = gameEngine ?? throw new ArgumentNullException(nameof(gameEngine)); - _engineErrors = engineErrors ?? throw new ArgumentNullException(nameof(gameEngine)); + Baseline = baseline ?? throw new ArgumentNullException(nameof(baseline)); + Suppressions = suppressions ?? throw new ArgumentNullException(nameof(suppressions)); + _verificationTarget = verificationTarget ?? throw new ArgumentNullException(nameof(verificationTarget)); _pipelineSettings = pipelineSettings ?? throw new ArgumentNullException(nameof(pipelineSettings)); - _reportSettings = reportSettings ?? throw new ArgumentNullException(nameof(reportSettings)); _progressReporter = progressReporter ?? throw new ArgumentNullException(nameof(progressReporter)); + _engineInitializationReporter = engineInitializationReporter; + _gameEngineService = serviceProvider.GetRequiredService(); + _logger = serviceProvider.GetService()?.CreateLogger(GetType()); - if (pipelineSettings.ParallelVerifiers is < 0 or > 64) - throw new ArgumentException("_pipelineSettings has invalid parallel worker number.", nameof(pipelineSettings)); - - if (pipelineSettings.ParallelVerifiers == 1) - _verifyRunner = new SequentialStepRunner(serviceProvider); - else - _verifyRunner = new ParallelStepRunner(pipelineSettings.ParallelVerifiers, serviceProvider); + FailFast = pipelineSettings.FailFastSettings.IsFailFast; + } - FailFast = pipelineSettings.FailFast; + protected override AsyncStepRunner CreateRunner() + { + var requestedRunnerCount = _pipelineSettings.ParallelVerifiers; + return requestedRunnerCount switch + { + < 0 or > 64 => throw new InvalidOperationException( + $"Invalid parallel worker count ({requestedRunnerCount}) specified in verifier settings."), + 1 => new SequentialStepRunner(ServiceProvider), + _ => new AsyncStepRunner(requestedRunnerCount, ServiceProvider) + }; } - protected override Task PrepareCoreAsync() + protected override async Task PrepareCoreAsync(CancellationToken token) { _verifiers.Clear(); - AddStep(new GameEngineErrorCollector(_engineErrors, _gameEngine, _pipelineSettings.GameVerifySettings, ServiceProvider)); - - foreach (var gameVerificationStep in CreateVerificationSteps(_gameEngine)) - AddStep(gameVerificationStep); - - return Task.FromResult(true); - } - - protected override async Task RunCoreAsync(CancellationToken token) - { - var aggregatedVerifyProgressReporter = new AggregatedVerifyProgressReporter(_progressReporter, _verificationSteps); + IStarWarsGameEngine gameEngine; try { - Logger?.LogInformation("Running game verifiers..."); - _verifyRunner.Error += OnError; - await _verifyRunner.RunAsync(token); + gameEngine = await _gameEngineService.InitializeAsync( + _verificationTarget.Engine, + _verificationTarget.Location, + _engineErrorReporter, + _engineInitializationReporter, + false, + CancellationToken.None).ConfigureAwait(false); } - finally + catch (Exception e) { - aggregatedVerifyProgressReporter.Dispose(); - _verifyRunner.Error -= OnError; - Logger?.LogDebug("Game verifiers finished."); + _logger?.LogError(e, "Creating game engine failed: {Message}", e.Message); + throw; } - token.ThrowIfCancellationRequested(); - - var failedSteps = _verifyRunner.ExecutedSteps.Where(p => - p.Error != null && !p.Error.IsExceptionType()).ToList(); + AddStep(new GameEngineErrorCollector(_engineErrorReporter, gameEngine, _pipelineSettings.GameVerifySettings, ServiceProvider)); - if (failedSteps.Count != 0) - throw new StepFailureException(failedSteps); + foreach (var gameVerificationStep in CreateVerificationSteps(gameEngine)) + AddStep(gameVerificationStep); + } + protected override void OnExecuteStarted() + { + Logger?.LogInformation("Running game verifiers..."); + _aggregatedVerifyProgressReporter = new AggregatedVerifyProgressReporter(_progressReporter, _verificationSteps); + _progressReporter.Report(0.0, $"Verifying {_verificationTarget.Name}...", VerifyProgress.ProgressType, default); + } + + protected override void OnExecuteCompleted() + { + Logger?.LogInformation("Game verifiers finished."); FilteredErrors = GetReportableErrors(_verifiers.SelectMany(s => s.VerifyErrors)).ToList(); + _progressReporter.Report(1.0, $"Finished Verifying {_verificationTarget.Name}", VerifyProgress.ProgressType, default); } - protected override void OnError(object sender, StepRunnerErrorEventArgs e) + protected override void OnRunnerExecutionError(object sender, StepRunnerErrorEventArgs e) { - if (FailFast && e.Exception is GameVerificationException v) + if (FailFast && e.Exception is GameVerificationException verificationException) { - // TODO: Apply globalMinSeverity - if (v.Errors.All(error => _reportSettings.Baseline.Contains(error) || _reportSettings.Suppressions.Suppresses(error))) + var minSeverity = _pipelineSettings.FailFastSettings.MinumumSeverity; + var ignoreError = verificationException.Errors + .Where(error => error.Severity >= minSeverity) + .All(error => Baseline.Contains(error) || Suppressions.Suppresses(error)); + if (ignoreError) return; } - base.OnError(sender, e); + base.OnRunnerExecutionError(sender, e); } - private IEnumerable CreateVerificationSteps(IStarWarsGameEngine database) + protected override IEnumerable GetFailedSteps(IEnumerable steps) { - return _pipelineSettings.VerifiersProvider.GetVerifiers(database, _pipelineSettings.GameVerifySettings, ServiceProvider); + return base.GetFailedSteps(steps).Where(s => s.Error is not GameVerificationException); } private void AddStep(GameVerifier verifier) { var verificationStep = new GameVerifierPipelineStep(verifier, ServiceProvider); - _verifyRunner.AddStep(verificationStep); + StepRunner.AddStep(verificationStep); _verificationSteps.Add(verificationStep); _verifiers.Add(verifier); } @@ -128,7 +141,18 @@ private IEnumerable GetReportableErrors(IEnumerable CreateVerificationSteps(IStarWarsGameEngine engine) + { + return _pipelineSettings.VerifiersProvider + .GetVerifiers(engine, _pipelineSettings.GameVerifySettings, ServiceProvider); + } + + protected override void DisposeResources() + { + base.DisposeResources(); + _aggregatedVerifyProgressReporter?.Dispose(); } } \ No newline at end of file diff --git a/src/ModVerify/Pipeline/Progress/AggregatedVerifyProgressReporter.cs b/src/ModVerify/Pipeline/Progress/AggregatedVerifyProgressReporter.cs index 361febe..cfdae25 100644 --- a/src/ModVerify/Pipeline/Progress/AggregatedVerifyProgressReporter.cs +++ b/src/ModVerify/Pipeline/Progress/AggregatedVerifyProgressReporter.cs @@ -59,6 +59,7 @@ protected override ProgressEventArgs CalculateAggregatedProg var progressInfo = new VerifyProgressInfo { TotalVerifiers = TotalStepCount, + IsDetailed = true }; return new ProgressEventArgs(totalProgress, progress.ProgressText, progressInfo); } diff --git a/src/ModVerify/Pipeline/Progress/VerifyProgressInfo.cs b/src/ModVerify/Pipeline/Progress/VerifyProgressInfo.cs index 1409239..cadeb0d 100644 --- a/src/ModVerify/Pipeline/Progress/VerifyProgressInfo.cs +++ b/src/ModVerify/Pipeline/Progress/VerifyProgressInfo.cs @@ -4,5 +4,5 @@ public struct VerifyProgressInfo { public bool IsDetailed { get; init; } - public int TotalVerifiers { get; internal set; } + public int TotalVerifiers { get; internal init; } } \ No newline at end of file diff --git a/src/ModVerify/Reporting/BaselineVerificationTarget.cs b/src/ModVerify/Reporting/BaselineVerificationTarget.cs new file mode 100644 index 0000000..6e21ddd --- /dev/null +++ b/src/ModVerify/Reporting/BaselineVerificationTarget.cs @@ -0,0 +1,23 @@ +using System.Text; +using PG.StarWarsGame.Engine; + +namespace AET.ModVerify.Reporting; + +public sealed class BaselineVerificationTarget +{ + public required GameEngineType Engine { get; init; } + public required string Name { get; init; } + public GameLocations? Location { get; init; } // Optional compared to Verification Target + public string? Version { get; init; } + public bool IsGame { get; init; } + + public override string ToString() + { + var sb = new StringBuilder($"[Name={Name};EngineType={Engine};IsGame={IsGame};"); + if (!string.IsNullOrEmpty(Version)) sb.Append($"Version={Version};"); + if (Location is not null) + sb.Append($"Location={Location};"); + sb.Append(']'); + return sb.ToString(); + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonBaselineParser.cs b/src/ModVerify/Reporting/Json/JsonBaselineParser.cs index ef2f5d1..669ef53 100644 --- a/src/ModVerify/Reporting/Json/JsonBaselineParser.cs +++ b/src/ModVerify/Reporting/Json/JsonBaselineParser.cs @@ -1,11 +1,10 @@ using System; using System.IO; using System.Text.Json; -using System.Text.Json.Nodes; namespace AET.ModVerify.Reporting.Json; -public static class JsonBaselineParser +internal static class JsonBaselineParser { public static VerificationBaseline Parse(Stream dataStream) { @@ -13,8 +12,8 @@ public static VerificationBaseline Parse(Stream dataStream) throw new ArgumentNullException(nameof(dataStream)); try { - var jsonNode = JsonNode.Parse(dataStream); - var jsonBaseline = ParseCore(jsonNode); + var jsonNode = JsonDocument.Parse(dataStream); + var jsonBaseline = EvaluateAndDeserialize(jsonNode); if (jsonBaseline is null) throw new InvalidBaselineException($"Unable to parse input from stream to {nameof(VerificationBaseline)}. Unknown Error!"); @@ -27,12 +26,11 @@ public static VerificationBaseline Parse(Stream dataStream) } } - private static JsonVerificationBaseline? ParseCore(JsonNode? jsonData) + private static JsonVerificationBaseline? EvaluateAndDeserialize(JsonDocument? json) { - if (jsonData is null) + if (json is null) return null; - - JsonBaselineSchema.Evaluate(jsonData); - return jsonData.Deserialize(); + JsonBaselineSchema.Evaluate(json.RootElement); + return json.Deserialize(); } } \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonBaselineSchema.cs b/src/ModVerify/Reporting/Json/JsonBaselineSchema.cs index 7c8b02a..12e3705 100644 --- a/src/ModVerify/Reporting/Json/JsonBaselineSchema.cs +++ b/src/ModVerify/Reporting/Json/JsonBaselineSchema.cs @@ -3,8 +3,9 @@ using System.Diagnostics; using System.Linq; using System.Text; -using System.Text.Json.Nodes; +using System.Text.Json; using Json.Schema; +using Json.Schema.Keywords; namespace AET.ModVerify.Reporting.Json; @@ -12,18 +13,20 @@ public static class JsonBaselineSchema { private static readonly JsonSchema Schema; private static readonly EvaluationOptions EvaluationOptions; - + private static readonly BuildOptions BuildOptions; + static JsonBaselineSchema() { - var evalvOptions = new EvaluationOptions + BuildOptions = new BuildOptions { - EvaluateAs = SpecVersion.Draft202012, - OutputFormat = OutputFormat.Hierarchical, - AllowReferencesIntoUnknownKeywords = false + Dialect = Dialect.Draft202012 }; Schema = GetCurrentSchema(); - EvaluationOptions = evalvOptions; + EvaluationOptions = new EvaluationOptions + { + OutputFormat = OutputFormat.Hierarchical + }; } /// @@ -31,11 +34,8 @@ static JsonBaselineSchema() /// /// The JSON node to evaluate. /// is not valid against the baseline JSON schema. - /// is . - public static void Evaluate(JsonNode json) + public static void Evaluate(JsonElement json) { - if (json == null) - throw new ArgumentNullException(nameof(json)); var result = Schema.Evaluate(json, EvaluationOptions); ThrowOnValidationError(result); } @@ -58,13 +58,17 @@ private static void ThrowOnValidationError(EvaluationResults result) private static KeyValuePair? GetFirstError(EvaluationResults result) { - if (result.HasErrors) - return result.Errors!.First(); - foreach (var child in result.Details) + if (result.Errors is not null) + return result.Errors.First(); + + if (result.Details is not null) { - var error = GetFirstError(child); - if (error is not null) - return error; + foreach (var child in result.Details) + { + var error = GetFirstError(child); + if (error is not null) + return error; + } } return null; } @@ -75,10 +79,12 @@ private static JsonSchema GetCurrentSchema() .Assembly.GetManifestResourceStream($"AET.ModVerify.Resources.Schemas.{GetVersionedPath()}.baseline.json"); Debug.Assert(resourceStream is not null); - var schema = JsonSchema.FromStream(resourceStream!).GetAwaiter().GetResult(); + var json = JsonDocument.Parse(resourceStream!).RootElement; + var schema = JsonSchema.Build(json, BuildOptions); + - var id = schema.GetId(); - if (id is null || !UriContainsVersion(id, VerificationBaseline.LatestVersionString)) + if (schema.Root.Keywords.FirstOrDefault(x => x.Handler is IdKeyword)?.Value is not Uri id + || !UriContainsVersion(id, VerificationBaseline.LatestVersionString)) throw new InvalidOperationException("Internal error: The embedded schema version does not match the expected baseline version!"); return schema; diff --git a/src/ModVerify/Reporting/Json/JsonGameLocation.cs b/src/ModVerify/Reporting/Json/JsonGameLocation.cs new file mode 100644 index 0000000..bd51993 --- /dev/null +++ b/src/ModVerify/Reporting/Json/JsonGameLocation.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; +using PG.StarWarsGame.Engine; + +namespace AET.ModVerify.Reporting.Json; + +internal class JsonGameLocation +{ + [JsonPropertyName("modPaths")] + public IReadOnlyList ModPaths { get; } + + [JsonPropertyName("gamePath")] + public string GamePath { get; } + + [JsonPropertyName("fallbackPaths")] + public IReadOnlyList FallbackPaths { get; } + + [JsonConstructor] + private JsonGameLocation(IReadOnlyList modPaths, string gamePath, IReadOnlyList fallbackPaths) + { + ModPaths = modPaths; + GamePath = gamePath; + FallbackPaths = fallbackPaths; + } + + public JsonGameLocation(GameLocations location) + { + ModPaths = location.ModPaths.ToArray(); + GamePath = location.GamePath; + FallbackPaths = location.FallbackPaths.ToArray(); + } + + public static GameLocations? ToLocation(JsonGameLocation? jsonLocation) + { + return jsonLocation is null + ? null + : new GameLocations(jsonLocation.ModPaths, jsonLocation.GamePath, jsonLocation.FallbackPaths); + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonVerificationBaseline.cs b/src/ModVerify/Reporting/Json/JsonVerificationBaseline.cs index 4688c98..0d9a1b7 100644 --- a/src/ModVerify/Reporting/Json/JsonVerificationBaseline.cs +++ b/src/ModVerify/Reporting/Json/JsonVerificationBaseline.cs @@ -10,6 +10,10 @@ internal class JsonVerificationBaseline [JsonPropertyName("version")] public Version? Version { get; } + [JsonPropertyName("target")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public JsonVerificationTarget? Target { get; } + [JsonPropertyName("minSeverity")] [JsonConverter(typeof(JsonStringEnumConverter))] public VerificationSeverity MinimumSeverity { get; } @@ -22,11 +26,17 @@ public JsonVerificationBaseline(VerificationBaseline baseline) Errors = baseline.Select(x => new JsonVerificationError(x)); Version = baseline.Version; MinimumSeverity = baseline.MinimumSeverity; + Target = baseline.Target is not null ? new JsonVerificationTarget(baseline.Target) : null; } [JsonConstructor] - private JsonVerificationBaseline(Version version, VerificationSeverity minimumSeverity, IEnumerable errors) + private JsonVerificationBaseline( + JsonVerificationTarget target, + Version version, + VerificationSeverity minimumSeverity, + IEnumerable errors) { + Target = target; Errors = errors; Version = version; MinimumSeverity = minimumSeverity; diff --git a/src/ModVerify/Reporting/Json/JsonVerificationTarget.cs b/src/ModVerify/Reporting/Json/JsonVerificationTarget.cs new file mode 100644 index 0000000..9495456 --- /dev/null +++ b/src/ModVerify/Reporting/Json/JsonVerificationTarget.cs @@ -0,0 +1,63 @@ +using System.Text.Json.Serialization; +using PG.StarWarsGame.Engine; + +namespace AET.ModVerify.Reporting.Json; + +internal class JsonVerificationTarget +{ + [JsonPropertyName("name")] + public string Name { get; } + + [JsonPropertyName("engine")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public GameEngineType Engine { get; } + + [JsonPropertyName("isGame")] + public bool IsGame { get; } + + [JsonPropertyName("version")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Version{ get; } + + [JsonPropertyName("location")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public JsonGameLocation? Location { get; } + + [JsonConstructor] + private JsonVerificationTarget( + string name, + string? version, + JsonGameLocation? location, + GameEngineType engine, + bool isGame) + { + Name = name; + Version = version; + Engine = engine; + Location = location; + IsGame = isGame; + } + + public JsonVerificationTarget(BaselineVerificationTarget target) + { + Name = target.Name; + Version = target.Version; + Engine = target.Engine; + Location = target.Location is null ? null : new JsonGameLocation(target.Location); + IsGame = target.IsGame; + } + + public static BaselineVerificationTarget? ToTarget(JsonVerificationTarget? jsonTarget) + { + if (jsonTarget is null) + return null; + return new BaselineVerificationTarget + { + Engine = jsonTarget.Engine, + Name = jsonTarget.Name, + Location = JsonGameLocation.ToLocation(jsonTarget.Location), + Version = jsonTarget.Version, + IsGame = jsonTarget.IsGame + }; + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/ConsoleReporter.cs b/src/ModVerify/Reporting/Reporters/ConsoleReporter.cs index 7ee435d..dbee50d 100644 --- a/src/ModVerify/Reporting/Reporters/ConsoleReporter.cs +++ b/src/ModVerify/Reporting/Reporters/ConsoleReporter.cs @@ -7,10 +7,10 @@ namespace AET.ModVerify.Reporting.Reporters; internal class ConsoleReporter( - VerifyReportSettings settings, + ReporterSettings settings, bool summaryOnly, IServiceProvider serviceProvider) : - ReporterBase(settings, serviceProvider) + ReporterBase(settings, serviceProvider) { public override Task ReportAsync(IReadOnlyCollection errors) { diff --git a/src/ModVerify/Reporting/Reporters/Engine/EngineErrorReporterBase.cs b/src/ModVerify/Reporting/Reporters/Engine/EngineErrorReporterBase.cs index 5ecbb6f..455212d 100644 --- a/src/ModVerify/Reporting/Reporters/Engine/EngineErrorReporterBase.cs +++ b/src/ModVerify/Reporting/Reporters/Engine/EngineErrorReporterBase.cs @@ -38,12 +38,13 @@ public ErrorData(string identifier, string message, IEnumerable context, ThrowHelper.ThrowIfNullOrEmpty(message); Identifier = identifier; Message = message; - Context = context; + Context = context ?? throw new ArgumentNullException(nameof(context)); Asset = asset; Severity = severity; } - public ErrorData(string identifier, string message, string asset, VerificationSeverity severity) : this(identifier, message, [], asset, severity) + public ErrorData(string identifier, string message, string asset, VerificationSeverity severity) + : this(identifier, message, [], asset, severity) { } } diff --git a/src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs b/src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs index cb6b258..6c5fb17 100644 --- a/src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs +++ b/src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs @@ -14,18 +14,10 @@ internal sealed class GameAssertErrorReporter(IGameRepository gameRepository, IS protected override ErrorData CreateError(EngineAssert assert) { - // TODO: Why is context not used atm? var context = new List(); - - if (assert.Value is not null) - context.Add($"value='{assert.Value}'"); - if (assert.Context is not null) - context.Add($"context='{assert.Context}'"); - - // The location is the only identifiable thing of an assert. 'Value' might be null, thus we cannot use it. - var asset = GetLocation(assert); - - return new ErrorData(GetIdFromError(assert.Kind), assert.Message, asset, VerificationSeverity.Warning); + context.AddRange(assert.Context); + context.Add($"location='{GetLocation(assert)}'"); + return new ErrorData(GetIdFromError(assert.Kind), assert.Message, context, assert.Value, VerificationSeverity.Warning); } private static string GetLocation(EngineAssert assert) diff --git a/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs b/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs index 941a9d5..348fbb1 100644 --- a/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs +++ b/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs @@ -12,7 +12,6 @@ internal class JsonReporter(JsonReporterSettings settings, IServiceProvider serv { public const string FileName = "VerificationResult.json"; - public override async Task ReportAsync(IReadOnlyCollection errors) { var report = new JsonVerificationReport(errors.Select(x => new JsonVerificationError(x))); diff --git a/src/ModVerify/Reporting/Reporters/ReporterBase.cs b/src/ModVerify/Reporting/Reporters/ReporterBase.cs index fd86119..df444e3 100644 --- a/src/ModVerify/Reporting/Reporters/ReporterBase.cs +++ b/src/ModVerify/Reporting/Reporters/ReporterBase.cs @@ -6,7 +6,7 @@ namespace AET.ModVerify.Reporting.Reporters; -public abstract class ReporterBase(T settings, IServiceProvider serviceProvider) : IVerificationReporter where T : VerifyReportSettings +public abstract class ReporterBase(T settings, IServiceProvider serviceProvider) : IVerificationReporter where T : ReporterSettings { protected IServiceProvider ServiceProvider { get; } = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); diff --git a/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs b/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs index 7de81eb..b8906ac 100644 --- a/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs +++ b/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs @@ -11,7 +11,7 @@ public static class VerificationReportersExtensions { public IServiceCollection RegisterJsonReporter() { - return RegisterJsonReporter(serviceCollection, new JsonReporterSettings + return serviceCollection.RegisterJsonReporter(new JsonReporterSettings { OutputDirectory = "." }); @@ -19,7 +19,7 @@ public IServiceCollection RegisterJsonReporter() public IServiceCollection RegisterTextFileReporter() { - return RegisterTextFileReporter(serviceCollection, new TextFileReporterSettings + return serviceCollection.RegisterTextFileReporter(new TextFileReporterSettings { OutputDirectory = "." }); @@ -27,7 +27,7 @@ public IServiceCollection RegisterTextFileReporter() public IServiceCollection RegisterConsoleReporter(bool summaryOnly = false) { - return RegisterConsoleReporter(serviceCollection, new VerifyReportSettings + return serviceCollection.RegisterConsoleReporter(new ReporterSettings { MinimumReportSeverity = VerificationSeverity.Error }, summaryOnly); @@ -43,7 +43,7 @@ public IServiceCollection RegisterTextFileReporter(TextFileReporterSettings sett return serviceCollection.AddSingleton(sp => new TextFileReporter(settings, sp)); } - public IServiceCollection RegisterConsoleReporter(VerifyReportSettings settings, + public IServiceCollection RegisterConsoleReporter(ReporterSettings settings, bool summaryOnly = false) { return serviceCollection.AddSingleton(sp => new ConsoleReporter(settings, summaryOnly, sp)); diff --git a/src/ModVerify/Reporting/Settings/FileBasedReporterSettings.cs b/src/ModVerify/Reporting/Settings/FileBasedReporterSettings.cs index c6233a1..759a6ab 100644 --- a/src/ModVerify/Reporting/Settings/FileBasedReporterSettings.cs +++ b/src/ModVerify/Reporting/Settings/FileBasedReporterSettings.cs @@ -2,13 +2,11 @@ namespace AET.ModVerify.Reporting.Settings; -public record FileBasedReporterSettings : VerifyReportSettings +public record FileBasedReporterSettings : ReporterSettings { - private readonly string _outputDirectory = Environment.CurrentDirectory; - public string OutputDirectory { - get => _outputDirectory; - init => _outputDirectory = string.IsNullOrEmpty(value) ? Environment.CurrentDirectory : value; - } + get; + init => field = string.IsNullOrEmpty(value) ? Environment.CurrentDirectory : value; + } = Environment.CurrentDirectory; } \ No newline at end of file diff --git a/src/ModVerify/Reporting/Settings/GlobalVerifyReportSettings.cs b/src/ModVerify/Reporting/Settings/GlobalVerifyReportSettings.cs index b376fe3..5a51436 100644 --- a/src/ModVerify/Reporting/Settings/GlobalVerifyReportSettings.cs +++ b/src/ModVerify/Reporting/Settings/GlobalVerifyReportSettings.cs @@ -1,8 +1,10 @@ namespace AET.ModVerify.Reporting.Settings; -public record GlobalVerifyReportSettings : VerifyReportSettings -{ - public VerificationBaseline Baseline { get; init; } = VerificationBaseline.Empty; +//public record GlobalVerifyReportSettings +//{ +// //public VerificationSeverity MinimumReportSeverity { get; init; } = VerificationSeverity.Information; - public SuppressionList Suppressions { get; init; } = SuppressionList.Empty; -} \ No newline at end of file +// public VerificationBaseline Baseline { get; init; } = VerificationBaseline.Empty; + +// public SuppressionList Suppressions { get; init; } = SuppressionList.Empty; +//} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Settings/VerifyReportSettings.cs b/src/ModVerify/Reporting/Settings/ReporterSettings.cs similarity index 81% rename from src/ModVerify/Reporting/Settings/VerifyReportSettings.cs rename to src/ModVerify/Reporting/Settings/ReporterSettings.cs index 1289822..ef33857 100644 --- a/src/ModVerify/Reporting/Settings/VerifyReportSettings.cs +++ b/src/ModVerify/Reporting/Settings/ReporterSettings.cs @@ -1,6 +1,6 @@ namespace AET.ModVerify.Reporting.Settings; -public record VerifyReportSettings +public record ReporterSettings { public VerificationSeverity MinimumReportSeverity { get; init; } = VerificationSeverity.Information; } \ No newline at end of file diff --git a/src/ModVerify/Reporting/VerificationBaseline.cs b/src/ModVerify/Reporting/VerificationBaseline.cs index c37539b..3f9c274 100644 --- a/src/ModVerify/Reporting/VerificationBaseline.cs +++ b/src/ModVerify/Reporting/VerificationBaseline.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using System.Text.Json; using System.Threading.Tasks; using AET.ModVerify.Reporting.Json; @@ -11,13 +12,15 @@ namespace AET.ModVerify.Reporting; public sealed class VerificationBaseline : IReadOnlyCollection { - public static readonly Version LatestVersion = new(2, 0); + public static readonly Version LatestVersion = new(2, 1); public static readonly string LatestVersionString = LatestVersion.ToString(2); - public static readonly VerificationBaseline Empty = new(VerificationSeverity.Information, []); + public static readonly VerificationBaseline Empty = new(VerificationSeverity.Information, [], null); private readonly HashSet _errors; + public BaselineVerificationTarget? Target { get; } + public Version? Version { get; } public VerificationSeverity MinimumSeverity { get; } @@ -25,18 +28,22 @@ public sealed class VerificationBaseline : IReadOnlyCollection public int Count => _errors.Count; + public bool IsEmpty => Count == 0; + internal VerificationBaseline(JsonVerificationBaseline baseline) { _errors = [..baseline.Errors.Select(x => new VerificationError(x))]; Version = baseline.Version; MinimumSeverity = baseline.MinimumSeverity; + Target = JsonVerificationTarget.ToTarget(baseline.Target); } - public VerificationBaseline(VerificationSeverity minimumSeverity, IEnumerable errors) + public VerificationBaseline(VerificationSeverity minimumSeverity, IEnumerable errors, BaselineVerificationTarget? target) { _errors = [..errors]; Version = LatestVersion; MinimumSeverity = minimumSeverity; + Target = target; } public bool Contains(VerificationError error) @@ -77,6 +84,10 @@ IEnumerator IEnumerable.GetEnumerator() public override string ToString() { - return $"Baseline [Version={Version}, MinSeverity={MinimumSeverity}, NumErrors={Count}]"; + var sb = new StringBuilder($"Baseline [Version={Version}, MinSeverity={MinimumSeverity}, NumErrors={Count}"); + if (Target is not null) + sb.Append($", Target={Target}"); + sb.Append(']'); + return sb.ToString(); } } \ No newline at end of file diff --git a/src/ModVerify/Reporting/VerificationError.cs b/src/ModVerify/Reporting/VerificationError.cs index 4174f32..538a8fd 100644 --- a/src/ModVerify/Reporting/VerificationError.cs +++ b/src/ModVerify/Reporting/VerificationError.cs @@ -117,6 +117,7 @@ public override int GetHashCode() public override string ToString() { - return $"[{Severity}] [{string.Join(" --> ", VerifierChain)}] {Id}: Message={Message}; Asset='{Asset}'; Context=[{string.Join(",", ContextEntries)}];"; + return $"[{Severity}] [{string.Join(" --> ", VerifierChain)}] " + + $"{Id}: Message={Message}; Asset='{Asset}'; Context=[{string.Join(",", ContextEntries)}];"; } } \ No newline at end of file diff --git a/src/ModVerify/Resources/Schemas/2.0/baseline.json b/src/ModVerify/Resources/Schemas/2.1/baseline.json similarity index 55% rename from src/ModVerify/Resources/Schemas/2.0/baseline.json rename to src/ModVerify/Resources/Schemas/2.1/baseline.json index 2520a58..da37c4d 100644 --- a/src/ModVerify/Resources/Schemas/2.0/baseline.json +++ b/src/ModVerify/Resources/Schemas/2.1/baseline.json @@ -1,9 +1,60 @@ { - "$id": "https://AlamoEngine-Tools.github.io/schemas/mod-verify/2.0/baseline", + "$id": "https://AlamoEngine-Tools.github.io/schemas/mod-verify/2.1/baseline", "$schema": "https://json-schema.org/draft/2020-12/schema", "description": "Represents a baseline for AET ModVerify", "type": "object", "$defs": { + "location": { + "type": "object", + "properties": { + "modPaths": { + "type": "array", + "items": { + "type": "string" + } + }, + "gamePath": { + "type": "string" + }, + "fallbackPaths": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "modPaths", + "gamePath", + "fallbackPaths" + ], + "additionalProperties": false + }, + "target": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "engine": { + "enum": [ "Eaw", "Foc" ] + }, + "location": { + "$ref": "#/$defs/location" + }, + "isGame": { + "type": "boolean" + } + }, + "required": [ + "name", + "engine" + ], + "additionalProperties": false + }, "severity": { "enum": [ "Information", "Warning", "Error", "Critical" ] }, @@ -48,7 +99,7 @@ }, "properties": { "version": { - "const": "2.0" + "const": "2.1" }, "minSeverity": { "$ref": "#/$defs/severity" @@ -59,6 +110,9 @@ "$ref": "#/$defs/error" }, "additionalItems": false + }, + "target": { + "$ref": "#/$defs/target" } }, "required": [ diff --git a/src/ModVerify/Settings/VerifyPipelineSettings.cs b/src/ModVerify/Settings/VerifyPipelineSettings.cs index 2e11c74..db65969 100644 --- a/src/ModVerify/Settings/VerifyPipelineSettings.cs +++ b/src/ModVerify/Settings/VerifyPipelineSettings.cs @@ -1,4 +1,5 @@ using AET.ModVerify.Pipeline; +using AET.ModVerify.Reporting; namespace AET.ModVerify.Settings; @@ -8,7 +9,22 @@ public sealed class VerifyPipelineSettings public required IGameVerifiersProvider VerifiersProvider { get; init; } - public bool FailFast { get; init; } + public FailFastSetting FailFastSettings { get; init; } = FailFastSetting.NoFailFast; public int ParallelVerifiers { get; init; } = 4; +} + +public readonly struct FailFastSetting +{ + public static readonly FailFastSetting NoFailFast = default; + + public readonly bool IsFailFast; + + public readonly VerificationSeverity MinumumSeverity; + + public FailFastSetting(VerificationSeverity severity) + { + IsFailFast = true; + MinumumSeverity = severity; + } } \ No newline at end of file diff --git a/src/ModVerify/VerificationTarget.cs b/src/ModVerify/VerificationTarget.cs new file mode 100644 index 0000000..c0d5b97 --- /dev/null +++ b/src/ModVerify/VerificationTarget.cs @@ -0,0 +1,41 @@ +using System; +using System.Text; +using PG.StarWarsGame.Engine; + +namespace AET.ModVerify; + +public sealed class VerificationTarget +{ + public required GameEngineType Engine { get; init; } + + public required string Name + { + get; + init + { + if (string.IsNullOrEmpty(value)) + throw new ArgumentNullException(nameof(value)); + field = value; + } + } + + public required GameLocations Location + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } + + public string? Version { get; init; } + + public bool IsGame => Location.ModPaths.Count == 0; + + public override string ToString() + { + var sb = new StringBuilder($"[Name={Name};EngineType={Engine};"); + if (!string.IsNullOrEmpty(Version)) + sb.Append($"Version={Version};"); + sb.Append($"Location={Location};"); + sb.Append(']'); + return sb.ToString(); + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/DuplicateNameFinder.cs b/src/ModVerify/Verifiers/DuplicateNameFinder.cs index 23ab1b8..0b3b585 100644 --- a/src/ModVerify/Verifiers/DuplicateNameFinder.cs +++ b/src/ModVerify/Verifiers/DuplicateNameFinder.cs @@ -40,10 +40,10 @@ private void CheckForDuplicateCrcEntries( string sourceName, TSource source, Func> crcSelector, - Func> entrySelector, + Func> entrySelector, Func entryToStringSelector, - Func, IEnumerable> contextSelector, - Func, string, string> errorMessageCreator) + Func, IEnumerable> contextSelector, + Func, string, string> errorMessageCreator) { foreach (var crc32 in crcSelector(source)) { @@ -87,13 +87,13 @@ private void CheckXmlObjectsForDuplicates(string databaseName, IGameManager entries, string fileName) + private static string CreateDuplicateMtdErrorMessage(ImmutableFrugalList entries, string fileName) { var firstEntry = entries.First(); return $"MTD File '{fileName}' has duplicate definitions for CRC ({firstEntry}): {string.Join(",", entries.Select(x => x.FileName))}"; } - private static string CreateDuplicateXmlErrorMessage(ReadOnlyFrugalList entries, string databaseName) where T : NamedXmlObject + private static string CreateDuplicateXmlErrorMessage(ImmutableFrugalList entries, string databaseName) where T : NamedXmlObject { var firstEntry = entries.First(); var message = $"{databaseName} '{firstEntry.Name}' ({firstEntry.Crc32}) has duplicate definitions: "; diff --git a/src/ModVerify/Verifiers/GameVerifierBase.cs b/src/ModVerify/Verifiers/GameVerifierBase.cs index 8c9d67d..02bacc4 100644 --- a/src/ModVerify/Verifiers/GameVerifierBase.cs +++ b/src/ModVerify/Verifiers/GameVerifierBase.cs @@ -15,10 +15,8 @@ namespace AET.ModVerify.Verifiers; public abstract class GameVerifierBase : IGameVerifierInfo { public event EventHandler? Error; - public event EventHandler>? Progress; - private readonly IStarWarsGameEngine _gameEngine; private readonly ConcurrentDictionary _verifyErrors = new(); protected readonly IFileSystem FileSystem; @@ -35,7 +33,7 @@ public abstract class GameVerifierBase : IGameVerifierInfo protected IStarWarsGameEngine GameEngine { get; } - protected IGameRepository Repository => _gameEngine.GameRepository; + protected IGameRepository Repository => GameEngine.GameRepository; protected IReadOnlyList VerifierChain { get; } @@ -49,7 +47,6 @@ protected GameVerifierBase( throw new ArgumentNullException(nameof(serviceProvider)); FileSystem = serviceProvider.GetRequiredService(); Services = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - _gameEngine = gameEngine ?? throw new ArgumentNullException(nameof(gameEngine)); Parent = parent; Settings = settings ?? throw new ArgumentNullException(nameof(settings)); GameEngine = gameEngine ?? throw new ArgumentNullException(nameof(gameEngine)); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager.cs index 311c212..0b8687e 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using PG.Commons.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.CommandBar.Components; using PG.StarWarsGame.Engine.CommandBar.Xml; @@ -18,6 +17,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using AnakinRaW.CommonUtilities.Collections; namespace PG.StarWarsGame.Engine.CommandBar; @@ -77,7 +77,7 @@ protected override async Task InitializeCoreAsync(CancellationToken token) var contentParser = new XmlContainerContentParser(ServiceProvider, ErrorReporter); contentParser.XmlParseError += OnParseError; - var parsedCommandBarComponents = new ValueListDictionary(); + var parsedCommandBarComponents = new FrugalValueListDictionary(); try { @@ -213,8 +213,8 @@ private void SetDefaultFont() if (_defaultFont is null) { // TODO: From GameConstants - string fontName = PGConstants.DefaultUnicodeFontName; - int size = 11; + var fontName = PGConstants.DefaultUnicodeFontName; + var size = 11; var font = fontManager.CreateFont(fontName, size, true, false, false, 1.0f); if (font is null) ErrorReporter.Assert(EngineAssert.FromNullOrEmpty([ToString()], $"Unable to create Default from name {fontName}")); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameLocations.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameLocations.cs index 3b77732..f57b605 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameLocations.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameLocations.cs @@ -1,7 +1,8 @@ -using System; +using AnakinRaW.CommonUtilities; +using System; using System.Collections.Generic; using System.Linq; -using AnakinRaW.CommonUtilities; +using System.Text; namespace PG.StarWarsGame.Engine; @@ -27,13 +28,13 @@ public GameLocations(string modPath, string gamePath, string fallbackGamePath) : ThrowHelper.ThrowIfNullOrEmpty(modPath); } - public GameLocations(IList modPaths, string gamePath, string fallbackGamePath) : this(modPaths, - gamePath, [fallbackGamePath]) + public GameLocations(IReadOnlyList modPaths, string gamePath, string fallbackGamePath) + : this(modPaths, gamePath, [fallbackGamePath]) { ThrowHelper.ThrowIfNullOrEmpty(fallbackGamePath); } - public GameLocations(IList modPaths, string gamePath, IList fallbackPaths) + public GameLocations(IReadOnlyList modPaths, string gamePath, IReadOnlyList fallbackPaths) { if (modPaths == null) throw new ArgumentNullException(nameof(modPaths)); @@ -48,4 +49,19 @@ public GameLocations(IList modPaths, string gamePath, IList fall ? ModPaths[0] : GamePath; } + + public override string ToString() + { + var sb = new StringBuilder(); + + sb.AppendLine("GameLocation=["); + if (ModPaths.Count > 0) + sb.AppendLine($"Mods=[{string.Join(";", ModPaths)}];"); + sb.AppendLine($"Game=[{GamePath}];"); + if (FallbackPaths.Count > 0) + sb.AppendLine($"Fallbacks=[{string.Join(";", FallbackPaths)}];"); + sb.AppendLine("]"); + + return sb.ToString(); + } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameManagerBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameManagerBase.cs index 130fb50..607121d 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameManagerBase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameManagerBase.cs @@ -6,7 +6,6 @@ using AnakinRaW.CommonUtilities.Collections; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using PG.Commons.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.ErrorReporting; using PG.StarWarsGame.Engine.IO.Repositories; @@ -16,13 +15,13 @@ namespace PG.StarWarsGame.Engine; internal abstract class GameManagerBase(GameRepository repository, GameEngineErrorReporterWrapper errorReporter, IServiceProvider serviceProvider) : GameManagerBase(repository, errorReporter, serviceProvider), IGameManager { - protected readonly ValueListDictionary NamedEntries = new(); + protected readonly FrugalValueListDictionary NamedEntries = new(); public ICollection Entries => NamedEntries.Values; public ICollection EntryKeys => NamedEntries.Keys; - public ReadOnlyFrugalList GetEntries(Crc32 key) + public ImmutableFrugalList GetEntries(Crc32 key) { return NamedEntries.GetValues(key); } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/XmlComponentTextureData.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/XmlComponentTextureData.cs index 2ed95ce..f102457 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/XmlComponentTextureData.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/XmlComponentTextureData.cs @@ -1,14 +1,14 @@ using System; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.StarWarsGame.Engine.Xml; using PG.StarWarsGame.Files.XML; namespace PG.StarWarsGame.Engine.GuiDialog.Xml; -public class XmlComponentTextureData(string componentId, IReadOnlyValueListDictionary textures, XmlLocationInfo location) +public class XmlComponentTextureData(string componentId, IReadOnlyFrugalValueListDictionary textures, XmlLocationInfo location) : XmlObject(location) { public string Component { get; } = componentId ?? throw new ArgumentNullException(componentId); - public IReadOnlyValueListDictionary Textures { get; } = textures ?? throw new ArgumentNullException(nameof(textures)); + public IReadOnlyFrugalValueListDictionary Textures { get; } = textures ?? throw new ArgumentNullException(nameof(textures)); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IGameEngineInitializationReporter.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IGameEngineInitializationReporter.cs new file mode 100644 index 0000000..2ff43bd --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IGameEngineInitializationReporter.cs @@ -0,0 +1,10 @@ +namespace PG.StarWarsGame.Engine; + +public interface IGameEngineInitializationReporter +{ + void ReportProgress(string message); + + void ReportStarted(); + + void ReportFinished(); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IGameManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IGameManager.cs index b9f60a7..750f00c 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IGameManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IGameManager.cs @@ -10,5 +10,5 @@ public interface IGameManager ICollection EntryKeys { get; } - ReadOnlyFrugalList GetEntries(Crc32 key); + ImmutableFrugalList GetEntries(Crc32 key); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IPetroglyphStarWarsGameEngineService.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IPetroglyphStarWarsGameEngineService.cs index 314ce06..80aaa61 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IPetroglyphStarWarsGameEngineService.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IPetroglyphStarWarsGameEngineService.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading; +using System.Threading; using System.Threading.Tasks; using PG.StarWarsGame.Engine.ErrorReporting; @@ -11,7 +10,7 @@ public Task InitializeAsync( GameEngineType engineType, GameLocations gameLocations, IGameEngineErrorReporter? errorReporter = null, - IProgress? initProgress = null, + IGameEngineInitializationReporter? initReporter = null, bool cancelOnInitializationError = false, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj index e54e6f0..1376246 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj @@ -23,10 +23,10 @@ - - - - + + + + @@ -39,7 +39,4 @@ - - - \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs index 8ce1a5a..02a8d9f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs @@ -27,7 +27,7 @@ public async Task InitializeAsync( GameEngineType engineType, GameLocations gameLocations, IGameEngineErrorReporter? errorReporter = null, - IProgress? progress = null, + IGameEngineInitializationReporter? initReporter = null, bool cancelOnInitializationError = false, CancellationToken cancellationToken = default) @@ -39,7 +39,7 @@ public async Task InitializeAsync( try { - return await InitializeEngine(engineType, gameLocations, errorListenerWrapper, progress, cts.Token) + return await InitializeEngineAsync(engineType, gameLocations, errorListenerWrapper, initReporter, cts.Token) .ConfigureAwait(false); } finally @@ -56,16 +56,17 @@ void OnInitializationError(object sender, InitializationError e) } } - private async Task InitializeEngine( + private async Task InitializeEngineAsync( GameEngineType engineType, GameLocations gameLocations, GameEngineErrorReporterWrapper errorReporter, - IProgress? progress, + IGameEngineInitializationReporter? initReporter, CancellationToken token) { try { _logger?.LogInformation("Initializing game engine for type '{GameEngineType}'.", engineType); + initReporter?.ReportStarted(); var repoFactory = _serviceProvider.GetRequiredService(); var repository = repoFactory.Create(engineType, gameLocations, errorReporter); @@ -73,7 +74,7 @@ private async Task InitializeEngine( var pgRender = new PGRender(repository, errorReporter, serviceProvider); var gameConstants = new GameConstants.GameConstants(repository, errorReporter, serviceProvider); - progress?.Report("Initializing GameConstants"); + initReporter?.ReportProgress("Initializing GameConstants"); await gameConstants.InitializeAsync(token); // AudioConstants @@ -81,23 +82,23 @@ private async Task InitializeEngine( // MousePointer var fontManger = new FontManager(repository, errorReporter, serviceProvider); - progress?.Report("Initializing FontManager"); + initReporter?.ReportProgress("Initializing FontManager"); await fontManger.InitializeAsync(token); var guiDialogs = new GuiDialogGameManager(repository, errorReporter, serviceProvider); - progress?.Report("Initializing GUIDialogManager"); + initReporter?.ReportProgress("Initializing GUIDialogManager"); await guiDialogs.InitializeAsync(token); var sfxGameManager = new SfxEventGameManager(repository, errorReporter, serviceProvider); - progress?.Report("Initializing SFXManager"); + initReporter?.ReportProgress("Initializing SFXManager"); await sfxGameManager.InitializeAsync(token); var commandBarManager = new CommandBarGameManager(repository, pgRender, gameConstants, fontManger, errorReporter, serviceProvider); - progress?.Report("Initializing CommandBar"); + initReporter?.ReportProgress("Initializing CommandBar"); await commandBarManager.InitializeAsync(token); var gameObjetTypeManager = new GameObjectTypeGameManager(repository, errorReporter, serviceProvider); - progress?.Report("Initializing GameObjectTypeManager"); + initReporter?.ReportProgress("Initializing GameObjectTypeManager"); await gameObjetTypeManager.InitializeAsync(token); token.ThrowIfCancellationRequested(); @@ -120,6 +121,7 @@ private async Task InitializeEngine( } finally { + initReporter?.ReportFinished(); _logger?.LogDebug("Finished initializing game database."); } } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/AnimationCollection.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/AnimationCollection.cs index 03916fe..dfd43aa 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/AnimationCollection.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/AnimationCollection.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using AnakinRaW.CommonUtilities; using AnakinRaW.CommonUtilities.Collections; -using PG.Commons.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Files.ALO.Files.Animations; @@ -13,10 +12,10 @@ public sealed class AnimationCollection : DisposableObject, IEnumerable _animations = new(); - private readonly ValueListDictionary _animationCrc = new(); + private readonly FrugalValueListDictionary _animations = new(); + private readonly FrugalValueListDictionary _animationCrc = new(); - public int Cout => _animations.Count; + public int Cout => _animations.ValueCount; public Crc32 GetAnimationCrc(ModelAnimationType type, int subIndex) { @@ -28,12 +27,12 @@ public Crc32 GetAnimationCrc(ModelAnimationType type, int subIndex) return checksumsForType[subIndex]; } - public ReadOnlyFrugalList GetAnimations(ModelAnimationType type) + public ImmutableFrugalList GetAnimations(ModelAnimationType type) { return _animations.GetValues(type); } - public bool TryGetAnimations(ModelAnimationType type, out ReadOnlyFrugalList animations) + public bool TryGetAnimations(ModelAnimationType type, out ImmutableFrugalList animations) { return _animations.TryGetValues(type, out animations); } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/WindowsFontManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/WindowsFontManager.cs index f11374e..fe8f546 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/WindowsFontManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/WindowsFontManager.cs @@ -28,8 +28,8 @@ public IEnumerable GetFontFamilies() return fonts; } - static IEnumerable<(Gdi32.ENUMLOGFONTEXDV lpelfe, Gdi32.ENUMTEXTMETRIC lpntme, Gdi32.FontType FontType)> GetFonts(Gdi32.SafeHDC hdc) + static IEnumerable<(Gdi32.ENUMLOGFONTEXDV lpelfe, Gdi32.ENUMTEXTMETRIC _, Gdi32.FontType __)> GetFonts(Gdi32.SafeHDC hdc) { - return Gdi32.EnumFontFamiliesEx(hdc, CharacterSet.DEFAULT_CHARSET); + return Gdi32.EnumFontFamiliesEx(hdc, lfCharSet: CharacterSet.DEFAULT_CHARSET); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/CommandBarComponentParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/CommandBarComponentParser.cs index 1e20a8b..297b474 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/CommandBarComponentParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/CommandBarComponentParser.cs @@ -1,7 +1,7 @@ using System; using System.Collections.ObjectModel; using System.Xml.Linq; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.CommandBar.Xml; using PG.StarWarsGame.Engine.Xml.Tags; @@ -12,7 +12,7 @@ namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; public sealed class CommandBarComponentParser( - IReadOnlyValueListDictionary parsedElements, + IReadOnlyFrugalValueListDictionary parsedElements, IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : XmlObjectParser(parsedElements, serviceProvider, errorReporter) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs index 4f445a7..5cac3e2 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs @@ -1,6 +1,6 @@ using System; using System.Xml.Linq; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.GameObjects; using PG.StarWarsGame.Files.XML; @@ -26,7 +26,7 @@ public static class GameObjectXmlTags } public sealed class GameObjectParser( - IReadOnlyValueListDictionary parsedElements, + IReadOnlyFrugalValueListDictionary parsedElements, IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : XmlObjectParser(parsedElements, serviceProvider, errorReporter) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs index 4e3a5cb..bc3439f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs @@ -1,7 +1,7 @@ using System; using System.Collections.ObjectModel; using System.Xml.Linq; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.Audio.Sfx; using PG.StarWarsGame.Engine.Xml.Tags; @@ -12,7 +12,7 @@ namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; public sealed class SfxEventParser( - IReadOnlyValueListDictionary parsedElements, + IReadOnlyFrugalValueListDictionary parsedElements, IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : XmlObjectParser(parsedElements, serviceProvider, errorReporter) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/CommandBarComponentFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/CommandBarComponentFileParser.cs index a32ff09..763d244 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/CommandBarComponentFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/CommandBarComponentFileParser.cs @@ -1,6 +1,6 @@ using System; using System.Xml.Linq; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.CommandBar.Xml; using PG.StarWarsGame.Engine.Xml.Parsers.Data; @@ -12,7 +12,7 @@ namespace PG.StarWarsGame.Engine.Xml.Parsers.File; internal class CommandBarComponentFileParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : PetroglyphXmlFileContainerParser(serviceProvider, errorReporter) { - protected override void Parse(XElement element, IValueListDictionary parsedElements, string fileName) + protected override void Parse(XElement element, IFrugalValueListDictionary parsedElements, string fileName) { var parser = new CommandBarComponentParser(parsedElements, ServiceProvider, ErrorReporter); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileParser.cs index d2abfed..ffeafff 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileParser.cs @@ -1,6 +1,6 @@ using System; using System.Xml.Linq; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.GameObjects; using PG.StarWarsGame.Engine.Xml.Parsers.Data; @@ -12,7 +12,7 @@ namespace PG.StarWarsGame.Engine.Xml.Parsers.File; internal class GameObjectFileParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : PetroglyphXmlFileContainerParser(serviceProvider, errorReporter) { - protected override void Parse(XElement element, IValueListDictionary parsedElements, string fileName) + protected override void Parse(XElement element, IFrugalValueListDictionary parsedElements, string fileName) { var parser = new GameObjectParser(parsedElements, ServiceProvider, ErrorReporter); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GuiDialogParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GuiDialogParser.cs index 7c23dd6..851aa9f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GuiDialogParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GuiDialogParser.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Xml.Linq; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.StarWarsGame.Engine.GuiDialog.Xml; using PG.StarWarsGame.Files.XML; using PG.StarWarsGame.Files.XML.ErrorHandling; @@ -49,7 +49,7 @@ private GuiDialogsXmlTextureData ParseTextures(XElement? element, string fileNam private XmlComponentTextureData ParseTexture(XElement texture) { var componentId = GetTagName(texture); - var textures = new ValueListDictionary(); + var textures = new FrugalValueListDictionary(); foreach (var entry in texture.Elements()) textures.Add(entry.Name.ToString(), PetroglyphXmlStringParser.Instance.Parse(entry)); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs index f40dd1a..841d805 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs @@ -1,6 +1,6 @@ using System; using System.Xml.Linq; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.Audio.Sfx; using PG.StarWarsGame.Engine.Xml.Parsers.Data; @@ -12,7 +12,7 @@ namespace PG.StarWarsGame.Engine.Xml.Parsers.File; internal class SfxEventFileParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : PetroglyphXmlFileContainerParser(serviceProvider, errorReporter) { - protected override void Parse(XElement element, IValueListDictionary parsedElements, string fileName) + protected override void Parse(XElement element, IFrugalValueListDictionary parsedElements, string fileName) { var parser = new SfxEventParser(parsedElements, ServiceProvider, ErrorReporter); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs index 0c911e2..00f4f61 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs @@ -1,9 +1,9 @@ using System; using System.Linq; using System.Xml; +using AnakinRaW.CommonUtilities.Collections; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using PG.Commons.Collections; using PG.Commons.Hashing; using PG.Commons.Services; using PG.StarWarsGame.Engine.IO; @@ -34,7 +34,7 @@ public void ParseEntriesFromFileListXml( string xmlFile, IGameRepository gameRepository, string lookupPath, - ValueListDictionary entries, + FrugalValueListDictionary entries, Action? onFileParseAction = null) where T : notnull { Logger.LogDebug("Parsing container data '{XmlFile}'", xmlFile); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs index fead02d..1565e9e 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs @@ -1,7 +1,7 @@ using System; using System.Xml.Linq; +using AnakinRaW.CommonUtilities.Collections; using Microsoft.Extensions.DependencyInjection; -using PG.Commons.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Files.XML.ErrorHandling; using PG.StarWarsGame.Files.XML.Parsers; @@ -9,7 +9,7 @@ namespace PG.StarWarsGame.Engine.Xml.Parsers; public abstract class XmlObjectParser( - IReadOnlyValueListDictionary parsedElements, + IReadOnlyFrugalValueListDictionary parsedElements, IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : XmlObjectParser(parsedElements, serviceProvider, errorReporter) where TObject : XmlObject @@ -34,12 +34,12 @@ public readonly struct EmptyParseState public abstract class XmlObjectParser( - IReadOnlyValueListDictionary parsedElements, + IReadOnlyFrugalValueListDictionary parsedElements, IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : PetroglyphXmlElementParser(errorReporter) where TObject : XmlObject { - protected IReadOnlyValueListDictionary ParsedElements { get; } = + protected IReadOnlyFrugalValueListDictionary ParsedElements { get; } = parsedElements ?? throw new ArgumentNullException(nameof(parsedElements)); protected ICrc32HashingService HashingService { get; } = serviceProvider.GetRequiredService(); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/PG.StarWarsGame.Files.ALO.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/PG.StarWarsGame.Files.ALO.csproj index c653074..052190c 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/PG.StarWarsGame.Files.ALO.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/PG.StarWarsGame.Files.ALO.csproj @@ -24,7 +24,4 @@ - - - \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj index 36221be..f49643c 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj @@ -17,9 +17,6 @@ preview - - - - + \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/PG.StarWarsGame.Files.XML.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/PG.StarWarsGame.Files.XML.csproj index fbb055c..75fecfd 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/PG.StarWarsGame.Files.XML.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/PG.StarWarsGame.Files.XML.csproj @@ -18,8 +18,7 @@ preview - - + diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlFileContainerParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlFileContainerParser.cs index f42a9a9..cd21e00 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlFileContainerParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlFileContainerParser.cs @@ -1,10 +1,10 @@ using System.IO; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; namespace PG.StarWarsGame.Files.XML.Parsers; public interface IPetroglyphXmlFileContainerParser : IPetroglyphXmlParser where T : notnull { - void ParseFile(Stream xmlStream, IValueListDictionary parsedEntries); + void ParseFile(Stream xmlStream, IFrugalValueListDictionary parsedEntries); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileContainerParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileContainerParser.cs index 4e371e2..d2b862a 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileContainerParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileContainerParser.cs @@ -1,7 +1,7 @@ using System; using System.IO; using System.Xml.Linq; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Files.XML.ErrorHandling; @@ -10,12 +10,12 @@ namespace PG.StarWarsGame.Files.XML.Parsers; public abstract class PetroglyphXmlFileContainerParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? listener = null) : PetroglyphXmlFileParserBase(serviceProvider, listener), IPetroglyphXmlFileContainerParser where T : notnull { - public void ParseFile(Stream xmlStream, IValueListDictionary parsedEntries) + public void ParseFile(Stream xmlStream, IFrugalValueListDictionary parsedEntries) { var root = GetRootElement(xmlStream, out var fileName); if (root is not null) Parse(root, parsedEntries, fileName); } - protected abstract void Parse(XElement element, IValueListDictionary parsedElements, string fileName); + protected abstract void Parse(XElement element, IFrugalValueListDictionary parsedElements, string fileName); } \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/CommonTestBase.cs b/test/ModVerify.CliApp.Test/CommonTestBase.cs index 7d6dc18..8a93507 100644 --- a/test/ModVerify.CliApp.Test/CommonTestBase.cs +++ b/test/ModVerify.CliApp.Test/CommonTestBase.cs @@ -1,29 +1,22 @@ -using System; -using System.IO.Abstractions; +using AET.SteamAbstraction; using AnakinRaW.CommonUtilities.Hashing; +using AnakinRaW.CommonUtilities.Testing; using Microsoft.Extensions.DependencyInjection; using PG.Commons; -using Testably.Abstractions.Testing; +using PG.StarWarsGame.Infrastructure; +using PG.StarWarsGame.Infrastructure.Clients.Steam; namespace ModVerify.CliApp.Test; -public abstract class CommonTestBase +public abstract class CommonTestBase : TestBaseWithFileSystem { - protected readonly MockFileSystem FileSystem = new(); - protected readonly IServiceProvider ServiceProvider; - - protected CommonTestBase() - { - var sc = new ServiceCollection(); - sc.AddSingleton(sp => new HashingService(sp)); - sc.AddSingleton(FileSystem); - PetroglyphCommons.ContributeServices(sc); - // ReSharper disable once VirtualMemberCallInConstructor - SetupServices(sc); - ServiceProvider = sc.BuildServiceProvider(); - } - - protected virtual void SetupServices(ServiceCollection serviceCollection) + protected override void SetupServices(IServiceCollection serviceCollection) { + base.SetupServices(serviceCollection); + serviceCollection.AddSingleton(sp => new HashingService(sp)); + PetroglyphCommons.ContributeServices(serviceCollection); + PetroglyphGameInfrastructure.InitializeServices(serviceCollection); + SteamAbstractionLayer.InitializeServices(serviceCollection); + SteamPetroglyphStarWarsGameClients.InitializeServices(serviceCollection); } } \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/EmbeddedBaselineTest.cs b/test/ModVerify.CliApp.Test/EmbeddedBaselineTest.cs index cde1dad..ecf3253 100644 --- a/test/ModVerify.CliApp.Test/EmbeddedBaselineTest.cs +++ b/test/ModVerify.CliApp.Test/EmbeddedBaselineTest.cs @@ -1,31 +1,18 @@ using AET.ModVerify.App; using AET.ModVerify.App.Reporting; -using AET.ModVerify.App.Settings; -using AET.ModVerify.Settings; using AnakinRaW.ApplicationBase.Environment; using Microsoft.Extensions.DependencyInjection; using PG.StarWarsGame.Engine; using System; using System.IO.Abstractions; -using ModVerify.CliApp.Test.TestData; using Testably.Abstractions; +using Xunit; namespace ModVerify.CliApp.Test; public class BaselineSelectorTest { private static readonly IFileSystem FileSystem = new RealFileSystem(); - private static readonly ModVerifyAppSettings TestSettings = new() - { - ReportSettings = new(), - GameInstallationsSettings = new (), - VerifyPipelineSettings = new() - { - GameVerifySettings = new GameVerifySettings(), - VerifiersProvider = new NoVerifierProvider() - } - }; - private readonly IServiceProvider _serviceProvider; public BaselineSelectorTest() @@ -42,6 +29,6 @@ public BaselineSelectorTest() public void LoadEmbeddedBaseline(GameEngineType engineType) { // Ensure this operation does not crash, meaning the embedded baseline is at least compatible. - new BaselineSelector(TestSettings, _serviceProvider).LoadEmbeddedBaseline(engineType); + BaselineSelector.LoadEmbeddedBaseline(engineType); } } \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj b/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj index 84d1bba..826987d 100644 --- a/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj +++ b/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj @@ -3,31 +3,36 @@ net10.0 $(TargetFrameworks);net481 - false preview + + false + true + Exe + + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + + - - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - - - diff --git a/test/ModVerify.CliApp.Test/ModVerifyOptionsParserTest.cs b/test/ModVerify.CliApp.Test/ModVerifyOptionsParserTest.cs index 294baf7..c5caf92 100644 --- a/test/ModVerify.CliApp.Test/ModVerifyOptionsParserTest.cs +++ b/test/ModVerify.CliApp.Test/ModVerifyOptionsParserTest.cs @@ -4,7 +4,11 @@ using System.IO.Abstractions; using ModVerify.CliApp.Test.TestData; using Testably.Abstractions; +using Xunit; +#if NETFRAMEWORK using ModVerify.CliApp.Test.Utilities; +#endif + namespace ModVerify.CliApp.Test; diff --git a/test/ModVerify.CliApp.Test/TargetSelectors/AutomaticSelectorTest.cs b/test/ModVerify.CliApp.Test/TargetSelectors/AutomaticSelectorTest.cs new file mode 100644 index 0000000..f01d5cd --- /dev/null +++ b/test/ModVerify.CliApp.Test/TargetSelectors/AutomaticSelectorTest.cs @@ -0,0 +1,406 @@ +using AET.Modinfo.Model; +using AET.Modinfo.Spec.Steam; +using AET.ModVerify.App.GameFinder; +using AET.ModVerify.App.Settings; +using AET.ModVerify.App.TargetSelectors; +using AET.ModVerify.App.Utilities; +using AET.SteamAbstraction.Testing; +using AnakinRaW.CommonUtilities.Registry; +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Infrastructure.Games; +using PG.StarWarsGame.Infrastructure.Testing; +using PG.StarWarsGame.Infrastructure.Testing.Installations; +using PG.StarWarsGame.Infrastructure.Testing.Installations.Game; +using System; +using Xunit; + +namespace ModVerify.CliApp.Test.TargetSelectors; + +public class AutomaticSelectorTest : CommonTestBase +{ + private readonly AutomaticSelector _selector; + private readonly IRegistry _registry = new InMemoryRegistry(InMemoryRegistryCreationFlags.WindowsLike); + + public AutomaticSelectorTest() + { + _selector = new AutomaticSelector(ServiceProvider); + } + + protected override void SetupServices(IServiceCollection serviceCollection) + { + base.SetupServices(serviceCollection); + serviceCollection.AddSingleton(_registry); + } + + [Fact] + public void Test_SelectTarget_GameNotInstalled() + { + var settings = new VerificationTargetSettings + { + TargetPath = "/test", + }; + Assert.Throws(() => _selector.SelectTarget(settings)); + } + + [Fact] + public void Test_SelectTarget_WrongSettings() + { + var settings = new VerificationTargetSettings + { + TargetPath = "/test", + FallbackGamePath = "does/not/exist", + ModPaths = ["also/does/not/exist"], + GamePath = "not/found" + }; + Assert.Throws(() => _selector.SelectTarget(settings)); + } + + [Theory] + [MemberData(nameof(GITestUtilities.RealGameIdentities), MemberType = typeof(GITestUtilities))] + public void Test_SelectTarget_FromGamePath(IGameIdentity identity) + { + TestSelectTarget( + identity, + i => i, + gi => new VerificationTargetSettings + { + TargetPath = gi.PlayableObject.Directory.FullName, + Engine = null + }); + TestSelectTarget( + identity, + i => i, + gi => new VerificationTargetSettings + { + TargetPath = gi.PlayableObject.Directory.FullName, + Engine = gi.PlayableObject.Game.Type.ToEngineType() + }); + } + + [Theory] + [MemberData(nameof(GITestUtilities.RealGameIdentities), MemberType = typeof(GITestUtilities))] + public void Test_SelectTarget_FromGamePath_OppositeEngine_ThrowsGameNotFoundException(IGameIdentity identity) + { + TestSelectTarget(identity, + i => i, + gi => new VerificationTargetSettings + { + TargetPath = gi.PlayableObject.Directory.FullName, + Engine = gi.PlayableObject.Game.Type.Opposite().ToEngineType(), + }, + typeof(GameNotFoundException)); + } + + [Theory] + [MemberData(nameof(GITestUtilities.RealGameIdentities), MemberType = typeof(GITestUtilities))] + public void Test_SelectTarget_ModInModsDir(IGameIdentity identity) + { + TestSelectTarget( + identity, + gameInstallation => gameInstallation.InstallMod("MyMod", false), + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = null + }); + TestSelectTarget( + identity, + gameInstallation => gameInstallation.InstallMod("MyMod", false), + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = ti.GameInstallation.Game.Type.ToEngineType() + }); + } + + [Theory] + [MemberData(nameof(GITestUtilities.RealGameIdentities), MemberType = typeof(GITestUtilities))] + public void Test_SelectTarget_ModInModsDir_WrongGameEngine_Throws(IGameIdentity identity) + { + TestSelectTarget( + identity, + gameInstallation => gameInstallation.InstallMod("MyMod", false), + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = ti.GameInstallation.Game.Type.Opposite().ToEngineType() + }, typeof(ArgumentException)); + } + + + [Theory] + [InlineData(GameType.Eaw)] + [InlineData(GameType.Foc)] + public void Test_SelectTarget_Workshops_UnknownModEngineType(GameType gameType) + { + var identity = new GameIdentity(gameType, GamePlatform.SteamGold); + TestSelectTarget( + identity, + gameInstallation => gameInstallation.InstallMod("MyMod", true), + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = ti.GameInstallation.Game.Type.ToEngineType() + }); + + TestSelectTarget( + identity, + gameInstallation => gameInstallation.InstallMod("MyMod", true), + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = null // Causes fallback to FoC + }, + overrideAssertData: new OverrideAssertData { GameType = GameType.Foc }); + } + + [Theory] + [InlineData(GameType.Eaw)] + [InlineData(GameType.Foc)] + public void Test_SelectTarget_Workshops_UnspecifiedEngineIsCorrectEngineTyp_KnownModEngineType(GameType gameType) + { + var identity = new GameIdentity(gameType, GamePlatform.SteamGold); + + var modinfo = new ModinfoData("MyMod") + { + SteamData = new SteamData("123456", "123456", SteamWorkshopVisibility.Public, "MyMod", + [gameType.ToString().ToUpper()]) + }; + + TestSelectTarget( + identity, + gameInstallation => + { + var modInstallation = gameInstallation.InstallMod(modinfo, true); + modInstallation.InstallModinfoFile(modinfo); + return modInstallation; + }, + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = null + }); + } + + [Theory] + [InlineData(GameType.Eaw)] + [InlineData(GameType.Foc)] + public void Test_SelectTarget_Workshops_IncompatibleKnownModEngineType_Throws(GameType gameType) + { + var identity = new GameIdentity(gameType, GamePlatform.SteamGold); + + var modinfo = new ModinfoData("MyMod") + { + SteamData = new SteamData("123456", "123456", SteamWorkshopVisibility.Public, "MyMod", + [gameType.ToString().ToUpper()]) + }; + + TestSelectTarget( + identity, + gameInstallation => + { + var modInstallation = gameInstallation.InstallMod(modinfo, true); + modInstallation.InstallModinfoFile(modinfo); + return modInstallation; + }, + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = gameType.Opposite().ToEngineType() + }, + typeof(ArgumentException)); + } + + [Theory] + [InlineData(GameType.Eaw)] + [InlineData(GameType.Foc)] + public void Test_SelectTarget_Workshops_MultipleKnownModEngineTypes(GameType gameType) + { + var identity = new GameIdentity(gameType, GamePlatform.SteamGold); + + var modinfo = new ModinfoData("MyMod") + { + SteamData = new SteamData("123456", "123456", SteamWorkshopVisibility.Public, "MyMod", + [gameType.ToString().ToUpper(), gameType.Opposite().ToString().ToUpper()]) + }; + + TestSelectTarget( + identity, + gameInstallation => + { + var modInstallation = gameInstallation.InstallMod(modinfo, true); + modInstallation.InstallModinfoFile(modinfo); + return modInstallation; + }, + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = gameType.ToEngineType() + }); + + TestSelectTarget( + identity, + gameInstallation => + { + var modInstallation = gameInstallation.InstallMod(modinfo, true); + modInstallation.InstallModinfoFile(modinfo); + return modInstallation; + }, + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = null // Causes fallback to FoC + }, + overrideAssertData: new OverrideAssertData{GameType = GameType.Foc}); + } + + [Theory] + [MemberData(nameof(GITestUtilities.RealGameIdentities), MemberType = typeof(GITestUtilities))] + public void Test_SelectTarget_DetachedMod_NoEngineSpecified_Throws(IGameIdentity identity) + { + TestSelectTarget( + identity, + gameInstallation => + { + var modinfo = new ModinfoData("DetachedMod"); + var modPath = FileSystem.Directory.CreateDirectory("/detachedMod"); + return gameInstallation.InstallMod(modinfo, modPath, false); + }, + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = null // No Engine means we cannot proceed + }, + expectedExceptionType: typeof(ArgumentException)); + } + + [Theory] + [MemberData(nameof(GITestUtilities.RealGameIdentities), MemberType = typeof(GITestUtilities))] + public void Test_SelectTarget_DetachedMod(IGameIdentity identity) + { + TestSelectTarget( + identity, + gameInstallation => + { + var modinfo = new ModinfoData("DetachedMod"); + var modPath = FileSystem.Directory.CreateDirectory("/detachedMod"); + return gameInstallation.InstallMod(modinfo, modPath, false); + }, + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = identity.Type.ToEngineType() + }); + } + + [Theory] + [MemberData(nameof(GITestUtilities.RealGameIdentities), MemberType = typeof(GITestUtilities))] + public void Test_SelectTarget_AttachedMod_DoesNotExist(IGameIdentity identity) + { + // Currently, only Steam is supported for detached mods. + TestSelectTarget( + identity, + gameInstallation => + { + var modInstallation = gameInstallation.InstallMod("MyMod", GITestUtilities.GetRandomWorkshopFlag(identity)); + modInstallation.Mod.Directory.Delete(true); + return modInstallation; + }, + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = identity.Type.ToEngineType() + }, + typeof(TargetNotFoundException)); + } + + [Theory] + [MemberData(nameof(GITestUtilities.RealGameIdentities), MemberType = typeof(GITestUtilities))] + public void Test_SelectTarget_DetachedMod_DoesNotExist(IGameIdentity identity) + { + TestSelectTarget( + identity, + gameInstallation => + { + var modinfo = new ModinfoData("DetachedMod"); + var modPath = FileSystem.DirectoryInfo.New("/detachedMod"); + var modInstallation = gameInstallation.InstallMod(modinfo, modPath, false); + modPath.Delete(true); + return modInstallation; + }, + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = identity.Type.ToEngineType() + }, + typeof(TargetNotFoundException)); + } + + private void TestSelectTarget( + IGameIdentity identity, + Func targetFactory, + Func settingsFactory, + Type? expectedExceptionType = null, + OverrideAssertData? overrideAssertData = null) + { + var (eaw, foc) = InstallGames(identity.Platform); + var gameInstallation = identity.Type switch + { + GameType.Eaw => eaw, + GameType.Foc => foc, + _ => throw new ArgumentOutOfRangeException() + }; + + var targetInstallation = targetFactory(gameInstallation); + + var settings = settingsFactory(targetInstallation); + + if (expectedExceptionType is not null) + { + Assert.Throws(expectedExceptionType, () => _selector.SelectTarget(settings)); + return; + } + + var result = _selector.SelectTarget(settings); + Assert.Equal(overrideAssertData?.GameType ?? identity.Type, result.Engine.FromEngineType()); + Assert.Equal(targetInstallation.PlayableObject.GetType(), result.Target!.GetType()); + Assert.Equal(targetInstallation.PlayableObject.Directory.FullName, result.Locations.TargetPath); + + if (result.Engine == GameEngineType.Foc) + Assert.NotEmpty(result.Locations.FallbackPaths); + else + Assert.Empty(result.Locations.FallbackPaths); + } + + private (ITestingGameInstallation eaw, ITestingGameInstallation foc) InstallGames(GamePlatform platform) + { + var eaw = GameInfrastructureTesting.Game(new GameIdentity(GameType.Eaw, platform), ServiceProvider); + var foc = GameInfrastructureTesting.Game(new GameIdentity(GameType.Foc, platform), ServiceProvider); + + GameInfrastructureTesting.Registry(ServiceProvider).CreateInstalled(eaw.Game); + GameInfrastructureTesting.Registry(ServiceProvider).CreateInstalled(foc.Game); + + eaw.InstallMod("OtherEawMod"); + foc.InstallMod("OtherFocMod"); + + if (platform == GamePlatform.SteamGold) + InstallSteam(); + + return (eaw, foc); + } + + private void InstallSteam() + { + var steam = SteamTesting.Steam(ServiceProvider); + steam.Install(); + // Register Game to Steam + var lib = steam.InstallDefaultLibrary(); + lib.InstallGame(32470, "Star Wars Empire at War", [32472]); + } + + private class OverrideAssertData + { + public GameType? GameType { get; init; } + } +} \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/TargetSelectors/GameFinderServiceTest.cs b/test/ModVerify.CliApp.Test/TargetSelectors/GameFinderServiceTest.cs new file mode 100644 index 0000000..334d43f --- /dev/null +++ b/test/ModVerify.CliApp.Test/TargetSelectors/GameFinderServiceTest.cs @@ -0,0 +1,72 @@ +using AET.ModVerify.App.GameFinder; +using AnakinRaW.CommonUtilities.Registry; +using AnakinRaW.CommonUtilities.Testing.Extensions; +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Infrastructure.Games; +using PG.StarWarsGame.Infrastructure.Testing; +using PG.StarWarsGame.Infrastructure.Testing.Installations; +using Xunit; + +namespace ModVerify.CliApp.Test.TargetSelectors; + +public class GameFinderServiceTest : CommonTestBase +{ + private readonly IRegistry _registry = new InMemoryRegistry(InMemoryRegistryCreationFlags.WindowsLike); + private readonly GameFinderService _finderService; + public GameFinderServiceTest() + { + _finderService = new GameFinderService(ServiceProvider); + } + protected override void SetupServices(IServiceCollection serviceCollection) + { + base.SetupServices(serviceCollection); + serviceCollection.AddSingleton(_registry); + } + + [Theory] + [MemberData(nameof(GITestUtilities.RealGameIdentities), MemberType = typeof(GITestUtilities))] + public void FindGame_SearchFallbackGame_FocInstalledButEawNot_ThrowsGameNotFoundException(IGameIdentity identity) + { + if (identity.Type == GameType.Eaw) + return; + + var foc = GameInfrastructureTesting.Game(identity, ServiceProvider); + GameInfrastructureTesting.Registry(ServiceProvider).CreateInstalled(foc.Game); + + Assert.Throws(() => _finderService.FindGame(foc.Game.Directory.FullName, new GameFinderSettings + { + Engine = GameEngineType.Foc, + SearchFallbackGame = true + })); + + Assert.Throws(() => _finderService.FindGame(foc.Game.Directory.FullName, new GameFinderSettings + { + Engine = null, + SearchFallbackGame = true + })); + } + + [Theory] + [MemberData(nameof(GITestUtilities.RealGameIdentities), MemberType = typeof(GITestUtilities))] + public void FindGame_EngineEaw_SearchFallbackGameIsIgnored(IGameIdentity identity) + { + if (identity.Type == GameType.Foc) + return; + + var eaw = GameInfrastructureTesting.Game(identity, ServiceProvider); + GameInfrastructureTesting.Registry(ServiceProvider).CreateInstalled(eaw.Game); + + Assert.DoesNotThrowException(() => _finderService.FindGame(eaw.Game.Directory.FullName, new GameFinderSettings + { + Engine = GameEngineType.Eaw, + SearchFallbackGame = true + })); + + Assert.DoesNotThrowException(() => _finderService.FindGame(eaw.Game.Directory.FullName, new GameFinderSettings + { + Engine = null, + SearchFallbackGame = true + })); + } +} \ No newline at end of file