diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7369175..7a9b844 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,8 +13,8 @@ on: env: TOOL_PROJ_PATH: ./src/ModVerify.CliApp/ModVerify.CliApp.csproj - CREATOR_PROJ_PATH: ./Modules/ModdingToolBase/src/AnakinApps/ApplicationManifestCreator/ApplicationManifestCreator.csproj - UPLOADER_PROJ_PATH: ./Modules/ModdingToolBase/src/AnakinApps/FtpUploader/FtpUploader.csproj + CREATOR_PROJ_PATH: ./modules/ModdingToolBase/src/AnakinApps/ApplicationManifestCreator/ApplicationManifestCreator.csproj + UPLOADER_PROJ_PATH: ./modules/ModdingToolBase/src/AnakinApps/FtpUploader/FtpUploader.csproj TOOL_EXE: ModVerify.exe UPDATER_EXE: AnakinRaW.ExternalUpdater.exe MANIFEST_CREATOR: AnakinRaW.ApplicationManifestCreator.dll @@ -35,18 +35,20 @@ jobs: runs-on: windows-latest steps: - name: Checkout sources - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 submodules: recursive - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 - name: Create NetFramework Release - run: dotnet publish ${{ env.TOOL_PROJ_PATH }} --configuration Release -f net48 --output ./releases/net48 /p:DebugType=None /p:DebugSymbols=false + # use build for .NET Framework to enusre external updatere .EXE is included + run: dotnet build ${{ env.TOOL_PROJ_PATH }} --configuration Release -f net481 --output ./releases/net481 /p:DebugType=None /p:DebugSymbols=false - name: Create Net Core Release - run: dotnet publish ${{ env.TOOL_PROJ_PATH }} --configuration Release -f net9.0 --output ./releases/net9.0 /p:DebugType=None /p:DebugSymbols=false + # use publish for .NET Core + run: dotnet publish ${{ env.TOOL_PROJ_PATH }} --configuration Release -f net10.0 --output ./releases/net10.0 /p:DebugType=None /p:DebugSymbols=false - name: Upload a Build Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Binary Releases path: ./releases @@ -62,17 +64,45 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: actions/download-artifact@v5 + submodules: recursive + - uses: actions/download-artifact@v6 with: name: Binary Releases path: ./releases + + # Deploy .NET Framework self-update release + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + - name: Build Creator + run: dotnet build ${{env.CREATOR_PROJ_PATH}} --configuration Release --output ./dev + - name: Build Uploader + run: dotnet build ${{env.UPLOADER_PROJ_PATH}} --configuration Release --output ./dev + - name: Create binaries directory + run: mkdir -p ./deploy + - name: Copy self-update files + run: | + cp ./releases/net481/${{env.TOOL_EXE}} ./deploy/ + cp ./releases/net481/${{env.UPDATER_EXE}} ./deploy/ + - name: Create Manifest + run: dotnet ./dev/${{env.MANIFEST_CREATOR}} -a deploy/${{env.TOOL_EXE}} --appDataFiles deploy/${{env.UPDATER_EXE}} --origin ${{env.ORIGIN_BASE}} -o ./deploy -b ${{env.BRANCH_NAME}} + - name: Upload Build + run: dotnet ./dev/${{env.SFTP_UPLOADER}} ftp --host $host --port $port -u ${{secrets.SFTP_USER}} -p ${{secrets.SFTP_PASSWORD}} --base $base_path -s $source + env: + host: republicatwar.com + port: 1579 + base_path: ${{env.ORIGIN_BASE_PART}} + source: ./deploy + + # Deploy .NET Core and .NET Framework apps to Github - name: Create NET Core .zip # Change into the artifacts directory to avoid including the directory itself in the zip archive - working-directory: ./releases/net9.0 - run: zip -r ../ModVerify-Net9.zip . + working-directory: ./releases/net10.0 + run: zip -r ../ModVerify-Net10.zip . - uses: dotnet/nbgv@v0.4.2 id: nbgv - name: Create GitHub release @@ -86,5 +116,5 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} generate_release_notes: true files: | - ./releases/net48/ModVerify.exe - ./releases/ModVerify-Net9.zip \ No newline at end of file + ./releases/net481/ModVerify.exe + ./releases/ModVerify-Net10.zip \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ed2af48..6e60fde 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,12 +19,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 submodules: recursive - uses: actions/setup-dotnet@v4 with: - dotnet-version: 9.0.x + dotnet-version: 10.0.x - name: Build & Test in Release Mode run: dotnet test --configuration Release \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..87124a4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "modules/ModdingToolBase"] + path = modules/ModdingToolBase + url = https://github.com/AnakinRaW/ModdingToolBase diff --git a/Directory.Build.props b/Directory.Build.props index 49ca26f..d475e7f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -24,7 +24,7 @@ - latest + preview disable enable True @@ -39,7 +39,7 @@ all - 3.7.115 + 3.9.50 diff --git a/ModVerify.sln b/ModVerify.sln deleted file mode 100644 index d09a64e..0000000 --- a/ModVerify.sln +++ /dev/null @@ -1,63 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.11.34909.67 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PetroglyphTools", "PetroglyphTools", "{15F8B753-814A-406E-9147-EB048DADAC96}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModVerify", "src\ModVerify\ModVerify.csproj", "{22ED0E2C-FF3B-40EB-9CE2-DCDE65CDF31B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModVerify.CliApp", "src\ModVerify.CliApp\ModVerify.CliApp.csproj", "{84479931-A329-4113-9BE5-90B71E5486E6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PG.StarWarsGame.Files.ChunkFiles", "src\PetroglyphTools\PG.StarWarsGame.Files.ChunkFiles\PG.StarWarsGame.Files.ChunkFiles.csproj", "{92F2A0C8-61B6-424B-99D5-7898CDBA7CA6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PG.StarWarsGame.Files.ALO", "src\PetroglyphTools\PG.StarWarsGame.Files.ALO\PG.StarWarsGame.Files.ALO.csproj", "{DF76A383-C94E-4D03-A07C-22D61ED37059}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PG.StarWarsGame.Files.XML", "src\PetroglyphTools\PG.StarWarsGame.Files.XML\PG.StarWarsGame.Files.XML.csproj", "{418C68FA-531B-432E-8459-6433181C8AD3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PG.StarWarsGame.Engine", "src\PetroglyphTools\PG.StarWarsGame.Engine\PG.StarWarsGame.Engine.csproj", "{DFD62F61-3455-44BE-BB7C-E954FF48534B}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {22ED0E2C-FF3B-40EB-9CE2-DCDE65CDF31B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {22ED0E2C-FF3B-40EB-9CE2-DCDE65CDF31B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {22ED0E2C-FF3B-40EB-9CE2-DCDE65CDF31B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {22ED0E2C-FF3B-40EB-9CE2-DCDE65CDF31B}.Release|Any CPU.Build.0 = Release|Any CPU - {84479931-A329-4113-9BE5-90B71E5486E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {84479931-A329-4113-9BE5-90B71E5486E6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {84479931-A329-4113-9BE5-90B71E5486E6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {84479931-A329-4113-9BE5-90B71E5486E6}.Release|Any CPU.Build.0 = Release|Any CPU - {92F2A0C8-61B6-424B-99D5-7898CDBA7CA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {92F2A0C8-61B6-424B-99D5-7898CDBA7CA6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {92F2A0C8-61B6-424B-99D5-7898CDBA7CA6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {92F2A0C8-61B6-424B-99D5-7898CDBA7CA6}.Release|Any CPU.Build.0 = Release|Any CPU - {DF76A383-C94E-4D03-A07C-22D61ED37059}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DF76A383-C94E-4D03-A07C-22D61ED37059}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DF76A383-C94E-4D03-A07C-22D61ED37059}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DF76A383-C94E-4D03-A07C-22D61ED37059}.Release|Any CPU.Build.0 = Release|Any CPU - {418C68FA-531B-432E-8459-6433181C8AD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {418C68FA-531B-432E-8459-6433181C8AD3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {418C68FA-531B-432E-8459-6433181C8AD3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {418C68FA-531B-432E-8459-6433181C8AD3}.Release|Any CPU.Build.0 = Release|Any CPU - {DFD62F61-3455-44BE-BB7C-E954FF48534B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DFD62F61-3455-44BE-BB7C-E954FF48534B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DFD62F61-3455-44BE-BB7C-E954FF48534B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DFD62F61-3455-44BE-BB7C-E954FF48534B}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {92F2A0C8-61B6-424B-99D5-7898CDBA7CA6} = {15F8B753-814A-406E-9147-EB048DADAC96} - {DF76A383-C94E-4D03-A07C-22D61ED37059} = {15F8B753-814A-406E-9147-EB048DADAC96} - {418C68FA-531B-432E-8459-6433181C8AD3} = {15F8B753-814A-406E-9147-EB048DADAC96} - {DFD62F61-3455-44BE-BB7C-E954FF48534B} = {15F8B753-814A-406E-9147-EB048DADAC96} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {D74A22E2-91F1-4BC7-9630-3CF930B45408} - EndGlobalSection -EndGlobal diff --git a/ModVerify.slnx b/ModVerify.slnx new file mode 100644 index 0000000..3527ff4 --- /dev/null +++ b/ModVerify.slnx @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/ModdingToolBase b/modules/ModdingToolBase new file mode 160000 index 0000000..479a088 --- /dev/null +++ b/modules/ModdingToolBase @@ -0,0 +1 @@ +Subproject commit 479a088a2b26dd4a3e2342b2e34f5359b0252e88 diff --git a/src/ModVerify.CliApp/ConsoleUtilities.cs b/src/ModVerify.CliApp/ConsoleUtilities.cs deleted file mode 100644 index db3e741..0000000 --- a/src/ModVerify.CliApp/ConsoleUtilities.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; - -namespace AET.ModVerifyTool; - -internal static class ConsoleUtilities -{ - public delegate bool ConsoleQuestionValueFactory(string input, out T value); - - public static void WriteHorizontalLine(char lineChar = '─', int length = 20) - { - var line = new string(lineChar, length); - Console.WriteLine(line); - } - - public static void WriteHeader() - { - Console.WriteLine("***********************************"); - Console.WriteLine("***********************************"); - Console.WriteLine(Figgle.FiggleFonts.Standard.Render("Mod Verify")); - Console.WriteLine("***********************************"); - Console.WriteLine("***********************************"); - Console.WriteLine(" by AnakinRaW"); - Console.WriteLine(); - Console.WriteLine(); - } - - public static void WriteApplicationFailure() - { - Console.WriteLine(); - WriteHorizontalLine('*'); - Console.ForegroundColor = ConsoleColor.DarkRed; - Console.WriteLine(" ModVerify Failure! "); - Console.ResetColor(); - WriteHorizontalLine('*'); - Console.WriteLine(); - Console.WriteLine("The application encountered an unexpected error and will terminate now!"); - Console.WriteLine(); - } - - public static T UserQuestionOnSameLine(string question, ConsoleQuestionValueFactory inputCorrect) - { - while (true) - { - var promptLeft = 0; - var promptTop = Console.CursorTop; - - Console.SetCursorPosition(promptLeft, promptTop); - Console.Write(question); - Console.SetCursorPosition(promptLeft + question.Length, promptTop); - - var input = ReadLineInline(); - - if (!inputCorrect(input, out var result)) - { - Console.SetCursorPosition(0, promptTop); - Console.Write(new string(' ', Console.WindowWidth - 1)); - continue; - } - - Console.WriteLine(); - return result; - } - } - - private static string ReadLineInline() - { - var input = ""; - while (true) - { - var key = Console.ReadKey(intercept: true); - - if (key.Key == ConsoleKey.Enter) - break; - - if (key.Key == ConsoleKey.Backspace) - { - if (input.Length > 0) - { - input = input[..^1]; - Console.SetCursorPosition(Console.CursorLeft - 1, Console.CursorTop); - Console.Write(' '); - Console.SetCursorPosition(Console.CursorLeft - 1, Console.CursorTop); - } - } - else if (!char.IsControl(key.KeyChar)) - { - input += key.KeyChar; - Console.Write(key.KeyChar); - } - } - - return input; - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ExtensionMethods.cs b/src/ModVerify.CliApp/ExtensionMethods.cs deleted file mode 100644 index b7e20c8..0000000 --- a/src/ModVerify.CliApp/ExtensionMethods.cs +++ /dev/null @@ -1,17 +0,0 @@ -using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Infrastructure.Games; - -namespace AET.ModVerifyTool; - -internal static class ExtensionMethods -{ - public static GameEngineType ToEngineType(this GameType type) - { - return type == GameType.Foc ? GameEngineType.Foc : GameEngineType.Eaw; - } - - public static GameType FromEngineType(this GameEngineType type) - { - return type == GameEngineType.Foc ? GameType.Foc : GameType.Eaw; - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/GameFinder/GameFinderResult.cs b/src/ModVerify.CliApp/GameFinder/GameFinderResult.cs index 3135955..beb3964 100644 --- a/src/ModVerify.CliApp/GameFinder/GameFinderResult.cs +++ b/src/ModVerify.CliApp/GameFinder/GameFinderResult.cs @@ -1,5 +1,5 @@ using PG.StarWarsGame.Infrastructure.Games; -namespace AET.ModVerifyTool.GameFinder; +namespace AET.ModVerify.App.GameFinder; internal record GameFinderResult(IGame Game, IGame? FallbackGame); \ No newline at end of file diff --git a/src/ModVerify.CliApp/GameFinder/GameFinderService.cs b/src/ModVerify.CliApp/GameFinder/GameFinderService.cs index 78fe408..a88ebd9 100644 --- a/src/ModVerify.CliApp/GameFinder/GameFinderService.cs +++ b/src/ModVerify.CliApp/GameFinder/GameFinderService.cs @@ -10,7 +10,7 @@ using PG.StarWarsGame.Infrastructure.Services; using PG.StarWarsGame.Infrastructure.Services.Detection; -namespace AET.ModVerifyTool.GameFinder; +namespace AET.ModVerify.App.GameFinder; internal class GameFinderService { @@ -79,7 +79,7 @@ private bool TryDetectGame(GameType gameType, IList detectors, ou catch (Exception e) { result = GameDetectionResult.NotInstalled(gameType); - _logger?.LogTrace($"Unable to find game installation: {e.Message}"); + _logger?.LogTrace("Unable to find game installation: {Message}", e.Message); return false; } } @@ -97,7 +97,8 @@ private GameFinderResult FindGames(IList detectors) if (result.GameLocation is null) throw new GameNotFoundException("Unable to find game installation: Wrong install path?"); - _logger?.LogInformation($"Found game installation: {result.GameIdentity} at {result.GameLocation.FullName}"); + _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, + "Found game installation: {ResultGameIdentity} at {GameLocationFullName}", result.GameIdentity, result.GameLocation.FullName); var game = _gameFactory.CreateGame(result, CultureInfo.InvariantCulture); @@ -118,7 +119,8 @@ private GameFinderResult FindGames(IList detectors) if (!TryDetectGame(GameType.Eaw, fallbackDetectors, out var fallbackResult) || fallbackResult.GameLocation is null) throw new GameNotFoundException("Unable to find fallback game installation: Wrong install path?"); - _logger?.LogInformation($"Found fallback game installation: {fallbackResult.GameIdentity} at {fallbackResult.GameLocation.FullName}"); + _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, + "Found fallback game installation: {FallbackResultGameIdentity} at {GameLocationFullName}", fallbackResult.GameIdentity, fallbackResult.GameLocation.FullName); fallbackGame = _gameFactory.CreateGame(fallbackResult, CultureInfo.InvariantCulture); diff --git a/src/ModVerify.CliApp/GameNotFoundException.cs b/src/ModVerify.CliApp/GameFinder/GameNotFoundException.cs similarity index 76% rename from src/ModVerify.CliApp/GameNotFoundException.cs rename to src/ModVerify.CliApp/GameFinder/GameNotFoundException.cs index 21cdc81..37b0386 100644 --- a/src/ModVerify.CliApp/GameNotFoundException.cs +++ b/src/ModVerify.CliApp/GameFinder/GameNotFoundException.cs @@ -1,5 +1,5 @@ using PG.StarWarsGame.Infrastructure.Games; -namespace AET.ModVerifyTool; +namespace AET.ModVerify.App.GameFinder; internal class GameNotFoundException(string message) : GameException(message); \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectors/AutomaticModSelector.cs b/src/ModVerify.CliApp/ModSelectors/AutomaticModSelector.cs index 00fc4ae..717db7b 100644 --- a/src/ModVerify.CliApp/ModSelectors/AutomaticModSelector.cs +++ b/src/ModVerify.CliApp/ModSelectors/AutomaticModSelector.cs @@ -2,8 +2,9 @@ using System.Globalization; using System.IO.Abstractions; using System.Linq; -using AET.ModVerifyTool.GameFinder; -using AET.ModVerifyTool.Options; +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; @@ -13,7 +14,7 @@ using PG.StarWarsGame.Infrastructure.Services; using PG.StarWarsGame.Infrastructure.Services.Detection; -namespace AET.ModVerifyTool.ModSelectors; +namespace AET.ModVerify.App.ModSelectors; internal class AutomaticModSelector(IServiceProvider serviceProvider) : ModSelectorBase(serviceProvider) { @@ -37,7 +38,7 @@ internal class AutomaticModSelector(IServiceProvider serviceProvider) : ModSelec } catch (GameNotFoundException) { - Logger?.LogError($"Unable to find games based of the given location '{settings.GamePath}'. Consider specifying all paths manually."); + 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; } @@ -59,7 +60,7 @@ internal class AutomaticModSelector(IServiceProvider serviceProvider) : ModSelec 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."); + 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); diff --git a/src/ModVerify.CliApp/ModSelectors/ConsoleModSelector.cs b/src/ModVerify.CliApp/ModSelectors/ConsoleModSelector.cs index 7a29368..c776d6d 100644 --- a/src/ModVerify.CliApp/ModSelectors/ConsoleModSelector.cs +++ b/src/ModVerify.CliApp/ModSelectors/ConsoleModSelector.cs @@ -1,14 +1,16 @@ using System; using System.Collections.Generic; using AET.Modinfo.Spec; -using AET.ModVerifyTool.GameFinder; -using AET.ModVerifyTool.Options; +using AET.ModVerify.App.GameFinder; +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.ModVerifyTool.ModSelectors; +namespace AET.ModVerify.App.ModSelectors; internal class ConsoleModSelector(IServiceProvider serviceProvider) : ModSelectorBase(serviceProvider) { @@ -100,7 +102,7 @@ private static IPhysicalPlayableObject SelectPlayableObject(GameFinderResult fin if (!int.TryParse(input, out value)) return false; - return value <= list.Count; + return value <= list.Count && value >= 0; }); return list[selected]; } diff --git a/src/ModVerify.CliApp/ModSelectors/IModSelector.cs b/src/ModVerify.CliApp/ModSelectors/IModSelector.cs index f0b30a0..a04858c 100644 --- a/src/ModVerify.CliApp/ModSelectors/IModSelector.cs +++ b/src/ModVerify.CliApp/ModSelectors/IModSelector.cs @@ -1,8 +1,8 @@ -using AET.ModVerifyTool.Options; +using AET.ModVerify.App.Settings; using PG.StarWarsGame.Engine; using PG.StarWarsGame.Infrastructure; -namespace AET.ModVerifyTool.ModSelectors; +namespace AET.ModVerify.App.ModSelectors; internal interface IModSelector { diff --git a/src/ModVerify.CliApp/ModSelectors/ManualModSelector.cs b/src/ModVerify.CliApp/ModSelectors/ManualModSelector.cs index d5913c6..34cf39d 100644 --- a/src/ModVerify.CliApp/ModSelectors/ManualModSelector.cs +++ b/src/ModVerify.CliApp/ModSelectors/ManualModSelector.cs @@ -1,9 +1,9 @@ using System; -using AET.ModVerifyTool.Options; +using AET.ModVerify.App.Settings; using PG.StarWarsGame.Engine; using PG.StarWarsGame.Infrastructure; -namespace AET.ModVerifyTool.ModSelectors; +namespace AET.ModVerify.App.ModSelectors; internal class ManualModSelector(IServiceProvider serviceProvider) : ModSelectorBase(serviceProvider) { diff --git a/src/ModVerify.CliApp/ModSelectors/ModSelectorBase.cs b/src/ModVerify.CliApp/ModSelectors/ModSelectorBase.cs index 0eec285..8dd1d90 100644 --- a/src/ModVerify.CliApp/ModSelectors/ModSelectorBase.cs +++ b/src/ModVerify.CliApp/ModSelectors/ModSelectorBase.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; -using AET.ModVerifyTool.GameFinder; -using AET.ModVerifyTool.Options; +using AET.ModVerify.App.GameFinder; +using AET.ModVerify.App.Settings; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using PG.StarWarsGame.Engine; @@ -10,7 +10,7 @@ using PG.StarWarsGame.Infrastructure.Mods; using PG.StarWarsGame.Infrastructure.Services.Dependencies; -namespace AET.ModVerifyTool.ModSelectors; +namespace AET.ModVerify.App.ModSelectors; internal abstract class ModSelectorBase : IModSelector { diff --git a/src/ModVerify.CliApp/ModSelectors/ModSelectorFactory.cs b/src/ModVerify.CliApp/ModSelectors/ModSelectorFactory.cs index 8335a86..07ef263 100644 --- a/src/ModVerify.CliApp/ModSelectors/ModSelectorFactory.cs +++ b/src/ModVerify.CliApp/ModSelectors/ModSelectorFactory.cs @@ -1,7 +1,7 @@ using System; -using AET.ModVerifyTool.Options; +using AET.ModVerify.App.Settings; -namespace AET.ModVerifyTool.ModSelectors; +namespace AET.ModVerify.App.ModSelectors; internal class ModSelectorFactory(IServiceProvider serviceProvider) { diff --git a/src/ModVerify.CliApp/ModSelectors/SettingsBasedModSelector.cs b/src/ModVerify.CliApp/ModSelectors/SettingsBasedModSelector.cs index 12f7861..221bb9e 100644 --- a/src/ModVerify.CliApp/ModSelectors/SettingsBasedModSelector.cs +++ b/src/ModVerify.CliApp/ModSelectors/SettingsBasedModSelector.cs @@ -1,14 +1,15 @@ using System; using System.Linq; -using AET.ModVerifyTool.Options; +using AET.ModVerify.App.GameFinder; +using AET.ModVerify.App.Settings; using PG.StarWarsGame.Engine; using PG.StarWarsGame.Infrastructure; -namespace AET.ModVerifyTool.ModSelectors; +namespace AET.ModVerify.App.ModSelectors; internal class SettingsBasedModSelector(IServiceProvider serviceProvider) { - public VerifyInstallationInformation CreateInstallationDataFromSettings(GameInstallationsSettings settings) + public VerifyInstallationData CreateInstallationDataFromSettings(GameInstallationsSettings settings) { var gameLocations = new ModSelectorFactory(serviceProvider) .CreateSelector(settings) @@ -20,7 +21,7 @@ public VerifyInstallationInformation CreateInstallationDataFromSettings(GameInst if (engineType is null) throw new InvalidOperationException("Engine type not specified."); - return new VerifyInstallationInformation + return new VerifyInstallationData { EngineType = engineType.Value, GameLocations = gameLocations, diff --git a/src/ModVerify.CliApp/VerifyInstallationInformation.cs b/src/ModVerify.CliApp/ModSelectors/VerifyInstallationData.cs similarity index 90% rename from src/ModVerify.CliApp/VerifyInstallationInformation.cs rename to src/ModVerify.CliApp/ModSelectors/VerifyInstallationData.cs index 66816ab..1a1fcd2 100644 --- a/src/ModVerify.CliApp/VerifyInstallationInformation.cs +++ b/src/ModVerify.CliApp/ModSelectors/VerifyInstallationData.cs @@ -1,9 +1,9 @@ using System.Text; using PG.StarWarsGame.Engine; -namespace AET.ModVerifyTool; +namespace AET.ModVerify.App.ModSelectors; -internal sealed class VerifyInstallationInformation +internal sealed class VerifyInstallationData { public required string Name { get; init; } diff --git a/src/ModVerify.CliApp/ModVerify.CliApp.csproj b/src/ModVerify.CliApp/ModVerify.CliApp.csproj index 3ca41b6..0073b2b 100644 --- a/src/ModVerify.CliApp/ModVerify.CliApp.csproj +++ b/src/ModVerify.CliApp/ModVerify.CliApp.csproj @@ -1,9 +1,9 @@  - net9.0;net48 + net10.0;net481 Exe - AET.ModVerifyTool + AET.ModVerify.App ModVerify $(RepoRootPath)aet.ico AlamoEngineTools.ModVerify.CliApp @@ -22,42 +22,46 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - @@ -65,11 +69,16 @@ + + + + + - - + + diff --git a/src/ModVerify.CliApp/ModVerifyApp.cs b/src/ModVerify.CliApp/ModVerifyApp.cs deleted file mode 100644 index 5cefc40..0000000 --- a/src/ModVerify.CliApp/ModVerifyApp.cs +++ /dev/null @@ -1,155 +0,0 @@ -using AET.ModVerify; -using AET.ModVerify.Reporting; -using AET.ModVerifyTool.ModSelectors; -using AET.ModVerifyTool.Options; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -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.Pipeline; -using AET.ModVerifyTool.Reporting; -using PG.StarWarsGame.Engine; - -namespace AET.ModVerifyTool; - -internal class ModVerifyApp(ModVerifyAppSettings settings, IServiceProvider services) -{ - private readonly ILogger? _logger = services.GetService()?.CreateLogger(typeof(ModVerifyApp)); - private readonly IFileSystem _fileSystem = services.GetRequiredService(); - - public async Task RunApplication() - { - var installData = new SettingsBasedModSelector(services) - .CreateInstallationDataFromSettings(settings.GameInstallationsSettings); - - _logger?.LogDebug($"Verify install data: {installData}"); - _logger?.LogTrace($"Verify settings: {settings}"); - - var allErrors = await Verify(installData).ConfigureAwait(false); - - try - { - await ReportErrors(allErrors).ConfigureAwait(false); - } - catch (GameVerificationException e) - { - return e.HResult; - } - - if (!settings.CreateNewBaseline) - return 0; - - await WriteBaseline(allErrors, settings.NewBaselinePath).ConfigureAwait(false); - _logger?.LogInformation("Baseline successfully created."); - - return 0; - } - - private async Task> Verify(VerifyInstallationInformation installInformation) - { - var gameEngineService = services.GetRequiredService(); - var engineErrorReporter = new ConcurrentGameEngineErrorReporter(); - - IStarWarsGameEngine gameEngine; - - try - { - var initProgress = new Progress(); - var initProgressReporter = new EngineInitializeProgressReporter(initProgress); - - try - { - _logger?.LogInformation($"Creating Game Engine '{installInformation.EngineType}'"); - gameEngine = await gameEngineService.InitializeAsync( - installInformation.EngineType, - installInformation.GameLocations, - engineErrorReporter, - initProgress, - false, - CancellationToken.None).ConfigureAwait(false); - _logger?.LogInformation("Game Engine created"); - } - finally - { - initProgressReporter.Dispose(); - } - } - catch (Exception e) - { - _logger?.LogError(e, $"Creating game engine failed: {e.Message}"); - throw; - } - - var progressReporter = new VerifyConsoleProgressReporter(installInformation.Name); - - using var verifyPipeline = new GameVerifyPipeline( - gameEngine, - engineErrorReporter, - settings.VerifyPipelineSettings, - settings.GlobalReportSettings, - progressReporter, - services); - - try - { - try - { - _logger?.LogInformation($"Verifying '{installInformation.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("Verification stopped due to enabled failFast setting."); - } - catch (Exception e) - { - _logger?.LogError(e, $"Verification failed: {e.Message}"); - throw; - } - - _logger?.LogInformation("Finished verification"); - return verifyPipeline.FilteredErrors; - } - - private async Task ReportErrors(IReadOnlyCollection errors) - { - _logger?.LogInformation("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(IEnumerable errors, string baselineFile) - { - var baseline = new VerificationBaseline(settings.GlobalReportSettings.MinimumReportSeverity, errors); - - var fullPath = _fileSystem.Path.GetFullPath(baselineFile); - _logger?.LogInformation($"Writing Baseline to '{fullPath}'"); - -#if NET - await -#endif - using var fs = _fileSystem.FileStream.New(fullPath, FileMode.Create, FileAccess.Write, FileShare.None); - await baseline.ToJsonAsync(fs); - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModVerifyAppEnvironment.cs b/src/ModVerify.CliApp/ModVerifyAppEnvironment.cs new file mode 100644 index 0000000..86bfc40 --- /dev/null +++ b/src/ModVerify.CliApp/ModVerifyAppEnvironment.cs @@ -0,0 +1,71 @@ +using System.IO.Abstractions; +using System.Reflection; +using AnakinRaW.ApplicationBase.Environment; +#if !NET +using System; +using System.Collections.Generic; +using AnakinRaW.AppUpdaterFramework.Configuration; +using AnakinRaW.CommonUtilities.DownloadManager.Configuration; +#endif + +namespace AET.ModVerify.App; + +internal sealed class ModVerifyAppEnvironment(Assembly assembly, IFileSystem fileSystem) +#if NET + : ApplicationEnvironment(assembly, fileSystem) +#else + : UpdatableApplicationEnvironment(assembly, fileSystem) +#endif +{ + public override string ApplicationName => ModVerifyConstants.AppNameString; + + protected override string ApplicationLocalDirectoryName => ModVerifyConstants.ModVerifyToolPath; + +#if NETFRAMEWORK + + public override ICollection UpdateMirrors { get; } = new List + { +#if DEBUG + new("C:\\Test\\ModVerify"), +#endif + new($"https://republicatwar.com/downloads/{ModVerifyConstants.ModVerifyToolPath}") + }; + + public override string UpdateRegistryPath => $@"SOFTWARE\{ModVerifyConstants.ModVerifyToolPath}\Update"; + + protected override UpdateConfiguration CreateUpdateConfiguration() + { + return new UpdateConfiguration + { + DownloadLocation = FileSystem.Path.Combine(ApplicationLocalPath, "downloads"), + BackupLocation = FileSystem.Path.Combine(ApplicationLocalPath, "backups"), + BackupPolicy = BackupPolicy.Required, + ComponentDownloadConfiguration = new DownloadManagerConfiguration + { + AllowEmptyFileDownload = false, + DownloadRetryDelay = 500, + ValidationPolicy = ValidationPolicy.Required + }, + ManifestDownloadConfiguration = new DownloadManagerConfiguration + { + AllowEmptyFileDownload = false, + DownloadRetryDelay = 500, + ValidationPolicy = ValidationPolicy.Optional + }, + BranchDownloadConfiguration = new DownloadManagerConfiguration + { + AllowEmptyFileDownload = false, + DownloadRetryDelay = 500, + ValidationPolicy = ValidationPolicy.NoValidation + }, + DownloadRetryCount = 3, + RestartConfiguration = new UpdateRestartConfiguration + { + SupportsRestart = true, + PassCurrentArgumentsForRestart = true + }, + ValidateInstallation = true + }; + } +#endif +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModVerifyApplication.cs b/src/ModVerify.CliApp/ModVerifyApplication.cs new file mode 100644 index 0000000..7ea461c --- /dev/null +++ b/src/ModVerify.CliApp/ModVerifyApplication.cs @@ -0,0 +1,251 @@ +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 new file mode 100644 index 0000000..6b60f06 --- /dev/null +++ b/src/ModVerify.CliApp/ModVerifyConstants.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.Logging; + +namespace AET.ModVerify.App; + +internal static class ModVerifyConstants +{ + public const string AppNameString = "AET Mod Verify"; + public const string ModVerifyToolId = "AET.ModVerify"; + public const string ModVerifyToolPath = "ModVerify"; + public const int ConsoleEventIdValue = 1138; + + public static readonly EventId ConsoleEventId = new(ConsoleEventIdValue, "LogToConsole"); +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Options/CommandLine/VerifyVerbOption.cs b/src/ModVerify.CliApp/Options/CommandLine/VerifyVerbOption.cs deleted file mode 100644 index 517ceda..0000000 --- a/src/ModVerify.CliApp/Options/CommandLine/VerifyVerbOption.cs +++ /dev/null @@ -1,26 +0,0 @@ -using AET.ModVerify.Reporting; -using CommandLine; - -namespace AET.ModVerifyTool.Options.CommandLine; - -[Verb("verify", true, HelpText = "Verifies the specified game and reports the findings.")] -internal class VerifyVerbOption : BaseModVerifyOptions -{ - [Option('o', "outDir", Required = false, HelpText = "Directory where result files shall be stored to.")] - public string? OutputDirectory { get; set; } - - [Option("failFast", Required = false, Default = false, - HelpText = "When set, the application will abort on the first failure. The option also recognized the 'MinimumFailureSeverity' setting.")] - public bool FailFast { get; set; } - - [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.")] - 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; set; } - - [Option("baseline", Required = false, HelpText = "Path to a JSON baseline file.")] - public string? Baseline { get; set; } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Program.cs b/src/ModVerify.CliApp/Program.cs index 3ac92ba..cd4747b 100644 --- a/src/ModVerify.CliApp/Program.cs +++ b/src/ModVerify.CliApp/Program.cs @@ -1,18 +1,21 @@ -using AET.ModVerify; +using AET.ModVerify.App.Settings; +using AET.ModVerify.App.Settings.CommandLine; +using AET.ModVerify.App.Updates; +using AET.ModVerify.App.Utilities; using AET.ModVerify.Reporting; using AET.ModVerify.Reporting.Reporters; using AET.ModVerify.Reporting.Reporters.JSON; using AET.ModVerify.Reporting.Reporters.Text; using AET.ModVerify.Reporting.Settings; -using AET.ModVerifyTool.Options; -using AET.ModVerifyTool.Options.CommandLine; -using AET.ModVerifyTool.Updates; using AET.SteamAbstraction; +using AnakinRaW.ApplicationBase; +using AnakinRaW.ApplicationBase.Environment; +using AnakinRaW.ApplicationBase.Update; +using AnakinRaW.ApplicationBase.Update.Options; +using AnakinRaW.AppUpdaterFramework.Json; using AnakinRaW.CommonUtilities.Hashing; using AnakinRaW.CommonUtilities.Registry; using AnakinRaW.CommonUtilities.Registry.Windows; -using CommandLine; -using CommandLine.Text; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using PG.Commons; @@ -28,194 +31,189 @@ using PG.StarWarsGame.Infrastructure.Services.Name; using Serilog; using Serilog.Events; +using Serilog.Expressions; using Serilog.Filters; using Serilog.Sinks.SystemConsole.Themes; using System; +using System.Collections.Generic; +using System.Diagnostics; using System.IO.Abstractions; +using System.Runtime.InteropServices; using System.Threading.Tasks; using Testably.Abstractions; using ILogger = Serilog.ILogger; -namespace AET.ModVerifyTool; +namespace AET.ModVerify.App; -internal class Program +internal class MainClass +{ + // Fody/Costura application with .NET Core apparently don't work well when the class containing the Main method are derived by a type in an embedded assembly. + private static Task Main(string[] args) + { + return new Program().StartAsync(args); + } +} + +internal class Program : SelfUpdateableAppLifecycle { private static readonly string EngineParserNamespace = typeof(XmlObjectParser<>).Namespace!; private static readonly string ParserNamespace = typeof(PetroglyphXmlFileParser<>).Namespace!; private static readonly string ModVerifyRootNameSpace = typeof(Program).Namespace!; + private static readonly CompiledExpression PrintToConsoleExpression = SerilogExpression.Compile($"EventId.Id = {ModVerifyConstants.ConsoleEventIdValue}"); - private static async Task Main(string[] args) - { - ConsoleUtilities.WriteHeader(); + private static ModVerifyOptionsContainer _optionsContainer = null!; - var result = 0; - - Type[] programVerbs = - [ - typeof(VerifyVerbOption), - typeof(CreateBaselineVerbOption), - ]; - - var parseResult = Parser.Default.ParseArguments(args, programVerbs); - - await parseResult.WithParsedAsync(async o => - { - result = await Run((BaseModVerifyOptions)o); - }); - await parseResult.WithNotParsedAsync(e => - { - Console.WriteLine(HelpText.AutoBuild(parseResult).ToString()); - result = 0xA0; - return Task.CompletedTask; - }); - - return result; - } - - private static async Task Run(BaseModVerifyOptions options) + protected override async Task InitializeAppAsync(IReadOnlyList args) { - var coreServiceCollection = CreateCoreServices(options.Verbose); - var coreServices = coreServiceCollection.BuildServiceProvider(); - var logger = coreServices.GetService()?.CreateLogger(typeof(Program)); + ModVerifyConsoleUtilities.WriteHeader(ApplicationEnvironment.AssemblyInfo.InformationalVersion); - logger?.LogDebug($"Raw command line: {Environment.CommandLine}"); + await base.InitializeAppAsync(args); - var interactive = false; try { - var settings = new SettingsBuilder(coreServices).BuildSettings(options); - interactive = settings.Interactive; - var services = CreateAppServices(coreServiceCollection, settings); - - if (!settings.Offline) - await CheckForUpdate(services, logger); - - var verifier = new ModVerifyApp(settings, services); - return await verifier.RunApplication().ConfigureAwait(false); + var settings = new ModVerifyOptionsParser(ApplicationEnvironment, BootstrapLoggerFactory).Parse(args); + if (!settings.HasOptions) + return 0xA0; + _optionsContainer = settings; + return 0; } catch (Exception e) { - ConsoleUtilities.WriteApplicationFailure(); - logger?.LogCritical(e, e.Message); + Logger?.LogCritical(e, "Failed to parse commandline arguments: {Message}", e.Message); + ConsoleUtilities.WriteApplicationFatalError(ModVerifyConstants.AppNameString, e); return e.HResult; } - finally + } + + protected override void CreateAppServices(IServiceCollection services, IReadOnlyList args) + { + base.CreateAppServices(services, args); + + services.AddSingleton((ApplicationEnvironment as ModVerifyAppEnvironment)!); + + services.AddLogging(ConfigureLogging); + + services.AddSingleton(sp => new HashingService(sp)); + + + if (IsUpdateableApplication) { #if NET - await Log.CloseAndFlushAsync(); -#else - Log.CloseAndFlush(); + throw new NotSupportedException(); #endif - if (interactive) - { - Console.WriteLine(); - ConsoleUtilities.WriteHorizontalLine('-'); - Console.WriteLine("Press any key to exit"); - Console.ReadLine(); - } + services.MakeAppUpdateable( + UpdatableApplicationEnvironment, + sp => new CosturaApplicationProductService(ApplicationEnvironment, sp), + sp => new JsonManifestLoader(sp)); } - } - private static async Task CheckForUpdate(IServiceProvider services, Microsoft.Extensions.Logging.ILogger? logger) - { - var updateChecker = new ModVerifyUpdaterChecker(services); + if (_optionsContainer.ModVerifyOptions is null) + return; - logger?.LogDebug("Checking for available update"); + SteamAbstractionLayer.InitializeServices(services); + PetroglyphGameInfrastructure.InitializeServices(services); - try - { - var updateInfo = await updateChecker.CheckForUpdateAsync().ConfigureAwait(false); - if (updateInfo.IsUpdateAvailable) - { - ConsoleUtilities.WriteHorizontalLine(); - - Console.ForegroundColor = ConsoleColor.DarkGreen; - Console.WriteLine("New Update Available!"); - Console.ResetColor(); - - Console.WriteLine($"Version: {updateInfo.NewVersion}, Download here: {updateInfo.DownloadLink}"); - ConsoleUtilities.WriteHorizontalLine(); - Console.WriteLine(); + services.SupportMTD(); + services.SupportMEG(); + services.SupportALO(); + services.SupportXML(); + PetroglyphCommons.ContributeServices(services); - } + PetroglyphEngineServiceContribution.ContributeServices(services); + services.RegisterVerifierCache(); + + + SetupVerifyReporting(services); + + if (_optionsContainer.ModVerifyOptions.OfflineMode) + { + services.AddSingleton(sp => new OfflineModNameResolver(sp)); + services.AddSingleton(sp => new OfflineModGameTypeResolver(sp)); } - catch(Exception e) + else { - logger?.LogWarning($"Unable to check for updates due to an internal error: {e.Message}"); - logger?.LogTrace(e, "Checking for update failed: " + e.Message); + services.AddSingleton(sp => new OnlineModNameResolver(sp)); + services.AddSingleton(sp => new OnlineModGameTypeResolver(sp)); } } - private static IServiceCollection CreateCoreServices(bool verboseLogging) + protected override ApplicationEnvironment CreateAppEnvironment() { - var fileSystem = new RealFileSystem(); - var serviceCollection = new ServiceCollection(); - - serviceCollection.AddSingleton(new WindowsRegistry()); - serviceCollection.AddSingleton(fileSystem); - - serviceCollection.AddLogging(builder => ConfigureLogging(builder, fileSystem, verboseLogging)); - - return serviceCollection; + return new ModVerifyAppEnvironment(typeof(Program).Assembly, FileSystem); } - private static IServiceProvider CreateAppServices(IServiceCollection serviceCollection, ModVerifyAppSettings settings) + protected override IFileSystem CreateFileSystem() { - serviceCollection.AddSingleton(sp => new HashingService(sp)); - - SteamAbstractionLayer.InitializeServices(serviceCollection); - PetroglyphGameInfrastructure.InitializeServices(serviceCollection); + return new RealFileSystem(); + } - serviceCollection.SupportMTD(); - serviceCollection.SupportMEG(); - serviceCollection.SupportALO(); - serviceCollection.SupportXML(); - PetroglyphCommons.ContributeServices(serviceCollection); + protected override IRegistry CreateRegistry() + { + return !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? new InMemoryRegistry(InMemoryRegistryCreationFlags.WindowsLike) + : new WindowsRegistry(); + } - PetroglyphEngineServiceContribution.ContributeServices(serviceCollection); - serviceCollection.RegisterVerifierCache(); + protected override async Task RunAppAsync(string[] args, IServiceProvider appServiceProvider) + { + var result = await HandleUpdate(appServiceProvider); + if (result != 0 || _optionsContainer.ModVerifyOptions is null) + return result; - SetupVerifyReporting(serviceCollection, settings); + ModVerifyAppSettings modVerifySettings; - if (settings.Offline) + try { - serviceCollection.AddSingleton(sp => new OfflineModNameResolver(sp)); - serviceCollection.AddSingleton(sp => new OfflineModGameTypeResolver(sp)); + modVerifySettings = new SettingsBuilder(appServiceProvider).BuildSettings(_optionsContainer.ModVerifyOptions); } - else + catch (Exception e) { - serviceCollection.AddSingleton(sp => new OnlineModNameResolver(sp)); - serviceCollection.AddSingleton(sp => new OnlineModGameTypeResolver(sp)); + Logger?.LogCritical(e, "Failed to create settings form commandline arguments: {EMessage}", e.Message); + ConsoleUtilities.WriteApplicationFatalError(ModVerifyConstants.AppNameString, e); + return e.HResult; } - - return serviceCollection.BuildServiceProvider(); + + return await new ModVerifyApplication(modVerifySettings, appServiceProvider).Run().ConfigureAwait(false); } - private static void SetupVerifyReporting(IServiceCollection serviceCollection, ModVerifyAppSettings settings) + private void SetupVerifyReporting(IServiceCollection serviceCollection) { - var printOnlySummary = settings.CreateNewBaseline; + var options = _optionsContainer.ModVerifyOptions; + Debug.Assert(options is not null); + + + var verifyVerb = options as VerifyVerbOption; + + // Console should be in minimal summary mode if we are not in verify mode. + var printOnlySummary = verifyVerb is null; + serviceCollection.RegisterConsoleReporter(new VerifyReportSettings { MinimumReportSeverity = VerificationSeverity.Error }, printOnlySummary); - if (string.IsNullOrEmpty(settings.ReportOutput)) + if (verifyVerb == null) return; + var outputDirectory = Environment.CurrentDirectory; + + if (!string.IsNullOrEmpty(verifyVerb.OutputDirectory)) + outputDirectory = FileSystem.Path.GetFullPath(FileSystem.Path.Combine(Environment.CurrentDirectory, verifyVerb.OutputDirectory!)); + serviceCollection.RegisterJsonReporter(new JsonReporterSettings { - OutputDirectory = settings.ReportOutput!, - MinimumReportSeverity = settings.GlobalReportSettings.MinimumReportSeverity + OutputDirectory = outputDirectory!, + MinimumReportSeverity = options.MinimumSeverity }); serviceCollection.RegisterTextFileReporter(new TextFileReporterSettings { - OutputDirectory = settings.ReportOutput!, - MinimumReportSeverity = settings.GlobalReportSettings.MinimumReportSeverity + OutputDirectory = outputDirectory!, + MinimumReportSeverity = options.MinimumSeverity }); } - private static void ConfigureLogging(ILoggingBuilder loggingBuilder, IFileSystem fileSystem, bool verbose) + private void ConfigureLogging(ILoggingBuilder loggingBuilder) { loggingBuilder.ClearProviders(); @@ -226,53 +224,116 @@ private static void ConfigureLogging(ILoggingBuilder loggingBuilder, IFileSystem loggingBuilder.AddDebug(); #endif - if (verbose) + if (_optionsContainer.ModVerifyOptions?.Verbose == true || _optionsContainer.UpdateOptions?.Verbose == true) { logLevel = LogEventLevel.Verbose; loggingBuilder.AddDebug(); } - var fileLogger = SetupFileLogging(fileSystem, logLevel); + var fileLogger = SetupFileLogging(); loggingBuilder.AddSerilog(fileLogger); - var cLogger = new LoggerConfiguration() - .WriteTo.Console( - logLevel, - theme: AnsiConsoleTheme.Code, - outputTemplate: "[{Level:u3}] {Message:lj}{NewLine}{Exception}") - .Filter.ByIncludingOnly(x => - { - if (!x.Properties.TryGetValue("SourceContext", out var value)) - return true; - - var source = value.ToString().AsSpan().Trim('\"'); - - return source.StartsWith(ModVerifyRootNameSpace.AsSpan()); - }) - .CreateLogger(); - loggingBuilder.AddSerilog(cLogger); + var consoleLogger = SetupConsoleLogging(); + loggingBuilder.AddSerilog(consoleLogger); + + return; + + ILogger SetupConsoleLogging() + { + return new LoggerConfiguration() + .WriteTo.Console( + logLevel, + theme: AnsiConsoleTheme.Code, + outputTemplate: "[{Level:u3}] {Message:lj}{NewLine}{Exception}") + .MinimumLevel.Is(logLevel) + .Filter.ByIncludingOnly(x => + { + // Fatal errors are handled by a global exception handler + if (x.Level == LogEventLevel.Fatal) + return false; + + // Verbose should print everything we get + if (logLevel == LogEventLevel.Verbose) + return true; + + // Debug should print everything that has something to do with ModVerify + if (logLevel == LogEventLevel.Debug) + { + if (!x.Properties.TryGetValue("SourceContext", out var value)) + return false; + var source = value.ToString().AsSpan().Trim('\"'); + return source.StartsWith(ModVerifyRootNameSpace.AsSpan()); + } + + // In normal operation, we only print logs, which have the print-to-console EventId set. + return ExpressionResult.IsTrue(PrintToConsoleExpression(x)); + }) + .CreateLogger(); + } + + ILogger SetupFileLogging() + { + var logPath = FileSystem.Path.Combine(ApplicationEnvironment.ApplicationLocalPath, "ModVerify_log.txt"); + + return new LoggerConfiguration() + .Enrich.FromLogContext() + .MinimumLevel.Is(logLevel) + .Filter.ByExcluding(IsXmlParserLogging) + .WriteTo.Async(c => + { + c.RollingFile( + logPath, + outputTemplate: + "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{SourceContext}] {Message}{NewLine}{Exception}"); + }) + .CreateLogger(); + } + + static bool IsXmlParserLogging(LogEvent logEvent) + { + return Matching.FromSource(ParserNamespace)(logEvent) || Matching.FromSource(EngineParserNamespace)(logEvent); + } } - private static ILogger SetupFileLogging(IFileSystem fileSystem, LogEventLevel minLevel) + private async Task HandleUpdate(IServiceProvider serviceProvider) { - var logPath = fileSystem.Path.Combine(fileSystem.Path.GetTempPath(), "ModVerify_log.txt"); + var updateOptions = _optionsContainer.UpdateOptions ?? new ApplicationUpdateOptions(); + ModVerifyUpdateMode updateMode; + + if (_optionsContainer.ModVerifyOptions is not null) + { + if (_optionsContainer.ModVerifyOptions.OfflineMode) + { + Logger?.LogTrace("Running in offline mode. There will be nothing to update."); + return 0; + } - return new LoggerConfiguration() - .Enrich.FromLogContext() - .MinimumLevel.Is(minLevel) - .Filter.ByExcluding(IsXmlParserLogging) - .WriteTo.Async(c => + updateMode = _optionsContainer.ModVerifyOptions.LaunchedWithoutArguments() + ? ModVerifyUpdateMode.InteractiveUpdate + : ModVerifyUpdateMode.CheckOnly; + } + 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); + Logger?.LogDebug("Update procedure completed successfully."); + return 0; + } + catch (Exception e) + { + Logger?.LogCritical(e, e.Message); + var action = updateMode switch { - c.RollingFile( - logPath, - outputTemplate: - "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{SourceContext}] {Message}{NewLine}{Exception}"); - }) - .CreateLogger(); - } + ModVerifyUpdateMode.CheckOnly => "checking for updates", + _ => "updating" + }; + ConsoleUtilities.WriteApplicationFatalError(ModVerifyConstants.AppNameString, $"Error while {action}: {e.Message}", e.StackTrace); + return e.HResult; + } - private static bool IsXmlParserLogging(LogEvent logEvent) - { - return Matching.FromSource(ParserNamespace)(logEvent) || Matching.FromSource(EngineParserNamespace)(logEvent); } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Properties/AssemblyAttributes.cs b/src/ModVerify.CliApp/Properties/AssemblyAttributes.cs new file mode 100644 index 0000000..af943d8 --- /dev/null +++ b/src/ModVerify.CliApp/Properties/AssemblyAttributes.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly:InternalsVisibleTo("ModVerify.CliApp.Test")] \ No newline at end of file diff --git a/src/ModVerify.CliApp/Properties/launchSettings.json b/src/ModVerify.CliApp/Properties/launchSettings.json index 1e01ece..e47583a 100644 --- a/src/ModVerify.CliApp/Properties/launchSettings.json +++ b/src/ModVerify.CliApp/Properties/launchSettings.json @@ -1,8 +1,12 @@ { "profiles": { + "Run": { + "commandName": "Project", + "commandLineArgs": "" + }, "Interactive Verify": { "commandName": "Project", - "commandLineArgs": "verify -o verifyResults --minFailSeverity Information -v --baseline focBaseline.json --offline" + "commandLineArgs": "verify -o verifyResults --minFailSeverity Information --offline" }, "Interactive Baseline": { "commandName": "Project", diff --git a/src/ModVerify.CliApp/Reporting/BaselineFactory.cs b/src/ModVerify.CliApp/Reporting/BaselineFactory.cs new file mode 100644 index 0000000..9fcc911 --- /dev/null +++ b/src/ModVerify.CliApp/Reporting/BaselineFactory.cs @@ -0,0 +1,71 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Abstractions; +using AET.ModVerify.Reporting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace AET.ModVerify.App.Reporting; + +internal sealed class BaselineFactory(IServiceProvider serviceProvider) +{ + private readonly ILogger? _logger = serviceProvider.GetService()?.CreateLogger(typeof(BaselineFactory)); + private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); + + public bool TryCreateBaseline( + string directory, + out VerificationBaseline baseline, + [NotNullWhen(true)] out string? path) + { + baseline = VerificationBaseline.Empty; + path = null; + + if (!_fileSystem.Directory.Exists(directory)) + return false; + + _logger?.LogDebug(ModVerifyConstants.ConsoleEventId, "Searching for baseline file at '{Directory}'", directory); + + var jsonFiles = _fileSystem.Directory.EnumerateFiles( + directory, + "*.json" +#if NET || NETSTANDARD2_1_OR_GREATER + , new EnumerationOptions + { + MatchCasing = MatchCasing.CaseInsensitive, + RecurseSubdirectories = false + } +#endif + ); + + foreach (var jsonFile in jsonFiles) + { + try + { + baseline = CreateBaselineFromFilePath(jsonFile); + path = jsonFile; + _logger?.LogDebug("Create baseline from file: {JsonFile}", jsonFile); + return true; + } + catch (InvalidBaselineException e) + { + _logger?.LogDebug("'{JsonFile}' is not a valid baseline file: {Message}", jsonFile, e.Message); + // Ignore this exception + } + } + + path = null; + return false; + } + + public VerificationBaseline CreateBaseline(string filePath) + { + return CreateBaselineFromFilePath(filePath); + } + + private VerificationBaseline CreateBaselineFromFilePath(string baselineFile) + { + using var fs = _fileSystem.FileStream.New(baselineFile, FileMode.Open, FileAccess.Read); + return VerificationBaseline.FromJson(fs); + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Reporting/BaselineSelector.cs b/src/ModVerify.CliApp/Reporting/BaselineSelector.cs new file mode 100644 index 0000000..95953f1 --- /dev/null +++ b/src/ModVerify.CliApp/Reporting/BaselineSelector.cs @@ -0,0 +1,139 @@ +using AET.ModVerify.App.ModSelectors; +using AET.ModVerify.App.Resources.Baselines; +using AET.ModVerify.App.Settings; +using AET.ModVerify.Reporting; +using AnakinRaW.ApplicationBase; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine; +using System; +using System.Diagnostics; + +namespace AET.ModVerify.App.Reporting; + +internal sealed class BaselineSelector(ModVerifyAppSettings settings, IServiceProvider services) +{ + private readonly ILogger? _logger = services.GetService()?.CreateLogger(typeof(ModVerifyApplication)); + private readonly BaselineFactory _baselineFactory = new(services); + + public VerificationBaseline SelectBaseline(VerifyInstallationData installationData, out string? usedBaselinePath) + { + var baselinePath = settings.ReportSettings.BaselinePath; + if (!string.IsNullOrEmpty(baselinePath)) + { + try + { + usedBaselinePath = baselinePath; + return _baselineFactory.CreateBaseline(baselinePath!); + } + catch (InvalidBaselineException e) + { + using (ConsoleUtilities.HorizontalLineSeparatedBlock('*')) + { + Console.WriteLine($"The baseline '{baselinePath}' is not a valid baseline file: {e.Message}" + + $"{Environment.NewLine}Please generate a new baseline file or download the latest version." + + $"{Environment.NewLine}"); + } + + // For now, we bubble up this exception because we except users + // to correctly specify their baselines through command line arguments. + throw; + } + } + + if (!settings.ReportSettings.SearchBaselineLocally) + { + _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 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); + + } + + private VerificationBaseline FindBaselineInteractive(VerifyInstallationData installationData, 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. + // 2. Use a baseline found in the directory ModVerify executable. + // 3. If the verification target is a mod, ask the user to apply the default game's baseline. + // In any case ask the use if they want to use the located baseline file, or they wish to continue using none/empty. + + _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Searching for local baseline files..."); + + if (!_baselineFactory.TryCreateBaseline(installationData.GameLocations.TargetPath, out var baseline, + out baselinePath)) + { + if (!_baselineFactory.TryCreateBaseline("./", 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.WriteLine("No baseline found locally."); + return TryGetDefaultBaseline(installationData.EngineType, out baselinePath); + } + } + + Debug.Assert(baselinePath is not null); + + return ConsoleUtilities.UserYesNoQuestion($"ModVerify found the baseline file '{baselinePath}'. Do you want to use it?") + ? baseline + : VerificationBaseline.Empty; + } + + private VerificationBaseline TryGetDefaultBaseline(GameEngineType engineType, out string? baselinePath) + { + baselinePath = null; + if (engineType == GameEngineType.Eaw) + { + // TODO: EAW currently not implemented + return VerificationBaseline.Empty; + } + + if (!ConsoleUtilities.UserYesNoQuestion($"Do you want to load the default baseline for game engine '{engineType}'?")) + return VerificationBaseline.Empty; + + baselinePath = $"{engineType} (Default)"; + + try + { + return LoadEmbeddedBaseline(engineType); + } + catch (InvalidBaselineException) + { + throw new InvalidOperationException( + "Invalid baseline packed along ModVerify App. Please reach out to the creators. Thanks!"); + } + } + + internal VerificationBaseline LoadEmbeddedBaseline(GameEngineType engineType) + { + var baselineFileName = $"baseline-{engineType.ToString().ToLower()}.json"; + var resourcePath = $"{typeof(BaselineResources).Namespace}.{baselineFileName}"; + + using var baselineStream = typeof(BaselineSelector).Assembly.GetManifestResourceStream(resourcePath)!; + return VerificationBaseline.FromJson(baselineStream); + } + + private VerificationBaseline FindBaselineNonInteractive(string targetPath, out string? usedPath) + { + if (_baselineFactory.TryCreateBaseline(targetPath, 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); + usedPath = null; + return VerificationBaseline.Empty; + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Reporting/EngineInitializeProgressReporter.cs b/src/ModVerify.CliApp/Reporting/EngineInitializeProgressReporter.cs index 69413c0..b994e97 100644 --- a/src/ModVerify.CliApp/Reporting/EngineInitializeProgressReporter.cs +++ b/src/ModVerify.CliApp/Reporting/EngineInitializeProgressReporter.cs @@ -1,6 +1,6 @@ using System; -namespace AET.ModVerifyTool.Reporting; +namespace AET.ModVerify.App.Reporting; internal sealed class EngineInitializeProgressReporter : IDisposable { diff --git a/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs b/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs index 37d3ca2..4700457 100644 --- a/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs +++ b/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs @@ -1,11 +1,11 @@ -using AnakinRaW.CommonUtilities; -using AnakinRaW.CommonUtilities.SimplePipeline.Progress; -using ShellProgressBar; -using System; +using System; using System.Threading; using AET.ModVerify.Pipeline.Progress; +using AnakinRaW.CommonUtilities; +using AnakinRaW.CommonUtilities.SimplePipeline.Progress; +using ShellProgressBar; -namespace AET.ModVerifyTool.Reporting; +namespace AET.ModVerify.App.Reporting; public sealed class VerifyConsoleProgressReporter(string toVerifyName) : DisposableObject, IVerifyProgressReporter { diff --git a/src/ModVerify.CliApp/Resources/Baselines/BaselineResources.cs b/src/ModVerify.CliApp/Resources/Baselines/BaselineResources.cs new file mode 100644 index 0000000..bf49f9e --- /dev/null +++ b/src/ModVerify.CliApp/Resources/Baselines/BaselineResources.cs @@ -0,0 +1,4 @@ +namespace AET.ModVerify.App.Resources.Baselines; + +// Marker class to provide static namespace information for resource lookup. +internal static class BaselineResources; \ No newline at end of file diff --git a/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json b/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json new file mode 100644 index 0000000..c94d121 --- /dev/null +++ b/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json @@ -0,0 +1,2081 @@ +{ + "version": "2.0", + "minSeverity": "Information", + "errors": [ + { + "id": "XML04", + "verifiers": [ + "XMLError" + ], + "message": "Expected double but got value \u002737\u0060\u0027. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #11571\u0027", + "severity": "Warning", + "context": [ + "DATA\\XML\\COMMANDBARCOMPONENTS.XML", + "Size", + "parentName=\u0027bm_text_steal\u0027" + ], + "asset": "Size" + }, + { + "id": "XML04", + "verifiers": [ + "XMLError" + ], + "message": "Expected integer but got \u002780, 20\u0027. File=\u0027DATA\\XML\\SFXEVENTSWEAPONS.XML #90\u0027", + "severity": "Warning", + "context": [ + "DATA\\XML\\SFXEVENTSWEAPONS.XML", + "Probability", + "parentName=\u0027Unit_TIE_Fighter_Fire\u0027" + ], + "asset": "Probability" + }, + { + "id": "FILE00", + "verifiers": [ + "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.", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\UV_SKIPRAY.ALO" + ], + "asset": "Default.fx" + }, + { + "id": "FILE00", + "verifiers": [ + "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", + "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" + ], + "message": "Unable to find .ALO file \u0027W_Kamino_Reflect.ALO\u0027", + "severity": "Error", + "context": [], + "asset": "W_Kamino_Reflect.ALO" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "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\\UV_ECLIPSE_UC_DC.ALO" + ], + "asset": "p_ssd_debris" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_p_proton_torpedo.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_p_proton_torpedo.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_DStar_LeverPanel.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_DStar_LeverPanel.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "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", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\UV_ECLIPSE.ALO" + ], + "asset": "lookat" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" + ], + "message": "Could not find texture \u0027Cin_DeathStar.tga\u0027 for context: [ALTTEST.ALO].", + "severity": "Error", + "context": [ + "ALTTEST.ALO" + ], + "asset": "Cin_DeathStar.tga" + }, + { + "id": "FILE00", + "verifiers": [ + "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", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\NB_PRISON.ALO" + ], + "asset": "p_prison_light" + }, + { + "id": "FILE00", + "verifiers": [ + "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.", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\EV_MDU_SENSORNODE.ALO" + ], + "asset": "Default.fx" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" + ], + "message": "Could not find texture \u0027w_grenade.tga\u0027 for context: [W_GRENADE.ALO].", + "severity": "Error", + "context": [ + "W_GRENADE.ALO" + ], + "asset": "w_grenade.tga" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_Rbel_NavyRow.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Rbel_NavyRow.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_Planet_Alderaan_High.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_Planet_Alderaan_High.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "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", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\UV_ECLIPSE_UC.ALO" + ], + "asset": "lookat" + }, + { + "id": "FILE00", + "verifiers": [ + "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", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\EI_MARAJADE.ALO" + ], + "asset": "p_desert_ground_dust" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027p_splash_wake_lava.alo\u0027", + "severity": "Error", + "context": [], + "asset": "p_splash_wake_lava.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_rv_XWingProp.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_rv_XWingProp.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "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" + ], + "message": "Unable to find .ALO file \u0027CIN_Probe_Droid.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Probe_Droid.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_05_STATION_D.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\UB_05_STATION_D.ALO" + ], + "asset": "p_uwstation_death" + }, + { + "id": "FILE00", + "verifiers": [ + "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", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\RI_KYLEKATARN.ALO" + ], + "asset": "p_desert_ground_dust" + }, + { + "id": "FILE00", + "verifiers": [ + "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", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\RB_HEAVYVEHICLEFACTORY.ALO" + ], + "asset": "p_steam_small" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" + ], + "message": "Could not find texture \u0027p_particle_master\u0027 for context: [P_DIRT_EMITTER_TEST1.ALO].", + "severity": "Error", + "context": [ + "P_DIRT_EMITTER_TEST1.ALO" + ], + "asset": "p_particle_master" + }, + { + "id": "FILE00", + "verifiers": [ + "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", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\NB_PRISON.ALO" + ], + "asset": "p_smoke_small_thin4" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_EI_Vader.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_EI_Vader.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "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", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\RB_HYPERVELOCITYGUN.ALO" + ], + "asset": "p_smoke_small_thin2" + }, + { + "id": "FILE00", + "verifiers": [ + "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.", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\EV_TIE_LANCET.ALO" + ], + "asset": "Default.fx" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_DeathStar_Wall.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_DeathStar_Wall.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027W_droid_steam.alo\u0027", + "severity": "Error", + "context": [], + "asset": "W_droid_steam.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_DeathStar_High.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_DeathStar_High.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027MODELS\u0027", + "severity": "Error", + "context": [], + "asset": "MODELS" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027W_AllShaders.ALO\u0027", + "severity": "Error", + "context": [], + "asset": "W_AllShaders.ALO" + }, + { + "id": "FILE00", + "verifiers": [ + "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", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\W_THERMAL_DETONATOR_EMPIRE.ALO" + ], + "asset": "p_bomb_spin" + }, + { + "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_03_STATION_D.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\UB_03_STATION_D.ALO" + ], + "asset": "p_uwstation_death" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_Rbel_GreyGroup.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Rbel_GreyGroup.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "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", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\NB_SCH.ALO" + ], + "asset": "p_cold_tiny01" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CINE_EV_StarDestroyer.ALO\u0027", + "severity": "Error", + "context": [], + "asset": "CINE_EV_StarDestroyer.ALO" + }, + { + "id": "FILE00", + "verifiers": [ + "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", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\EV_ARCHAMMER.ALO" + ], + "asset": "p_hp_archammer-damage" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_Rbel_grey.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Rbel_grey.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_Reb_CelebHall.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Reb_CelebHall.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027P_mptl-2a_Die\u0027 not found for model \u0027DATA\\ART\\MODELS\\RV_MPTL-2A.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\RV_MPTL-2A.ALO" + ], + "asset": "P_mptl-2a_Die" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_ImperialCraft.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_ImperialCraft.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_DStar_Dish_close.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_DStar_Dish_close.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "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", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\RV_BWING.ALO" + ], + "asset": "pe_bwing_yellow" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_bridge.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_bridge.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "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", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\UI_SABOTEUR.ALO" + ], + "asset": "p_desert_ground_dust" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_Trooper_Row.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Trooper_Row.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_EV_TieAdvanced.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_EV_TieAdvanced.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027w_sith_arch.alo\u0027", + "severity": "Error", + "context": [], + "asset": "w_sith_arch.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" + ], + "message": "Could not find texture \u0027NB_YsalamiriTree_B.tga\u0027 for context: [UV_MDU_CAGE.ALO].", + "severity": "Error", + "context": [ + "UV_MDU_CAGE.ALO" + ], + "asset": "NB_YsalamiriTree_B.tga" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" + ], + "message": "Could not find texture \u0027W_TE_Rock_f_02_b.tga\u0027 for context: [EV_TIE_PHANTOM.ALO].", + "severity": "Error", + "context": [ + "EV_TIE_PHANTOM.ALO" + ], + "asset": "W_TE_Rock_f_02_b.tga" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_EI_Palpatine.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_EI_Palpatine.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_Rbel_Soldier.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Rbel_Soldier.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "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", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\NB_MONCAL_BUILDING.ALO" + ], + "asset": "p_smoke_small_thin2" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027W_Bush_Swmp00.ALO\u0027", + "severity": "Error", + "context": [], + "asset": "W_Bush_Swmp00.ALO" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "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_HIGH.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\W_STARS_HIGH.ALO" + ], + "asset": "Lensflare0" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" + ], + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall_B.tga\u0027 for context: [W_SITH_LEFTHALL.ALO].", + "severity": "Error", + "context": [ + "W_SITH_LEFTHALL.ALO" + ], + "asset": "Cin_Reb_CelebHall_Wall_B.tga" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_Coruscant.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_Coruscant.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "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", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\UI_EWOK_HANDLER.ALO" + ], + "asset": "p_ewok_drag_dirt" + }, + { + "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": [ + "DATA\\ART\\MODELS\\NB_VCH.ALO" + ], + "asset": "P_heat_small01" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027W_Vol_Steam01.ALO\u0027", + "severity": "Error", + "context": [], + "asset": "W_Vol_Steam01.ALO" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "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\\NB_NOGHRI_HUT.ALO" + ], + "asset": "p_explosion_smoke_small_thin5" + }, + { + "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\\W_STARS_MEDIUM.ALO" + ], + "asset": "Lensflare0" + }, + { + "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\\UB_02_STATION_D.ALO" + ], + "asset": "p_uwstation_death" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_Officer.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_Officer.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_04_STATION_D.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\UB_04_STATION_D.ALO" + ], + "asset": "p_uwstation_death" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_Lambda_Head.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Lambda_Head.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_Biker_Row.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Biker_Row.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_DStar_protons.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_DStar_protons.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "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", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\UI_IG88.ALO" + ], + "asset": "p_desert_ground_dust" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_Lambda_Mouth.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Lambda_Mouth.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_Planet_Hoth_High.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_Planet_Hoth_High.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_LOW.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\W_STARS_LOW.ALO" + ], + "asset": "Lensflare0" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_NavyTrooper_Row.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_NavyTrooper_Row.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_DStar_TurretLasers.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_DStar_TurretLasers.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": "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 \u0027Cin_EV_Stardestroyer_Warp.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_EV_Stardestroyer_Warp.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_CINE_LUA.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\W_STARS_CINE_LUA.ALO" + ], + "asset": "Lensflare0" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_DeathStar_Hangar.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_DeathStar_Hangar.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_Fire_Medium.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Fire_Medium.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_CINE.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\W_STARS_CINE.ALO" + ], + "asset": "Lensflare0" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_Rbel_Soldier_Group.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Rbel_Soldier_Group.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "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": "Unable to find .ALO file \u0027W_Volcano_Rock02.ALO\u0027", + "severity": "Error", + "context": [], + "asset": "W_Volcano_Rock02.ALO" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027w_planet_volcanic.alo\u0027", + "severity": "Error", + "context": [], + "asset": "w_planet_volcanic.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_REb_CelebCharacters.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_REb_CelebCharacters.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "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.", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\UV_CRUSADERCLASSCORVETTE.ALO" + ], + "asset": "Default.fx" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" + ], + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall.tga\u0027 for context: [W_SITH_LEFTHALL.ALO].", + "severity": "Error", + "context": [ + "W_SITH_LEFTHALL.ALO" + ], + "asset": "Cin_Reb_CelebHall_Wall.tga" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_EV_lambdaShuttle_150.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_EV_lambdaShuttle_150.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "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\\EB_COMMANDCENTER.ALO" + ], + "asset": "p_explosion_small_delay00" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" + ], + "message": "Could not find texture \u0027UB_girder_B.tga\u0027 for context: [UV_MDU_CAGE.ALO].", + "severity": "Error", + "context": [ + "UV_MDU_CAGE.ALO" + ], + "asset": "UB_girder_B.tga" + }, + { + "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_01_STATION_D.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\UB_01_STATION_D.ALO" + ], + "asset": "p_uwstation_death" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0206_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0206_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0204_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0204_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0102_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0102_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0215_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0215_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0107_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0107_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0504_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Remove_Corruption_Leia" + ], + "asset": "U000_LEI0504_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027AMB_DES_CLEAR_LOOP_1.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Weather_Ambient_Clear_Sandstorm_Loop" + ], + "asset": "AMB_DES_CLEAR_LOOP_1.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0105_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0105_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0213_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0213_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0201_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0201_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0303_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0303_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0103_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0103_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0207_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0207_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_DEF3006_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Corrupt_Sabateur" + ], + "asset": "U000_DEF3006_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0309_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0309_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0209_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0209_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_DEF3106_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Weaken_Sabateur" + ], + "asset": "U000_DEF3106_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0503_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Remove_Corruption_Leia" + ], + "asset": "U000_LEI0503_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0502_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Remove_Corruption_Leia" + ], + "asset": "U000_LEI0502_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0212_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0212_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027AMB_URB_CLEAR_LOOP_1.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Weather_Ambient_Clear_Urban_Loop" + ], + "asset": "AMB_URB_CLEAR_LOOP_1.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0311_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0311_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0115_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0115_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0101_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0101_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0401_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Guard_Leia" + ], + "asset": "U000_LEI0401_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0315_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0315_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0106_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0106_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0603_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Increase_Production_Leia" + ], + "asset": "U000_LEI0603_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0104_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0104_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0501_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Remove_Corruption_Leia" + ], + "asset": "U000_LEI0501_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027TESTUNITMOVE_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Gneneric_Test" + ], + "asset": "TESTUNITMOVE_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0108_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0108_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_MCF1601_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_StarDest_MC30_Frigate" + ], + "asset": "U000_MCF1601_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0111_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0111_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0211_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0211_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0110_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0110_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0403_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Guard_Leia" + ], + "asset": "U000_LEI0403_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0306_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0306_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0308_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0308_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0112_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0112_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0301_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0301_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0404_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Guard_Leia" + ], + "asset": "U000_LEI0404_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_TMC0212_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Tie_Mauler" + ], + "asset": "U000_TMC0212_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027EGL_STAR_VIPER_SPINNING_1.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Star_Viper_Spinning_By" + ], + "asset": "EGL_STAR_VIPER_SPINNING_1.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0208_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0208_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0604_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Increase_Production_Leia" + ], + "asset": "U000_LEI0604_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027FS_BEETLE_3.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "SFX_Anim_Beetle_Footsteps" + ], + "asset": "FS_BEETLE_3.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0109_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0109_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0202_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0202_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0602_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Increase_Production_Leia" + ], + "asset": "U000_LEI0602_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0305_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0305_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_MAL0503_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Assist_Move_Missile_Launcher" + ], + "asset": "U000_MAL0503_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0601_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Increase_Production_Leia" + ], + "asset": "U000_LEI0601_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_ARC3106_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Complete_Troops_Arc_Hammer" + ], + "asset": "U000_ARC3106_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027FS_BEETLE_4.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "SFX_Anim_Beetle_Footsteps" + ], + "asset": "FS_BEETLE_4.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027FS_BEETLE_1.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "SFX_Anim_Beetle_Footsteps" + ], + "asset": "FS_BEETLE_1.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0205_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0205_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0113_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0113_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0314_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0314_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0304_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0304_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0203_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0203_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027C000_DST0102_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "EHD_Death_Star_Activate" + ], + "asset": "C000_DST0102_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0114_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0114_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_ARC3104_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Produce_Troops_Arc_Hammer" + ], + "asset": "U000_ARC3104_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027FS_BEETLE_2.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "SFX_Anim_Beetle_Footsteps" + ], + "asset": "FS_BEETLE_2.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_ARC3105_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Complete_Troops_Arc_Hammer" + ], + "asset": "U000_ARC3105_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0307_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0307_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0402_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Guard_Leia" + ], + "asset": "U000_LEI0402_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0312_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0312_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0210_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0210_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0313_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0313_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" + ], + "message": "Could not find GUI texture \u0027underworld_logo_selected.tga\u0027 at location \u0027MegaTexture\u0027.", + "severity": "Error", + "context": [ + "IDC_PLAY_FACTION_A_BUTTON_BIG", + "MegaTexture" + ], + "asset": "underworld_logo_selected.tga" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" + ], + "message": "Could not find GUI texture \u0027i_button_petro_sliver.tga\u0027 at location \u0027MegaTexture\u0027.", + "severity": "Error", + "context": [ + "IDC_MENU_PETRO_LOGO", + "MegaTexture" + ], + "asset": "i_button_petro_sliver.tga" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" + ], + "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" + ], + "asset": "i_dialogue_button_large_middle_off.tga" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" + ], + "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_rollover.tga" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" + ], + "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" + ], + "asset": "underworld_logo_off.tga" + } + ] +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Options/CommandLine/BaseModVerifyOptions.cs b/src/ModVerify.CliApp/Settings/CommandLine/BaseModVerifyOptions.cs similarity index 83% rename from src/ModVerify.CliApp/Options/CommandLine/BaseModVerifyOptions.cs rename to src/ModVerify.CliApp/Settings/CommandLine/BaseModVerifyOptions.cs index d239112..0e5e203 100644 --- a/src/ModVerify.CliApp/Options/CommandLine/BaseModVerifyOptions.cs +++ b/src/ModVerify.CliApp/Settings/CommandLine/BaseModVerifyOptions.cs @@ -3,57 +3,57 @@ using CommandLine; using PG.StarWarsGame.Engine; -namespace AET.ModVerifyTool.Options.CommandLine; +namespace AET.ModVerify.App.Settings.CommandLine; internal abstract class BaseModVerifyOptions { [Option('v', "verbose", Required = false, HelpText = "Sets output to verbose messages.")] - public bool Verbose { get; set; } + public bool Verbose { get; init; } [Option("offline", Default = false, HelpText = "When set, the application will work in offline mode and does not need an Internet connection.")] - public bool OfflineMode { get; set; } + public bool OfflineMode { get; init; } [Option("minSeverity", Required = false, Default = VerificationSeverity.Information, HelpText = "When set, only findings with at least the specified severity value are processed.")] - public VerificationSeverity MinimumSeverity { get; set; } + public VerificationSeverity MinimumSeverity { get; init; } [Option("suppressions", Required = false, HelpText = "Path to a JSON suppression file.")] - public string? Suppressions { get; set; } + 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 submods 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 or base games itself. " + "The argument cannot be combined with any of --mods, --game or --fallbackGame")] - public string? AutoPath { get; set; } + public string? AutoPath { 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. " + "Leave empty, if you want to verify a game. If you want to use the interactive mode, leave this, --game and --fallbackGame empty.")] - public IList? ModPaths { get; set; } + public IList? ModPaths { get; init; } [Option("game", SetName = "manualPaths", Required = false, Default = null, HelpText = "The path of the base game. For FoC mods this points to the FoC installation, for EaW mods this points to the EaW installation. " + "Leave empty, if you want to auto-detect games. If you want to use the interactive mode, leave this, --mods and --fallbackGame empty. " + "If this argument is set, you also need to set --mods (including sub mods) and --fallbackGame manually.")] - public string? GamePath { get; set; } + public string? GamePath { get; init; } [Option("fallbackGame", SetName = "manualPaths", Required = false, Default = null, HelpText = "The path of the fallback game. Usually this points to the EaW installation. This argument only recognized if --game is set.")] - public string? FallbackGamePath { get; set; } + 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'. " + "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; set; } + public GameEngineType? GameType { get; init; } [Option("additionalFallbackPaths", Required = false, Separator = ';', HelpText = "Additional fallback paths, which may contain assets that shall be included when doing the verification. Do not add EaW here. " + "Multiple paths can be separated using the ';' (semicolon) character.")] - public IList? AdditionalFallbackPath { get; set; } + public IList? AdditionalFallbackPath { get; init; } [Option("parallel", Default = false, HelpText = "When set, game verifiers will run in parallel. " + "While this may reduce analysis time, console output might be harder to read.")] - public bool Parallel { get; set; } + public bool Parallel { get; init; } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Options/CommandLine/CreateBaselineVerbOption.cs b/src/ModVerify.CliApp/Settings/CommandLine/CreateBaselineVerbOption.cs similarity index 59% rename from src/ModVerify.CliApp/Options/CommandLine/CreateBaselineVerbOption.cs rename to src/ModVerify.CliApp/Settings/CommandLine/CreateBaselineVerbOption.cs index 78132cc..dc60a73 100644 --- a/src/ModVerify.CliApp/Options/CommandLine/CreateBaselineVerbOption.cs +++ b/src/ModVerify.CliApp/Settings/CommandLine/CreateBaselineVerbOption.cs @@ -1,10 +1,10 @@ using CommandLine; -namespace AET.ModVerifyTool.Options.CommandLine; +namespace AET.ModVerify.App.Settings.CommandLine; [Verb("createBaseline", HelpText = "Verifies the specified game and creates a new baseline file at the specified location.")] -internal class CreateBaselineVerbOption : BaseModVerifyOptions +internal sealed class CreateBaselineVerbOption : BaseModVerifyOptions { [Option('o', "outFile", Required = true, HelpText = "The file path of the new baseline file.")] - public string OutputFile { get; set; } + public required string OutputFile { get; init; } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/CommandLine/ModVerifyOptionsContainer.cs b/src/ModVerify.CliApp/Settings/CommandLine/ModVerifyOptionsContainer.cs new file mode 100644 index 0000000..c9aab20 --- /dev/null +++ b/src/ModVerify.CliApp/Settings/CommandLine/ModVerifyOptionsContainer.cs @@ -0,0 +1,12 @@ +using AnakinRaW.ApplicationBase.Update.Options; + +namespace AET.ModVerify.App.Settings.CommandLine; + +internal sealed class ModVerifyOptionsContainer +{ + public BaseModVerifyOptions? ModVerifyOptions { get; init; } + + public ApplicationUpdateOptions? UpdateOptions { get; init; } + + public bool HasOptions => ModVerifyOptions is not null || UpdateOptions is not null; +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/CommandLine/ModVerifyOptionsParser.cs b/src/ModVerify.CliApp/Settings/CommandLine/ModVerifyOptionsParser.cs new file mode 100644 index 0000000..63a29d8 --- /dev/null +++ b/src/ModVerify.CliApp/Settings/CommandLine/ModVerifyOptionsParser.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using AET.ModVerify.App.Utilities; +using AnakinRaW.ApplicationBase.Environment; +using AnakinRaW.ApplicationBase.Update.Options; +using AnakinRaW.ExternalUpdater; +using CommandLine; +using CommandLine.Text; +using Microsoft.Extensions.Logging; + +namespace AET.ModVerify.App.Settings.CommandLine; + +internal sealed class ModVerifyOptionsParser +{ + private readonly ApplicationEnvironment _applicationEnvironment; + private readonly ILogger? _logger; + + [field: AllowNull, MaybeNull] + private Type[] AvailableVerbTypes => LazyInitializer.EnsureInitialized(ref field, GetAvailableVerbTypes)!; + + public ModVerifyOptionsParser(ApplicationEnvironment applicationEnvironment, ILoggerFactory? loggerFactory) + { + _applicationEnvironment = applicationEnvironment; + _logger = loggerFactory?.CreateLogger(GetType()); + } + + public ModVerifyOptionsContainer Parse(IReadOnlyList args) + { + // If the application is updatable (.NET Framework) we need to remove potential arguments from the external updater + // in order to keep strict parsing rules enabled for better user error messages. + if (_applicationEnvironment.IsUpdatable()) + args = StripExternalUpdateResults(args); + + return ParseArguments(args); + } + + private ModVerifyOptionsContainer ParseArguments(IReadOnlyList args) + { + // Empty arguments means that we are "interactive" mode (user simply double-clicked the executable) + if (args.Count == 0) + { + return new ModVerifyOptionsContainer + { + ModVerifyOptions = VerifyVerbOption.WithoutArguments, + UpdateOptions = null + }; + } + + var parseResult = Parser.Default.ParseArguments(args, AvailableVerbTypes); + + BaseModVerifyOptions? modVerifyOptions = null; + ApplicationUpdateOptions? updateOptions = null; + + parseResult.WithParsed(o => modVerifyOptions = o); + parseResult.WithParsed(o => updateOptions = o); + + parseResult.WithNotParsed(_ => + { + _logger?.LogError("Unable to parse command line"); + Console.WriteLine(HelpText.AutoBuild(parseResult).ToString()); + }); + + return new ModVerifyOptionsContainer + { + ModVerifyOptions = modVerifyOptions, + UpdateOptions = updateOptions, + }; + } + + public static IReadOnlyList StripExternalUpdateResults(IReadOnlyList args) + { + // Parser.Default.FormatCommandLine(ResultOption) as used in ProcessTool.cs either returns + // two argument segments or none (if Result == UpdaterNotRun) + if (args.Count < 2) + return args; + + // The external updater promises to append the result to the arguments. + // Thus, it's sufficient to check the second last segment whether it matches. + var secondLast = args[^2]; + + return secondLast == ExternalUpdaterResultOptions.RawOptionString + ? [..args.Take(args.Count - 2)] + : args; + } + + private Type[] GetAvailableVerbTypes() + { + return _applicationEnvironment.IsUpdatable() + ? [typeof(VerifyVerbOption), typeof(CreateBaselineVerbOption), typeof(ApplicationUpdateOptions)] + : [typeof(VerifyVerbOption), typeof(CreateBaselineVerbOption)]; + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/CommandLine/VerifyVerbOption.cs b/src/ModVerify.CliApp/Settings/CommandLine/VerifyVerbOption.cs new file mode 100644 index 0000000..97f1536 --- /dev/null +++ b/src/ModVerify.CliApp/Settings/CommandLine/VerifyVerbOption.cs @@ -0,0 +1,39 @@ +using AET.ModVerify.Reporting; +using CommandLine; + +namespace AET.ModVerify.App.Settings.CommandLine; + +[Verb("verify", HelpText = "Verifies the specified game and reports the findings.")] +internal sealed class VerifyVerbOption : BaseModVerifyOptions +{ + internal static readonly VerifyVerbOption WithoutArguments = new() + { + IsRunningWithoutArguments = true, + SearchBaselineLocally = true, + }; + + [Option('o', "outDir", Required = false, HelpText = "Directory where result files shall be stored to.")] + 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.")] + 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.")] + 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; } + + [Option("searchBaseline", SetName = "baselineSelection", Required = false, + HelpText = "When set, the application will search for baseline files and use them for verification. Cannot be used together with --baseline")] + public bool SearchBaselineLocally { get; init; } + + public bool IsRunningWithoutArguments { get; init; } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Options/GameInstallationsSettings.cs b/src/ModVerify.CliApp/Settings/GameInstallationsSettings.cs similarity index 74% rename from src/ModVerify.CliApp/Options/GameInstallationsSettings.cs rename to src/ModVerify.CliApp/Settings/GameInstallationsSettings.cs index 667e2cf..00bc4f0 100644 --- a/src/ModVerify.CliApp/Options/GameInstallationsSettings.cs +++ b/src/ModVerify.CliApp/Settings/GameInstallationsSettings.cs @@ -1,11 +1,10 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using PG.StarWarsGame.Engine; -namespace AET.ModVerifyTool.Options; +namespace AET.ModVerify.App.Settings; -internal record GameInstallationsSettings +internal sealed record GameInstallationsSettings { public bool Interactive => string.IsNullOrEmpty(AutoPath) && ModPaths.Count == 0 && string.IsNullOrEmpty(GamePath); @@ -17,13 +16,13 @@ internal record GameInstallationsSettings public string? AutoPath { get; init; } - public IList ModPaths { get; init; } = Array.Empty(); + public IList ModPaths { get; init; } = []; public string? GamePath { get; init; } public string? FallbackGamePath { get; init; } - public IList AdditionalFallbackPaths { get; init; } = Array.Empty(); + public IList AdditionalFallbackPaths { get; init; } = []; public GameEngineType? EngineType { get; init; } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Options/ModVerifyAppSettings.cs b/src/ModVerify.CliApp/Settings/ModVerifyAppSettings.cs similarity index 72% rename from src/ModVerify.CliApp/Options/ModVerifyAppSettings.cs rename to src/ModVerify.CliApp/Settings/ModVerifyAppSettings.cs index 6a3b0bd..3376354 100644 --- a/src/ModVerify.CliApp/Options/ModVerifyAppSettings.cs +++ b/src/ModVerify.CliApp/Settings/ModVerifyAppSettings.cs @@ -1,9 +1,8 @@ using System.Diagnostics.CodeAnalysis; using AET.ModVerify.Reporting; -using AET.ModVerify.Reporting.Settings; using AET.ModVerify.Settings; -namespace AET.ModVerifyTool.Options; +namespace AET.ModVerify.App.Settings; internal sealed class ModVerifyAppSettings { @@ -11,18 +10,14 @@ internal sealed class ModVerifyAppSettings public required VerifyPipelineSettings VerifyPipelineSettings { get; init; } - public required GlobalVerifyReportSettings GlobalReportSettings { get; init; } + public required ModVerifyReportSettings ReportSettings { get; init; } public required GameInstallationsSettings GameInstallationsSettings { get; init; } public VerificationSeverity? AppThrowsOnMinimumSeverity { get; init; } - public string? ReportOutput { get; init; } - [MemberNotNullWhen(true, nameof(NewBaselinePath))] public bool CreateNewBaseline => !string.IsNullOrEmpty(NewBaselinePath); public string? NewBaselinePath { get; init; } - - public bool Offline { get; init; } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/ModVerifyReportSettings.cs b/src/ModVerify.CliApp/Settings/ModVerifyReportSettings.cs new file mode 100644 index 0000000..482b844 --- /dev/null +++ b/src/ModVerify.CliApp/Settings/ModVerifyReportSettings.cs @@ -0,0 +1,14 @@ +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/SettingsBuilder.cs b/src/ModVerify.CliApp/Settings/SettingsBuilder.cs similarity index 60% rename from src/ModVerify.CliApp/SettingsBuilder.cs rename to src/ModVerify.CliApp/Settings/SettingsBuilder.cs index d2ba861..3c5535f 100644 --- a/src/ModVerify.CliApp/SettingsBuilder.cs +++ b/src/ModVerify.CliApp/Settings/SettingsBuilder.cs @@ -1,23 +1,20 @@ -using AET.ModVerify.Reporting; -using AET.ModVerify.Reporting.Settings; -using AET.ModVerify.Settings; -using AET.ModVerifyTool.Options; -using Microsoft.Extensions.DependencyInjection; -using System; +using System; using System.Collections.Generic; -using System.IO; using System.IO.Abstractions; +using AET.ModVerify.App.Settings.CommandLine; +using AET.ModVerify.App.Utilities; using AET.ModVerify.Pipeline; -using AET.ModVerifyTool.Options.CommandLine; +using AET.ModVerify.Reporting; +using AET.ModVerify.Settings; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace AET.ModVerifyTool; +namespace AET.ModVerify.App.Settings; -internal sealed class SettingsBuilder(IServiceProvider services) +internal sealed class SettingsBuilder(IServiceProvider serviceProvider) { - private readonly IFileSystem _fileSystem = services.GetRequiredService(); - private readonly ILogger? _logger = - services.GetRequiredService()?.CreateLogger(typeof(SettingsBuilder)); + private readonly ILogger? _logger = serviceProvider.GetService()?.CreateLogger(typeof(SettingsBuilder)); + private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); public ModVerifyAppSettings BuildSettings(BaseModVerifyOptions options) { @@ -33,12 +30,6 @@ public ModVerifyAppSettings BuildSettings(BaseModVerifyOptions options) private ModVerifyAppSettings BuildFromVerifyVerb(VerifyVerbOption verifyOptions) { - var output = Environment.CurrentDirectory; - var outDir = verifyOptions.OutputDirectory; - - if (!string.IsNullOrEmpty(outDir)) - output = _fileSystem.Path.GetFullPath(_fileSystem.Path.Combine(Environment.CurrentDirectory, outDir!)); - return new ModVerifyAppSettings { VerifyPipelineSettings = new VerifyPipelineSettings @@ -54,9 +45,7 @@ private ModVerifyAppSettings BuildFromVerifyVerb(VerifyVerbOption verifyOptions) }, AppThrowsOnMinimumSeverity = verifyOptions.MinimumFailureSeverity, GameInstallationsSettings = BuildInstallationSettings(verifyOptions), - GlobalReportSettings = BuilderGlobalReportSettings(verifyOptions), - ReportOutput = output, - Offline = verifyOptions.OfflineMode + ReportSettings = BuildReportSettings(verifyOptions), }; VerificationSeverity? GetVerifierMinimumThrowSeverity() @@ -66,8 +55,8 @@ private ModVerifyAppSettings BuildFromVerifyVerb(VerifyVerbOption verifyOptions) { if (minFailSeverity == null) { - _logger?.LogWarning($"Verification is configured to fail fast but 'minFailSeverity' is not specified. " + - $"Using severity '{VerificationSeverity.Information}'."); + _logger?.LogWarning(ModVerifyConstants.ConsoleEventId, + "Verification is configured to fail fast but 'minFailSeverity' is not specified. Using severity '{Info}'.", VerificationSeverity.Information); minFailSeverity = VerificationSeverity.Information; } @@ -97,49 +86,28 @@ private ModVerifyAppSettings BuildFromCreateBaselineVerb(CreateBaselineVerbOptio }, AppThrowsOnMinimumSeverity = null, GameInstallationsSettings = BuildInstallationSettings(baselineVerb), - GlobalReportSettings = BuilderGlobalReportSettings(baselineVerb), + ReportSettings = BuildReportSettings(baselineVerb), NewBaselinePath = baselineVerb.OutputFile, - ReportOutput = null, - Offline = baselineVerb.OfflineMode }; } - private GlobalVerifyReportSettings BuilderGlobalReportSettings(BaseModVerifyOptions options) + private static ModVerifyReportSettings BuildReportSettings(BaseModVerifyOptions options) { - return new GlobalVerifyReportSettings + var baselinePath = (options as VerifyVerbOption)?.Baseline; + + return new ModVerifyReportSettings { - Baseline = CreateBaseline(), - Suppressions = CreateSuppressions(), + BaselinePath = baselinePath, MinimumReportSeverity = options.MinimumSeverity, + SearchBaselineLocally = SearchLocally(options), + SuppressionsPath = options.Suppressions }; - VerificationBaseline CreateBaseline() - { - // It does not make sense to create a baseline on another baseline. - if (options is not VerifyVerbOption verifyOptions || string.IsNullOrEmpty(verifyOptions.Baseline)) - return VerificationBaseline.Empty; - - using var fs = _fileSystem.FileStream.New(verifyOptions.Baseline!, FileMode.Open, FileAccess.Read); - - try - { - return VerificationBaseline.FromJson(fs); - } - catch (IncompatibleBaselineException) - { - Console.WriteLine($"The baseline '{verifyOptions.Baseline}' is not compatible with with version of ModVerify." + - $"{Environment.NewLine}Please generate a new baseline file or download the latest version." + - $"{Environment.NewLine}"); - throw; - } - } - - SuppressionList CreateSuppressions() + static bool SearchLocally(BaseModVerifyOptions o) { - if (options.Suppressions is null) - return SuppressionList.Empty; - using var fs = _fileSystem.FileStream.New(options.Suppressions, FileMode.Open, FileAccess.Read); - return SuppressionList.FromJson(fs); + if (o is not VerifyVerbOption v) + return false; + return v.SearchBaselineLocally || v.LaunchedWithoutArguments(); } } @@ -167,16 +135,16 @@ private GameInstallationsSettings BuildInstallationSettings(BaseModVerifyOptions var gamePath = options.GamePath; if (!string.IsNullOrEmpty(gamePath)) - gamePath = _fileSystem.Path.GetFullPath(gamePath); + gamePath = _fileSystem.Path.GetFullPath(gamePath!); string? fallbackGamePath = null; if (!string.IsNullOrEmpty(gamePath) && !string.IsNullOrEmpty(options.FallbackGamePath)) - fallbackGamePath = _fileSystem.Path.GetFullPath(options.FallbackGamePath); + fallbackGamePath = _fileSystem.Path.GetFullPath(options.FallbackGamePath!); var autoPath = options.AutoPath; if (!string.IsNullOrEmpty(autoPath)) - autoPath = _fileSystem.Path.GetFullPath(autoPath); + autoPath = _fileSystem.Path.GetFullPath(autoPath!); return new GameInstallationsSettings { diff --git a/src/ModVerify.CliApp/Updates/GithubReleaseEntry.cs b/src/ModVerify.CliApp/Updates/Github/GithubReleaseEntry.cs similarity index 91% rename from src/ModVerify.CliApp/Updates/GithubReleaseEntry.cs rename to src/ModVerify.CliApp/Updates/Github/GithubReleaseEntry.cs index 968e6a9..4cd4585 100644 --- a/src/ModVerify.CliApp/Updates/GithubReleaseEntry.cs +++ b/src/ModVerify.CliApp/Updates/Github/GithubReleaseEntry.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace AET.ModVerifyTool.Updates; +namespace AET.ModVerify.App.Updates.Github; [JsonUnmappedMemberHandling(JsonUnmappedMemberHandling.Skip)] [method: JsonConstructor] diff --git a/src/ModVerify.CliApp/Updates/GithubReleaseList.cs b/src/ModVerify.CliApp/Updates/Github/GithubReleaseList.cs similarity index 70% rename from src/ModVerify.CliApp/Updates/GithubReleaseList.cs rename to src/ModVerify.CliApp/Updates/Github/GithubReleaseList.cs index a54edfb..92ef368 100644 --- a/src/ModVerify.CliApp/Updates/GithubReleaseList.cs +++ b/src/ModVerify.CliApp/Updates/Github/GithubReleaseList.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -namespace AET.ModVerifyTool.Updates; +namespace AET.ModVerify.App.Updates.Github; internal sealed class GithubReleaseList : List; \ No newline at end of file diff --git a/src/ModVerify.CliApp/Updates/ModVerifyUpdaterChecker.cs b/src/ModVerify.CliApp/Updates/Github/GithubUpdateChecker.cs similarity index 60% rename from src/ModVerify.CliApp/Updates/ModVerifyUpdaterChecker.cs rename to src/ModVerify.CliApp/Updates/Github/GithubUpdateChecker.cs index d0ab59b..df4d769 100644 --- a/src/ModVerify.CliApp/Updates/ModVerifyUpdaterChecker.cs +++ b/src/ModVerify.CliApp/Updates/Github/GithubUpdateChecker.cs @@ -1,29 +1,31 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System; +using System; using System.IO; using System.Linq; using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Semver; -namespace AET.ModVerifyTool.Updates; +namespace AET.ModVerify.App.Updates.Github; -internal sealed class ModVerifyUpdaterChecker +internal class GithubUpdateChecker { private readonly ILogger? _logger; + private readonly ModVerifyAppEnvironment _appEnvironment; - public ModVerifyUpdaterChecker(IServiceProvider serviceProvider) + public GithubUpdateChecker(IServiceProvider serviceProvider) { _logger = serviceProvider.GetService()?.CreateLogger(GetType()); + _appEnvironment = serviceProvider.GetRequiredService(); } - public async Task CheckForUpdateAsync() + public async Task CheckForUpdateAsync() { var githubReleases = await DownloadReleaseList().ConfigureAwait(false); - var branch = ModVerifyUpdaterInformation.BranchName; + var branch = GithubUpdateConstants.BranchName; var latestRelease = githubReleases.FirstOrDefault(r => r.Branch == branch); if (latestRelease == null) @@ -32,20 +34,20 @@ public async Task CheckForUpdateAsync() if (!SemVersion.TryParse(latestRelease.Tag, SemVersionStyles.Any, out var latestVersion)) throw new InvalidOperationException($"Cannot create a version from tag '{latestRelease.Tag}'."); - var currentVersion = ModVerifyUpdaterInformation.CurrentVersion; + var currentVersion = _appEnvironment.AssemblyInfo.InformationalAsSemVer(); if (currentVersion is null) throw new InvalidOperationException("Unable to get current version."); if (SemVersion.ComparePrecedence(currentVersion, latestVersion) >= 0) { - _logger?.LogDebug($"No update available - [Current Version = {currentVersion}], [Available Version = {latestVersion}]"); + _logger?.LogDebug("No update available - [Current Version = {CurrentVersion}], [Available Version = {LatestVersion}]", currentVersion, latestVersion); return default; } - _logger?.LogDebug($"Update available - [Current Version = {currentVersion}], [Available Version = {latestVersion}]"); - return new UpdateInfo + _logger?.LogDebug("Update available - [Current Version = {CurrentVersion}], [Available Version = {LatestVersion}]", currentVersion, latestVersion); + return new GithubUpdateInfo { - DownloadLink = ModVerifyUpdaterInformation.ModVerifyReleasesDownloadLink, + DownloadLink = GithubUpdateConstants.ModVerifyReleasesDownloadLink, IsUpdateAvailable = true, NewVersion = latestVersion.ToString() }; @@ -54,8 +56,8 @@ public async Task CheckForUpdateAsync() private static async Task DownloadReleaseList() { using var httpClient = new HttpClient(); - httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(ModVerifyUpdaterInformation.UserAgent); - using var downloadStream = await httpClient.GetStreamAsync(ModVerifyUpdaterInformation.GithubReleasesApiLink).ConfigureAwait(false); + httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(GithubUpdateConstants.UserAgent); + using var downloadStream = await httpClient.GetStreamAsync(GithubUpdateConstants.GithubReleasesApiLink).ConfigureAwait(false); using var jsonStream = new MemoryStream(); await downloadStream.CopyToAsync(jsonStream).ConfigureAwait(false); diff --git a/src/ModVerify.CliApp/Updates/Github/GithubUpdateConstants.cs b/src/ModVerify.CliApp/Updates/Github/GithubUpdateConstants.cs new file mode 100644 index 0000000..7695246 --- /dev/null +++ b/src/ModVerify.CliApp/Updates/Github/GithubUpdateConstants.cs @@ -0,0 +1,9 @@ +namespace AET.ModVerify.App.Updates.Github; + +internal static class GithubUpdateConstants +{ + public const string BranchName = "main"; + public const string GithubReleasesApiLink = "https://api.github.com/repos/AlamoEngine-Tools/ModVerify/releases"; + public const string ModVerifyReleasesDownloadLink = "https://github.com/AlamoEngine-Tools/ModVerify/releases/latest"; + public const string UserAgent = "AET.Modifo"; +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Updates/UpdateInfo.cs b/src/ModVerify.CliApp/Updates/Github/GithubUpdateInfo.cs similarity index 75% rename from src/ModVerify.CliApp/Updates/UpdateInfo.cs rename to src/ModVerify.CliApp/Updates/Github/GithubUpdateInfo.cs index b6a6505..8d8f532 100644 --- a/src/ModVerify.CliApp/Updates/UpdateInfo.cs +++ b/src/ModVerify.CliApp/Updates/Github/GithubUpdateInfo.cs @@ -1,8 +1,8 @@ using System.Diagnostics.CodeAnalysis; -namespace AET.ModVerifyTool.Updates; +namespace AET.ModVerify.App.Updates.Github; -internal readonly struct UpdateInfo +internal readonly struct GithubUpdateInfo { public string DownloadLink { get; init; } diff --git a/src/ModVerify.CliApp/Updates/ModVerifyUpdateMode.cs b/src/ModVerify.CliApp/Updates/ModVerifyUpdateMode.cs new file mode 100644 index 0000000..882a6a2 --- /dev/null +++ b/src/ModVerify.CliApp/Updates/ModVerifyUpdateMode.cs @@ -0,0 +1,8 @@ +namespace AET.ModVerify.App.Updates; + +public enum ModVerifyUpdateMode +{ + CheckOnly, + InteractiveUpdate, + AutoUpdate, +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Updates/ModVerifyUpdater.cs b/src/ModVerify.CliApp/Updates/ModVerifyUpdater.cs new file mode 100644 index 0000000..af7d369 --- /dev/null +++ b/src/ModVerify.CliApp/Updates/ModVerifyUpdater.cs @@ -0,0 +1,149 @@ +using AET.ModVerify.App.Updates.Github; +using AET.ModVerify.App.Updates.SelfUpdate; +using AET.ModVerify.App.Utilities; +using AnakinRaW.ApplicationBase; +using AnakinRaW.ApplicationBase.Update.Options; +using AnakinRaW.AppUpdaterFramework.Metadata.Update; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace AET.ModVerify.App.Updates; + +internal sealed class ModVerifyUpdater +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger? _logger; + private readonly ModVerifyAppEnvironment _appEnvironment; + + public ModVerifyUpdater(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + _logger = serviceProvider.GetService()?.CreateLogger(GetType()); + _appEnvironment = serviceProvider.GetRequiredService(); + } + + public async Task RunUpdateProcedure(ApplicationUpdateOptions updateOptions, ModVerifyUpdateMode mode) + { + _logger?.LogTrace("Running update procedure - '{mode}'", mode); + + // If we are in the check-only mode, GitHub check is sufficient. + if (mode == ModVerifyUpdateMode.CheckOnly) + { + await CheckForUpdateAndReport().ConfigureAwait(false); + return; + } + + await UpdateApplication(updateOptions, mode).ConfigureAwait(false); + } + + private async Task UpdateApplication(ApplicationUpdateOptions updateOptions, ModVerifyUpdateMode mode) + { + if (!_appEnvironment.IsUpdatable(out var updatableEnvironment)) + { + _logger?.LogWarning("Application is not updatable, yet we entered the update path. Checking only."); + await CheckForUpdateAndReport().ConfigureAwait(false); + return; + } + + var updater = new ModVerifyApplicationUpdater(updatableEnvironment, _serviceProvider); + + var actualBranchName = updater.GetBranchNameFromRegistry(updateOptions.BranchName, false); + var branch = updater.CreateBranch(actualBranchName, updateOptions.ManifestUrl); + + using (ConsoleUtilities.CreateHorizontalFrame(length: 40, startWithNewLine: true, newLineAtEnd: true)) + { + var currentAction = "checking for update"; + try + { + var updateCheckSpinner = new ConsoleSpinnerOptions + { + CompletedMessage = "Update check completed.", + RunningMessage = "Checking for update...", + FailedMessage = "Update check failed", + HideCursor = true + }; + + var updateCatalog = await ConsoleSpinner.Run(async () => + await updater.CheckForUpdateAsync(branch, CancellationToken.None), + updateCheckSpinner); + + + if (updateCatalog.Action != UpdateCatalogAction.Update) + { + Console.WriteLine("No update available."); + return; + } + + Console.ForegroundColor = ConsoleColor.DarkGreen; + Console.WriteLine($"New update available: Version {updateCatalog.UpdateReference.Version}"); + Console.ResetColor(); + + if (mode == ModVerifyUpdateMode.InteractiveUpdate) + { + var shallUpdate = ConsoleUtilities.UserYesNoQuestion("Do you want to update now?"); + if (!shallUpdate) + return; + } + + currentAction = "updating"; + + + var updatingSpinner = new ConsoleSpinnerOptions + { + RunningMessage = $"Updating {ModVerifyConstants.AppNameString}...", + HideCursor = true + }; + await ConsoleSpinner.Run(async () => + await updater.UpdateAsync(updateCatalog, CancellationToken.None), + updatingSpinner); + } + catch (Exception e) + { + WriteError(e, $"Error while {currentAction}: {e.Message}"); + } + } + } + + private async Task CheckForUpdateAndReport() + { + _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Checking for available update..."); + try + { + var updateInfo = await new GithubUpdateChecker(_serviceProvider) + .CheckForUpdateAsync().ConfigureAwait(false); + + if (updateInfo.IsUpdateAvailable) + { + using (ConsoleUtilities.HorizontalLineSeparatedBlock(startWithNewLine: true, newLineAtEnd: true)) + { + Console.ForegroundColor = ConsoleColor.DarkGreen; + Console.WriteLine("New Update Available!"); + Console.ResetColor(); + Console.WriteLine($"Version: {updateInfo.NewVersion}, Download here: {updateInfo.DownloadLink}"); + } + } + else + { + _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "No update available."); + } + } + catch (Exception e) + { + _logger?.LogWarning(ModVerifyConstants.ConsoleEventId, "Unable to check for updates due to an internal error: {message}", e.Message); + _logger?.LogTrace(e, "Checking for update failed: {message}", e.Message); + } + } + + private void WriteError(Exception e, string? customMessage) + { + Console.WriteLine(); + Console.ForegroundColor = ConsoleColor.DarkRed; + if (!string.IsNullOrEmpty(customMessage)) + Console.WriteLine(customMessage); + Console.ResetColor(); + _logger?.LogError(e, e.Message); + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Updates/ModVerifyUpdaterInformation.cs b/src/ModVerify.CliApp/Updates/ModVerifyUpdaterInformation.cs deleted file mode 100644 index 2fa1022..0000000 --- a/src/ModVerify.CliApp/Updates/ModVerifyUpdaterInformation.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Diagnostics; -using Semver; - -namespace AET.ModVerifyTool.Updates; - -internal static class ModVerifyUpdaterInformation -{ - public const string BranchName = "main"; - public const string GithubReleasesApiLink = "https://api.github.com/repos/AlamoEngine-Tools/ModVerify/releases"; - public const string ModVerifyReleasesDownloadLink = "https://github.com/AlamoEngine-Tools/ModVerify/releases/latest"; - public const string UserAgent = "AET.Modifo"; - - public static readonly SemVersion? CurrentVersion; - - static ModVerifyUpdaterInformation() - { - var currentAssembly = typeof(ModVerifyUpdaterInformation).Assembly; - var fi = FileVersionInfo.GetVersionInfo(currentAssembly.Location); - SemVersion.TryParse(fi.ProductVersion, SemVersionStyles.Any, out var currentVersion); - CurrentVersion = currentVersion; - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Updates/SelfUpdate/AssemblyInfo.cs b/src/ModVerify.CliApp/Updates/SelfUpdate/AssemblyInfo.cs new file mode 100644 index 0000000..4b94b5e --- /dev/null +++ b/src/ModVerify.CliApp/Updates/SelfUpdate/AssemblyInfo.cs @@ -0,0 +1,6 @@ +#if NETFRAMEWORK +using AnakinRaW.AppUpdaterFramework.Attributes; + +[assembly: UpdateProduct("AET ModVerify")] +[assembly: UpdateComponent("AET.ModVerify.Exe", Name = "AET ModVerify")] +#endif \ No newline at end of file diff --git a/src/ModVerify.CliApp/Updates/SelfUpdate/ModVerifyApplicationUpdater.cs b/src/ModVerify.CliApp/Updates/SelfUpdate/ModVerifyApplicationUpdater.cs new file mode 100644 index 0000000..9b97043 --- /dev/null +++ b/src/ModVerify.CliApp/Updates/SelfUpdate/ModVerifyApplicationUpdater.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using AnakinRaW.ApplicationBase.Environment; +using AnakinRaW.ApplicationBase.Update; +using AnakinRaW.AppUpdaterFramework.Metadata.Product; +using AnakinRaW.AppUpdaterFramework.Metadata.Update; + +namespace AET.ModVerify.App.Updates.SelfUpdate; + + +internal class ModVerifyApplicationUpdater( + UpdatableApplicationEnvironment environment, + IServiceProvider serviceProvider) + : ApplicationUpdater(environment, serviceProvider) +{ + public override async Task CheckForUpdateAsync(ProductBranch branch, CancellationToken token = default) + { + var updateReference = ProductService.CreateProductReference(null, branch); + + var updateCatalog = await UpdateService.CheckForUpdatesAsync(updateReference, token); + + if (updateCatalog is null) + throw new InvalidOperationException("Update service was already doing something."); + + return updateCatalog.Action is UpdateCatalogAction.Install or UpdateCatalogAction.Uninstall + ? throw new NotSupportedException("Install and Uninstall operations are not supported") + : updateCatalog; + } + + public override async Task UpdateAsync(UpdateCatalog updateCatalog, CancellationToken token = default) + { + var updateResult = await UpdateService.UpdateAsync(updateCatalog, token).ConfigureAwait(false); + if (updateResult is null) + throw new InvalidOperationException("There is already an update running."); + + var resultHandler = new ModVerifyUpdateResultHandler(Environment, ServiceProvider); + await resultHandler.Handle(updateResult).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Updates/SelfUpdate/ModVerifyUpdateResultHandler.cs b/src/ModVerify.CliApp/Updates/SelfUpdate/ModVerifyUpdateResultHandler.cs new file mode 100644 index 0000000..42ab413 --- /dev/null +++ b/src/ModVerify.CliApp/Updates/SelfUpdate/ModVerifyUpdateResultHandler.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; +using AnakinRaW.ApplicationBase.Environment; +using AnakinRaW.ApplicationBase.Update; +using AnakinRaW.AppUpdaterFramework.Handlers; +using AnakinRaW.AppUpdaterFramework.Updater; + +namespace AET.ModVerify.App.Updates.SelfUpdate; + +internal sealed class ModVerifyUpdateResultHandler( + UpdatableApplicationEnvironment applicationEnvironment, + IServiceProvider serviceProvider) + : ApplicationUpdateResultHandler(applicationEnvironment, serviceProvider) +{ + protected override Task ShowError(UpdateResult updateResult) + { + Console.WriteLine(); + Console.WriteLine($"Update failed with error: {updateResult.ErrorMessage}"); + return base.ShowError(updateResult); + } + + protected override void RestartApplication(RestartReason reason) + { + Console.WriteLine(); + Console.WriteLine("Restarting application to complete update..."); + base.RestartApplication(reason); + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Utilities/ExtensionMethods.cs b/src/ModVerify.CliApp/Utilities/ExtensionMethods.cs new file mode 100644 index 0000000..b2e7ab0 --- /dev/null +++ b/src/ModVerify.CliApp/Utilities/ExtensionMethods.cs @@ -0,0 +1,41 @@ +using System.Diagnostics.CodeAnalysis; +using AET.ModVerify.App.Settings.CommandLine; +using AnakinRaW.ApplicationBase.Environment; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Infrastructure.Games; + +namespace AET.ModVerify.App.Utilities; + +internal static class ExtensionMethods +{ + public static GameEngineType ToEngineType(this GameType type) + { + return type == GameType.Foc ? GameEngineType.Foc : GameEngineType.Eaw; + } + + public static GameType FromEngineType(this GameEngineType type) + { + return type == GameEngineType.Foc ? GameType.Foc : GameType.Eaw; + } + + extension(ApplicationEnvironment modVerifyEnvironment) + { + public bool IsUpdatable() + { + return modVerifyEnvironment.IsUpdatable(out _); + } + + public bool IsUpdatable([NotNullWhen(true)] out UpdatableApplicationEnvironment? updatableEnvironment) + { + updatableEnvironment = modVerifyEnvironment as UpdatableApplicationEnvironment; + return updatableEnvironment is not null; + } + } + + public static bool LaunchedWithoutArguments(this BaseModVerifyOptions options) + { + if (options is VerifyVerbOption verifyOptions) + return verifyOptions.IsRunningWithoutArguments; + return false; + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Utilities/ModVerifyConsoleUtilities.cs b/src/ModVerify.CliApp/Utilities/ModVerifyConsoleUtilities.cs new file mode 100644 index 0000000..6e6664a --- /dev/null +++ b/src/ModVerify.CliApp/Utilities/ModVerifyConsoleUtilities.cs @@ -0,0 +1,30 @@ +using AnakinRaW.ApplicationBase; +using Figgle; +using System; + +namespace AET.ModVerify.App.Utilities; + +[GenerateFiggleText("HeaderText", "standard", ModVerifyConstants.AppNameString)] +internal static partial class ModVerifyConsoleUtilities +{ + public static void WriteHeader(string? version = null) + { + const int lineLength = 73; + const string author = "by AnakinRaW"; + + ConsoleUtilities.WriteHorizontalLine('*', lineLength); + Console.WriteLine(HeaderText); + if (!string.IsNullOrEmpty(version)) + { + Console.ForegroundColor = ConsoleColor.DarkGray; + ConsoleUtilities.WriteLineRight($"Version: {version}", lineLength); + Console.ResetColor(); + Console.WriteLine(); + } + ConsoleUtilities.WriteHorizontalLine('*', lineLength); + + ConsoleUtilities.WriteLineRight(author, lineLength); + Console.WriteLine(); + Console.WriteLine(); + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Utilities/Spinner.cs b/src/ModVerify.CliApp/Utilities/Spinner.cs new file mode 100644 index 0000000..f25edda --- /dev/null +++ b/src/ModVerify.CliApp/Utilities/Spinner.cs @@ -0,0 +1,172 @@ +using AnakinRaW.CommonUtilities; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace AET.ModVerify.App.Utilities; + +/// +/// Options for configuring a . +/// +public sealed class ConsoleSpinnerOptions +{ + public string? RunningMessage { get; init; } + public string? CompletedMessage { get; init; } + public string? FailedMessage { get; init; } + public bool HideCursor { get; init; } + public TextWriter Writer { get; init; } = Console.Out; + public int Interval { get; init; } = 200; + public string[] Animation { get; init; } = ["|", "/", "-", "\\"]; + + public static ConsoleSpinnerOptions Default { get; } = new(); +} + + + +internal sealed class ConsoleSpinner : IAsyncDisposable +{ + private readonly ConsoleSpinnerOptions _options; + private readonly CancellationTokenSource _cts = new(); + private readonly Task _observedTask; + private readonly bool _origCursorVisibility; + private readonly string[] _animation; + private int _frame; + private int _lastTextLength; + + private ConsoleSpinner(Task observedTask, ConsoleSpinnerOptions options) + { + _observedTask = observedTask; + _options = options; + _animation = options.Animation; + _origCursorVisibility = Console.CursorVisible; + + if (_options.HideCursor) + Console.CursorVisible = false; + + SpinnerLoop().Forget(); + } + + public static async Task Run(Task task, ConsoleSpinnerOptions? options = null) + { + options ??= ConsoleSpinnerOptions.Default; + await using var spinner = new ConsoleSpinner(task, options); + var result = await task.ConfigureAwait(false); + return result; + } + + public static async Task Run(Task task, ConsoleSpinnerOptions? options = null) + { + options ??= ConsoleSpinnerOptions.Default; + await using var spinner = new ConsoleSpinner(task, options); + await task.ConfigureAwait(false); + } + + public static Task Run(Func asyncAction, ConsoleSpinnerOptions? options = null) + { + if (asyncAction is null) + throw new ArgumentNullException(nameof(asyncAction)); + return Run(asyncAction(), options); + } + + public static Task Run(Func> asyncAction, ConsoleSpinnerOptions? options = null) + { + if (asyncAction is null) + throw new ArgumentNullException(nameof(asyncAction)); + return Run(asyncAction(), options); + } + + public static ConsoleSpinner Endless(ConsoleSpinnerOptions? options = null) + { + options ??= ConsoleSpinnerOptions.Default; + var tcs = new TaskCompletionSource(); + return new ConsoleSpinner(tcs.Task, options); + } + + private async Task SpinnerLoop() + { + try + { + while (!_cts.IsCancellationRequested && !_observedTask.IsCompleted) + { + await ShowFrameAsync(); + await Task.Delay(_options.Interval, _cts.Token); + } + } + catch (OperationCanceledException) + { + // Ignore + } + } + + private async Task ShowFrameAsync() + { + // Clear previous content if any + if (_lastTextLength > 0) + { + await ClearTextAsync(_lastTextLength); + } + + // Write new frame + var frameChar = _animation[_frame++ % _animation.Length]; + var text = string.IsNullOrEmpty(_options.RunningMessage) + ? frameChar + : $"{frameChar} {_options.RunningMessage}"; + + await _options.Writer.WriteAsync(text); + await _options.Writer.FlushAsync(); + _lastTextLength = text.Length; + } + + public async Task CleanupAndFinishAsync() + { + // Clear spinner content + if (_lastTextLength > 0) + { + await ClearTextAsync(_lastTextLength); + } + + // Show final message if needed + var finalMessage = GetFinalMessage(); + if (!string.IsNullOrEmpty(finalMessage)) + { + await _options.Writer.WriteLineAsync(finalMessage); + } + + await _options.Writer.FlushAsync(); + Console.CursorVisible = _origCursorVisibility; + } + + private async Task ClearTextAsync(int length) + { + // Use backspaces to go back, spaces to clear, backspaces to return to start + await _options.Writer.WriteAsync(new string('\b', length)); + await _options.Writer.WriteAsync(new string(' ', length)); + await _options.Writer.WriteAsync(new string('\b', length)); + } + + private string? GetFinalMessage() + { + return _observedTask.IsCompleted + ? _observedTask.IsFaulted || _observedTask.IsCanceled ? _options.FailedMessage : _options.CompletedMessage + : null; + } + + public async ValueTask DisposeAsync() + { + await CleanupAndFinishAsync(); + + await CastAndDispose(_cts); + await CastAndDispose(_observedTask); + + return; + + static async ValueTask CastAndDispose(IDisposable resource) + { + if (resource is IAsyncDisposable resourceAsyncDisposable) + await resourceAsyncDisposable.DisposeAsync(); + else + resource.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/ModVerify/ModVerify.csproj b/src/ModVerify/ModVerify.csproj index fecceb2..427458d 100644 --- a/src/ModVerify/ModVerify.csproj +++ b/src/ModVerify/ModVerify.csproj @@ -25,18 +25,25 @@ - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + + + + + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - diff --git a/src/ModVerify/Pipeline/GameVerifierPipelineStep.cs b/src/ModVerify/Pipeline/GameVerifierPipelineStep.cs index a27ee9b..6405dbd 100644 --- a/src/ModVerify/Pipeline/GameVerifierPipelineStep.cs +++ b/src/ModVerify/Pipeline/GameVerifierPipelineStep.cs @@ -26,13 +26,13 @@ protected override void RunCore(CancellationToken token) { try { - Logger?.LogDebug($"Running verifier '{GameVerifier.FriendlyName}'..."); + Logger?.LogDebug("Running verifier '{Name}'...", GameVerifier.FriendlyName); ReportProgress(new ProgressEventArgs(0.0, "Started")); GameVerifier.Progress += OnVerifyProgress; GameVerifier.Verify(token); - Logger?.LogDebug($"Finished verifier '{GameVerifier.FriendlyName}'"); + Logger?.LogDebug("Finished verifier '{Name}'", GameVerifier.FriendlyName); ReportProgress(new ProgressEventArgs(1.0, "Finished")); } finally diff --git a/src/ModVerify/Pipeline/GameVerifyPipeline.cs b/src/ModVerify/Pipeline/GameVerifyPipeline.cs index b7ac00d..810651a 100644 --- a/src/ModVerify/Pipeline/GameVerifyPipeline.cs +++ b/src/ModVerify/Pipeline/GameVerifyPipeline.cs @@ -103,6 +103,7 @@ protected override void OnError(object sender, StepRunnerErrorEventArgs e) { if (FailFast && e.Exception is GameVerificationException v) { + // TODO: Apply globalMinSeverity if (v.Errors.All(error => _reportSettings.Baseline.Contains(error) || _reportSettings.Suppressions.Suppresses(error))) return; } diff --git a/src/ModVerify/Reporting/IncompatibleBaselineException.cs b/src/ModVerify/Reporting/IncompatibleBaselineException.cs deleted file mode 100644 index c9a9eb1..0000000 --- a/src/ModVerify/Reporting/IncompatibleBaselineException.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace AET.ModVerify.Reporting; - -public sealed class IncompatibleBaselineException : Exception -{ - public override string Message => "The specified baseline is not compatible to this version of the application."; -} \ No newline at end of file diff --git a/src/ModVerify/Reporting/InvalidBaselineException.cs b/src/ModVerify/Reporting/InvalidBaselineException.cs new file mode 100644 index 0000000..37ab9c8 --- /dev/null +++ b/src/ModVerify/Reporting/InvalidBaselineException.cs @@ -0,0 +1,14 @@ +using System; + +namespace AET.ModVerify.Reporting; + +public sealed class InvalidBaselineException : Exception +{ + public InvalidBaselineException(string message) : base(message) + { + } + + public InvalidBaselineException(string? message, Exception? inner) : base(message, inner) + { + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonBaselineParser.cs b/src/ModVerify/Reporting/Json/JsonBaselineParser.cs new file mode 100644 index 0000000..ef2f5d1 --- /dev/null +++ b/src/ModVerify/Reporting/Json/JsonBaselineParser.cs @@ -0,0 +1,38 @@ +using System; +using System.IO; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace AET.ModVerify.Reporting.Json; + +public static class JsonBaselineParser +{ + public static VerificationBaseline Parse(Stream dataStream) + { + if (dataStream == null) + throw new ArgumentNullException(nameof(dataStream)); + try + { + var jsonNode = JsonNode.Parse(dataStream); + var jsonBaseline = ParseCore(jsonNode); + + if (jsonBaseline is null) + throw new InvalidBaselineException($"Unable to parse input from stream to {nameof(VerificationBaseline)}. Unknown Error!"); + + return new VerificationBaseline(jsonBaseline); + } + catch (JsonException cause) + { + throw new InvalidBaselineException(cause.Message, cause); + } + } + + private static JsonVerificationBaseline? ParseCore(JsonNode? jsonData) + { + if (jsonData is null) + return null; + + JsonBaselineSchema.Evaluate(jsonData); + return jsonData.Deserialize(); + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonBaselineSchema.cs b/src/ModVerify/Reporting/Json/JsonBaselineSchema.cs new file mode 100644 index 0000000..7c8b02a --- /dev/null +++ b/src/ModVerify/Reporting/Json/JsonBaselineSchema.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Text.Json.Nodes; +using Json.Schema; + +namespace AET.ModVerify.Reporting.Json; + +public static class JsonBaselineSchema +{ + private static readonly JsonSchema Schema; + private static readonly EvaluationOptions EvaluationOptions; + + static JsonBaselineSchema() + { + var evalvOptions = new EvaluationOptions + { + EvaluateAs = SpecVersion.Draft202012, + OutputFormat = OutputFormat.Hierarchical, + AllowReferencesIntoUnknownKeywords = false + }; + + Schema = GetCurrentSchema(); + EvaluationOptions = evalvOptions; + } + + /// + /// Evaluates a JSON node against the ModVerify Baseline JSON schema. + /// + /// The JSON node to evaluate. + /// is not valid against the baseline JSON schema. + /// is . + public static void Evaluate(JsonNode json) + { + if (json == null) + throw new ArgumentNullException(nameof(json)); + var result = Schema.Evaluate(json, EvaluationOptions); + ThrowOnValidationError(result); + } + + private static void ThrowOnValidationError(EvaluationResults result) + { + if (!result.IsValid) + { + var error = GetFirstError(result); + var errorMessage = "Baseline JSON not valid"; + + if (error is null) + errorMessage += ": Unknown Error"; + else + errorMessage += $": {error}"; + + throw new InvalidBaselineException(errorMessage); + } + } + + private static KeyValuePair? GetFirstError(EvaluationResults result) + { + if (result.HasErrors) + return result.Errors!.First(); + foreach (var child in result.Details) + { + var error = GetFirstError(child); + if (error is not null) + return error; + } + return null; + } + + private static JsonSchema GetCurrentSchema() + { + using var resourceStream = typeof(JsonBaselineSchema) + .Assembly.GetManifestResourceStream($"AET.ModVerify.Resources.Schemas.{GetVersionedPath()}.baseline.json"); + + Debug.Assert(resourceStream is not null); + var schema = JsonSchema.FromStream(resourceStream!).GetAwaiter().GetResult(); + + var id = schema.GetId(); + if (id is null || !UriContainsVersion(id, VerificationBaseline.LatestVersionString)) + throw new InvalidOperationException("Internal error: The embedded schema version does not match the expected baseline version!"); + + return schema; + } + + private static bool UriContainsVersion(Uri id, string latestVersionString) + { + foreach (var segment in id.Segments) + { + var trimmed = segment.AsSpan().TrimEnd('/'); + if (trimmed.Equals(latestVersionString, StringComparison.OrdinalIgnoreCase)) + return true; + } + return false; + } + + private static string GetVersionedPath() + { + var version = VerificationBaseline.LatestVersion; + var sb = new StringBuilder(); + + AddVersionSegment(version.Major, ref sb); + AddVersionSegment(version.Minor, ref sb); + AddVersionSegment(version.Build, ref sb); + AddVersionSegment(version.Revision, ref sb); + + // Remove the trailing dot + sb.Length -= 1; + + return sb.ToString(); + + static void AddVersionSegment(int segment, ref StringBuilder sb) + { + if (segment >= 0) + { + sb.Append('_'); + sb.Append(segment); + sb.Append("."); + } + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonVerificationError.cs b/src/ModVerify/Reporting/Json/JsonVerificationError.cs index ac80810..55f7b92 100644 --- a/src/ModVerify/Reporting/Json/JsonVerificationError.cs +++ b/src/ModVerify/Reporting/Json/JsonVerificationError.cs @@ -31,7 +31,7 @@ private JsonVerificationError( string message, VerificationSeverity severity, IEnumerable? contextEntries, - string asset) + string? asset) { Id = id; VerifierChain = verifierChain ?? []; diff --git a/src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs b/src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs index a2a00c5..cb6b258 100644 --- a/src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs +++ b/src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs @@ -14,6 +14,7 @@ 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) diff --git a/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs b/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs index 601e424..7de81eb 100644 --- a/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs +++ b/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs @@ -7,49 +7,46 @@ namespace AET.ModVerify.Reporting.Reporters; public static class VerificationReportersExtensions { - public static IServiceCollection RegisterJsonReporter(this IServiceCollection serviceCollection) + extension(IServiceCollection serviceCollection) { - return RegisterJsonReporter(serviceCollection, new JsonReporterSettings + public IServiceCollection RegisterJsonReporter() { - OutputDirectory = "." - }); - } + return RegisterJsonReporter(serviceCollection, new JsonReporterSettings + { + OutputDirectory = "." + }); + } - public static IServiceCollection RegisterTextFileReporter(this IServiceCollection serviceCollection) - { - return RegisterTextFileReporter(serviceCollection, new TextFileReporterSettings + public IServiceCollection RegisterTextFileReporter() { - OutputDirectory = "." - }); - } + return RegisterTextFileReporter(serviceCollection, new TextFileReporterSettings + { + OutputDirectory = "." + }); + } - public static IServiceCollection RegisterConsoleReporter(this IServiceCollection serviceCollection, bool summaryOnly = false) - { - return RegisterConsoleReporter(serviceCollection, new VerifyReportSettings + public IServiceCollection RegisterConsoleReporter(bool summaryOnly = false) { - MinimumReportSeverity = VerificationSeverity.Error - }, summaryOnly); - } + return RegisterConsoleReporter(serviceCollection, new VerifyReportSettings + { + MinimumReportSeverity = VerificationSeverity.Error + }, summaryOnly); + } - public static IServiceCollection RegisterJsonReporter( - this IServiceCollection serviceCollection, - JsonReporterSettings settings) - { - return serviceCollection.AddSingleton(sp => new JsonReporter(settings, sp)); - } + public IServiceCollection RegisterJsonReporter(JsonReporterSettings settings) + { + return serviceCollection.AddSingleton(sp => new JsonReporter(settings, sp)); + } - public static IServiceCollection RegisterTextFileReporter( - this IServiceCollection serviceCollection, - TextFileReporterSettings settings) - { - return serviceCollection.AddSingleton(sp => new TextFileReporter(settings, sp)); - } + public IServiceCollection RegisterTextFileReporter(TextFileReporterSettings settings) + { + return serviceCollection.AddSingleton(sp => new TextFileReporter(settings, sp)); + } - public static IServiceCollection RegisterConsoleReporter( - this IServiceCollection serviceCollection, - VerifyReportSettings settings, - bool summaryOnly = false) - { - return serviceCollection.AddSingleton(sp => new ConsoleReporter(settings, summaryOnly, sp)); + public IServiceCollection RegisterConsoleReporter(VerifyReportSettings settings, + bool summaryOnly = false) + { + return serviceCollection.AddSingleton(sp => new ConsoleReporter(settings, summaryOnly, sp)); + } } } \ No newline at end of file diff --git a/src/ModVerify/Reporting/VerificationBaseline.cs b/src/ModVerify/Reporting/VerificationBaseline.cs index 55a8973..c37539b 100644 --- a/src/ModVerify/Reporting/VerificationBaseline.cs +++ b/src/ModVerify/Reporting/VerificationBaseline.cs @@ -11,7 +11,8 @@ namespace AET.ModVerify.Reporting; public sealed class VerificationBaseline : IReadOnlyCollection { - private static readonly Version LatestVersion = new(2, 0); + public static readonly Version LatestVersion = new(2, 0); + public static readonly string LatestVersionString = LatestVersion.ToString(2); public static readonly VerificationBaseline Empty = new(VerificationSeverity.Information, []); @@ -60,14 +61,7 @@ public Task ToJsonAsync(Stream stream) public static VerificationBaseline FromJson(Stream stream) { - var baselineJson = JsonSerializer.Deserialize(stream, JsonSerializerOptions.Default); - if (baselineJson is null) - throw new InvalidOperationException("Unable to deserialize baseline."); - - if (baselineJson.Version is null || baselineJson.Version != LatestVersion) - throw new IncompatibleBaselineException(); - - return new VerificationBaseline(baselineJson); + return JsonBaselineParser.Parse(stream); } /// diff --git a/src/ModVerify/Resources/Schemas/2.0/baseline.json b/src/ModVerify/Resources/Schemas/2.0/baseline.json new file mode 100644 index 0000000..2520a58 --- /dev/null +++ b/src/ModVerify/Resources/Schemas/2.0/baseline.json @@ -0,0 +1,70 @@ +{ + "$id": "https://AlamoEngine-Tools.github.io/schemas/mod-verify/2.0/baseline", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Represents a baseline for AET ModVerify", + "type": "object", + "$defs": { + "severity": { + "enum": [ "Information", "Warning", "Error", "Critical" ] + }, + "error": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + }, + "asset": { + "type": "string" + }, + "severity": { + "$ref": "#/$defs/severity" + }, + "verifiers": { + "type": "array", + "items": { + "type": "string" + } + }, + "context": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "message", + "asset", + "severity", + "verifiers", + "context" + ], + "additionalProperties": false + } + }, + "properties": { + "version": { + "const": "2.0" + }, + "minSeverity": { + "$ref": "#/$defs/severity" + }, + "errors": { + "type": "array", + "items": { + "$ref": "#/$defs/error" + }, + "additionalItems": false + } + }, + "required": [ + "version", + "minSeverity", + "errors" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/src/ModVerify/Utilities/VerificationErrorExtensions.cs b/src/ModVerify/Utilities/VerificationErrorExtensions.cs index 1244b45..e5aa9fe 100644 --- a/src/ModVerify/Utilities/VerificationErrorExtensions.cs +++ b/src/ModVerify/Utilities/VerificationErrorExtensions.cs @@ -6,23 +6,24 @@ namespace AET.ModVerify.Utilities; public static class VerificationErrorExtensions { - public static IEnumerable ApplyBaseline(this IEnumerable errors, - VerificationBaseline baseline) + extension(IEnumerable errors) { - if (errors == null) - throw new ArgumentNullException(nameof(errors)); - if (baseline == null) - throw new ArgumentNullException(nameof(baseline)); - return baseline.Apply(errors); - } + public IEnumerable ApplyBaseline(VerificationBaseline baseline) + { + if (errors == null) + throw new ArgumentNullException(nameof(errors)); + if (baseline == null) + throw new ArgumentNullException(nameof(baseline)); + return baseline.Apply(errors); + } - public static IEnumerable ApplySuppressions(this IEnumerable errors, - SuppressionList suppressions) - { - if (errors == null) - throw new ArgumentNullException(nameof(errors)); - if (suppressions == null) - throw new ArgumentNullException(nameof(suppressions)); - return suppressions.Apply(errors); + public IEnumerable ApplySuppressions(SuppressionList suppressions) + { + if (errors == null) + throw new ArgumentNullException(nameof(errors)); + if (suppressions == null) + throw new ArgumentNullException(nameof(suppressions)); + return suppressions.Apply(errors); + } } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameLocations.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameLocations.cs index 18539a7..3b77732 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameLocations.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameLocations.cs @@ -7,6 +7,11 @@ namespace PG.StarWarsGame.Engine; public sealed class GameLocations { + /// + /// Gets the path that represents the topmost playable target. This is typically the actual mod selected by the user. + /// + public string TargetPath { get; } + public IReadOnlyList ModPaths { get; } public string GamePath { get; } @@ -38,5 +43,9 @@ public GameLocations(IList modPaths, string gamePath, IList fall ModPaths = modPaths.ToList(); GamePath = gamePath; FallbackPaths = fallbackPaths.ToList(); + + TargetPath = ModPaths.Count > 0 + ? ModPaths[0] + : GamePath; } } \ 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 7fa4d02..130fb50 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameManagerBase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameManagerBase.cs @@ -62,7 +62,7 @@ public async Task InitializeAsync(CancellationToken token) } catch (Exception e) { - Logger?.LogError(e, $"Initialization of {this} failed: {e.Message}"); + Logger?.LogError(e, "Initialization of {Class} failed: {Message}", this, e.Message); throw; } OnInitialized(); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager.cs index 82ce825..879c16f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager.cs @@ -79,7 +79,7 @@ public IReadOnlyDictionary GetTextureEn { if (!_perComponentTextures.TryGetValue(component, out var textures)) { - Logger?.LogDebug($"The component '{component}' has no overrides. Using default textures."); + Logger?.LogDebug("The component '{Component}' has no overrides. Using default textures.", component); componentExist = false; return DefaultTextureEntries; } @@ -92,7 +92,7 @@ public bool TryGetTextureEntry(string component, GuiComponentType key, out Compo { if (!_perComponentTextures.TryGetValue(component, out var textures)) { - Logger?.LogDebug($"The component '{component}' has no overrides. Using default textures."); + Logger?.LogDebug("The component '{Component}' has no overrides. Using default textures.", component); textures = _defaultTextures; } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs index 1304a89..e952db8 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs @@ -84,7 +84,7 @@ protected FileFoundInfo GetFileInfoFromMasterMeg(ReadOnlySpan filePath) if (filePath.Length > PGConstants.MaxMegEntryPathLength) { - Logger.LogWarning($"Trying to open a MEG entry which is longer than 259 characters: '{filePath.ToString()}'"); + Logger.LogWarning("Trying to open a MEG entry which is longer than 259 characters: '{FilePath}'", filePath.ToString()); return default; } @@ -97,7 +97,7 @@ protected FileFoundInfo GetFileInfoFromMasterMeg(ReadOnlySpan filePath) if (fileName.Length > PGConstants.MaxMegEntryPathLength) { - Logger.LogWarning($"Trying to open a MEG entry which is longer than 259 characters after normalization: '{fileName.ToString()}'"); + Logger.LogWarning("Trying to open a MEG entry which is longer than 259 characters after normalization: '{FileName}'", fileName.ToString()); return default; } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.cs index 42bfa54..d1451b0 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.cs @@ -116,9 +116,9 @@ public void AddMegFile(string megFile) if (megArchive is null) { if (IsSpeechMeg(megFile)) - Logger.LogDebug($"Unable to find Speech MEG file at '{megFile}'"); + Logger.LogDebug("Unable to find Speech MEG file at '{MegFile}'", megFile); else - Logger.LogWarning($"Unable to find MEG file at '{megFile}'"); + Logger.LogWarning("Unable to find MEG file at '{MegFile}'", megFile); return; } @@ -217,7 +217,7 @@ protected IList LoadMegArchivesFromXml(string lookupPath) if (xmlStream is null) { - Logger.LogWarning($"Unable to find MegaFiles.xml at '{lookupPath}'"); + Logger.LogWarning("Unable to find MegaFiles.xml at '{LookupPath}'", lookupPath); return Array.Empty(); } @@ -251,12 +251,12 @@ internal void Seal() if (megFileStream is not FileSystemStream fileSystemStream) { if (IsSpeechMeg(megPath)) - Logger.LogDebug($"Unable to find Speech MEG file '{megPath}'"); + Logger.LogDebug("Unable to find Speech MEG file '{MegPath}'", megPath); else { var message = $"Unable to find MEG file '{megPath}'"; _errorReporter.Assert(EngineAssert.Create(EngineAssertKind.FileNotFound, megPath, [], message)); - Logger.LogWarning($"Unable to find MEG file '{megPath}'"); + Logger.LogWarning("Unable to find MEG file '{MegPath}'", megPath); } return null; } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj index 6055649..e54e6f0 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj @@ -1,6 +1,6 @@  - netstandard2.0;netstandard2.1;net9.0 + netstandard2.0;netstandard2.1;net10.0 PG.StarWarsGame.Engine PG.StarWarsGame.Engine AlamoEngineTools.PG.StarWarsGame.Engine @@ -23,22 +23,16 @@ - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs index d2acfba..8ce1a5a 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs @@ -65,7 +65,7 @@ private async Task InitializeEngine( { try { - _logger?.LogInformation($"Initializing game engine for type '{engineType}'."); + _logger?.LogInformation("Initializing game engine for type '{GameEngineType}'.", engineType); var repoFactory = _serviceProvider.GetRequiredService(); var repository = repoFactory.Create(engineType, gameLocations, errorReporter); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs index 5d5f98a..0c911e2 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs @@ -37,13 +37,13 @@ public void ParseEntriesFromFileListXml( ValueListDictionary entries, Action? onFileParseAction = null) where T : notnull { - Logger.LogDebug($"Parsing container data '{xmlFile}'"); + Logger.LogDebug("Parsing container data '{XmlFile}'", xmlFile); using var containerStream = gameRepository.TryOpenFile(xmlFile); if (containerStream == null) { _reporter?.Report(this, XmlParseErrorEventArgs.FromMissingFile(xmlFile)); - Logger.LogWarning($"Could not find XML file '{xmlFile}'"); + Logger.LogWarning("Could not find XML file '{XmlFile}'", xmlFile); var args = new XmlContainerParserErrorEventArgs(xmlFile, null, true) { @@ -89,7 +89,7 @@ public void ParseEntriesFromFileListXml( if (fileStream is null) { _reporter?.Report(parser, XmlParseErrorEventArgs.FromMissingFile(file)); - Logger.LogWarning($"Could not find XML file '{file}'"); + Logger.LogWarning("Could not find XML file '{File}'", file); var args = new XmlContainerParserErrorEventArgs(file); XmlParseError?.Invoke(this, args); @@ -99,7 +99,7 @@ public void ParseEntriesFromFileListXml( return; } - Logger.LogDebug($"Parsing File '{file}'"); + Logger.LogDebug("Parsing File '{File}'", file); try { 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 4fd3dd4..c653074 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 @@ -16,11 +16,7 @@ snupkg - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + all runtime; build; native; contentfiles; analyzers; buildtransitive 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 d6a7939..36221be 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 @@ -14,9 +14,10 @@ true snupkg + preview - + 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 066e32d..fbb055c 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 @@ -15,15 +15,16 @@ true snupkg true + preview - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/CommonTestBase.cs b/test/ModVerify.CliApp.Test/CommonTestBase.cs new file mode 100644 index 0000000..7d6dc18 --- /dev/null +++ b/test/ModVerify.CliApp.Test/CommonTestBase.cs @@ -0,0 +1,29 @@ +using System; +using System.IO.Abstractions; +using AnakinRaW.CommonUtilities.Hashing; +using Microsoft.Extensions.DependencyInjection; +using PG.Commons; +using Testably.Abstractions.Testing; + +namespace ModVerify.CliApp.Test; + +public abstract class CommonTestBase +{ + 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) + { + } +} \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/EmbeddedBaselineTest.cs b/test/ModVerify.CliApp.Test/EmbeddedBaselineTest.cs new file mode 100644 index 0000000..cde1dad --- /dev/null +++ b/test/ModVerify.CliApp.Test/EmbeddedBaselineTest.cs @@ -0,0 +1,47 @@ +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; + +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() + { + var sc = new ServiceCollection(); + sc.AddSingleton(FileSystem); + sc.AddSingleton(new ModVerifyAppEnvironment(typeof(ModVerifyAppEnvironment).Assembly, FileSystem)); + _serviceProvider = sc.BuildServiceProvider(); + } + + [Theory] + // [InlineData(GameEngineType.Eaw)] TODO EaW is currently not supported + [InlineData(GameEngineType.Foc)] + 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); + } +} \ 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 new file mode 100644 index 0000000..84d1bba --- /dev/null +++ b/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj @@ -0,0 +1,33 @@ + + + + net10.0 + $(TargetFrameworks);net481 + false + preview + + + + + 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 new file mode 100644 index 0000000..294baf7 --- /dev/null +++ b/test/ModVerify.CliApp.Test/ModVerifyOptionsParserTest.cs @@ -0,0 +1,210 @@ +using AET.ModVerify.App.Settings.CommandLine; +using AnakinRaW.ApplicationBase.Environment; +using System; +using System.IO.Abstractions; +using ModVerify.CliApp.Test.TestData; +using Testably.Abstractions; +using ModVerify.CliApp.Test.Utilities; + +namespace ModVerify.CliApp.Test; + +public class ModVerifyOptionsParserTest_Updateable : ModVerifyOptionsParserTestBase +{ + protected override bool IsUpdatable => true; + + protected override ApplicationEnvironment CreateEnvironment() + { + return new UpdatableEnv(GetType().Assembly, FileSystem); + } + + [Fact] + public void Parse_UpdateAppArg() + { + const string argString = "updateApplication --updateBranch test --updateManifestUrl https://examlple.com"; + + var settings = Parser.Parse(argString.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + + Assert.True(settings.HasOptions); + Assert.Null(settings.ModVerifyOptions); + Assert.NotNull(settings.UpdateOptions); + Assert.Equal("test", settings.UpdateOptions.BranchName); + Assert.Equal("https://examlple.com", settings.UpdateOptions.ManifestUrl); + } + + [Fact] + public void Parse_CombinedIsNotAllowed() + { + const string argString = "verify --updateBranch test --updateManifestUrl https://examlple.com"; + + var settings = Parser.Parse(argString.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + + Assert.False(settings.HasOptions); + Assert.Null(settings.ModVerifyOptions); + Assert.Null(settings.UpdateOptions); + } +} + +public class ModVerifyOptionsParserTest_NotUpdateable : ModVerifyOptionsParserTestBase +{ + protected override bool IsUpdatable => false; + + protected override ApplicationEnvironment CreateEnvironment() + { + return new TestEnv(GetType().Assembly, FileSystem); + } + + [Theory] + [InlineData("verify --externalUpdaterResult UpdateSuccess")] + [InlineData("createBaseline --externalUpdaterResult UpdateSuccess")] + [InlineData("verify --junkOption")] + [InlineData("createBaseline --junkOption")] + [InlineData("updateApplication")] + [InlineData("updateApplication --updateBranch test --updateManifestUrl https://examlple.com")] + public void Parse_InvalidArgs_NotUpdateable(string argString) + { + var settings = Parser.Parse(argString.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + + Assert.False(settings.HasOptions); + Assert.Null(settings.ModVerifyOptions); + Assert.Null(settings.UpdateOptions); + } +} + + +public abstract class ModVerifyOptionsParserTestBase +{ + private protected readonly ModVerifyOptionsParser Parser; + protected readonly IFileSystem FileSystem = new RealFileSystem(); + + protected abstract ApplicationEnvironment CreateEnvironment(); + + protected abstract bool IsUpdatable { get; } + + protected ModVerifyOptionsParserTestBase() + { + Parser = new ModVerifyOptionsParser(CreateEnvironment(), null); + } + + [Fact] + public void Parse_NoArgs_IsVerify_IsInteractive() + { + var settings = Parser.Parse([]); + + Assert.True(settings.HasOptions); + var verify = Assert.IsType(settings.ModVerifyOptions); + Assert.True(verify.IsRunningWithoutArguments); + Assert.Null(settings.UpdateOptions); + } + + [Theory] + [InlineData("verify", false)] + [InlineData("verify -v", false)] + [InlineData("createBaseline -o out.json", true)] + [InlineData("createBaseline -v -o out.json", true)] + public void Parse_Interactive(string argString, bool createBaseLine) + { + var settings = Parser.Parse(argString.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + + Assert.True(settings.HasOptions); + if (createBaseLine) + { + Assert.IsType(settings.ModVerifyOptions); + } + else + { + var verify = Assert.IsType(settings.ModVerifyOptions); + Assert.False(verify.IsRunningWithoutArguments); + } + Assert.Null(settings.UpdateOptions); + } + + [Theory] + [InlineData("verify --path myMod", false)] + [InlineData("verify -v --game myGame", false)] + [InlineData("createBaseline -o out.json --path myMod", true)] + [InlineData("createBaseline -v -o out.json --game myGame", true)] + public void Parse_NotInteractive(string argString, bool createBaseLine) + { + var settings = Parser.Parse(argString.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + + Assert.True(settings.HasOptions); + Assert.NotNull(settings.ModVerifyOptions); + + if (createBaseLine) + Assert.IsType(settings.ModVerifyOptions); + else + { + var verify = Assert.IsType(settings.ModVerifyOptions); + Assert.False(verify.IsRunningWithoutArguments); + } + + Assert.Null(settings.UpdateOptions); + } + + [Theory] + [InlineData("verify --path myMod --game myGame")] + [InlineData("verify --game myMod --path myMod")] + [InlineData("verify --mod myMod --path myMod")] + [InlineData("verify --fallbackGame myGame --path myMod")] + public void Parse_InvalidPathConfig(string argString) + { + var settings = Parser.Parse(argString.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + + Assert.False(settings.HasOptions); + Assert.Null(settings.ModVerifyOptions); + Assert.Null(settings.UpdateOptions); + } + + [Theory] + [InlineData("")] + [InlineData("junkVerb")] + [InlineData("junkVerb verify")] + [InlineData("junkVerb verify --v")] + [InlineData("junkVerb --v")] + [InlineData("verify --junkOption")] + [InlineData("verify -v --junkOption")] + [InlineData("updateApplication --junkOption")] + [InlineData("--junkOption")] + [InlineData("junkVerb --junkOption")] + [InlineData("junkVerb --externalUpdaterResult UpdateSuccess")] + [InlineData("-v")] + public void Parse_InvalidArgs(string argString) + { + var settings = Parser.Parse(argString.Split(' ')); + + Assert.False(settings.HasOptions); + Assert.Null(settings.ModVerifyOptions); + Assert.Null(settings.UpdateOptions); + } + + [Fact] + public void Parse_UpdatePerformed_RestartedFromNoArgs() + { + // This only happens when we run without args, performed an auto-update and restarted the application automatically. + const string argString = "--externalUpdaterResult UpdateSuccess"; + + var settings = Parser.Parse(argString.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + + if (!IsUpdatable) + Assert.False(settings.HasOptions); + else + { + Assert.True(settings.HasOptions); + var verify = Assert.IsType(settings.ModVerifyOptions); + Assert.True(verify.IsRunningWithoutArguments); + Assert.Null(settings.UpdateOptions); + } + } + + [Theory] + [InlineData("createBaseline")] + [InlineData("createBaseline -v")] + public void Parse_CreateBaseline_MissingRequired_Fails(string argString) + { + var settings = Parser.Parse(argString.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + + Assert.False(settings.HasOptions); + Assert.Null(settings.ModVerifyOptions); + Assert.Null(settings.UpdateOptions); + } +} \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/TestData/NoVerifierProvider.cs b/test/ModVerify.CliApp.Test/TestData/NoVerifierProvider.cs new file mode 100644 index 0000000..ef7ad74 --- /dev/null +++ b/test/ModVerify.CliApp.Test/TestData/NoVerifierProvider.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using AET.ModVerify.Pipeline; +using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers; +using PG.StarWarsGame.Engine; + +namespace ModVerify.CliApp.Test.TestData; + +internal sealed class NoVerifierProvider : IGameVerifiersProvider +{ + public IEnumerable GetVerifiers(IStarWarsGameEngine database, GameVerifySettings settings, IServiceProvider serviceProvider) + { + yield break; + } +} \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/TestData/TestEnv.cs b/test/ModVerify.CliApp.Test/TestData/TestEnv.cs new file mode 100644 index 0000000..f72f001 --- /dev/null +++ b/test/ModVerify.CliApp.Test/TestData/TestEnv.cs @@ -0,0 +1,11 @@ +using System.IO.Abstractions; +using System.Reflection; +using AnakinRaW.ApplicationBase.Environment; + +namespace ModVerify.CliApp.Test.TestData; + +internal class TestEnv(Assembly assembly, IFileSystem fileSystem) : ApplicationEnvironment(assembly, fileSystem) +{ + public override string ApplicationName => "TestEnv"; + protected override string ApplicationLocalDirectoryName => ApplicationName; +} \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/TestData/UpdatableEnv.cs b/test/ModVerify.CliApp.Test/TestData/UpdatableEnv.cs new file mode 100644 index 0000000..557f522 --- /dev/null +++ b/test/ModVerify.CliApp.Test/TestData/UpdatableEnv.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Reflection; +using AnakinRaW.ApplicationBase.Environment; +using AnakinRaW.AppUpdaterFramework.Configuration; + +namespace ModVerify.CliApp.Test.TestData; + +internal class UpdatableEnv(Assembly assembly, IFileSystem fileSystem) : UpdatableApplicationEnvironment(assembly, fileSystem) +{ + public override string ApplicationName => "TestUpdateEnv"; + protected override string ApplicationLocalDirectoryName => ApplicationName; + public override ICollection UpdateMirrors => []; + public override string UpdateRegistryPath => ApplicationName; + protected override UpdateConfiguration CreateUpdateConfiguration() + { + return UpdateConfiguration.Default; + } +} \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/Utilities/StringExtensions.cs b/test/ModVerify.CliApp.Test/Utilities/StringExtensions.cs new file mode 100644 index 0000000..052c322 --- /dev/null +++ b/test/ModVerify.CliApp.Test/Utilities/StringExtensions.cs @@ -0,0 +1,13 @@ +using System; + +namespace ModVerify.CliApp.Test.Utilities; + +internal static class StringExtensions +{ +#if NETFRAMEWORK + public static string[] Split(this string str, char separator, StringSplitOptions options) + { + return str.Split([separator], options); + } +#endif +} \ No newline at end of file diff --git a/version.json b/version.json index 251e6e2..3e87545 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.0-alpha", + "version": "0.1-beta", "publicReleaseRefSpec": [ "^refs/heads/main$" ],