diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b43ddf6..8c2061c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,7 +8,7 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "daily" + interval: "weekly" groups: actions-deps: patterns: @@ -19,7 +19,7 @@ updates: - package-ecosystem: "nuget" directory: "/" schedule: - interval: "daily" + interval: "weekly" target-branch: "develop" open-pull-requests-limit: 1 groups: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7ff6b7d..be274ae 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@v4 + 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@v6 with: name: Binary Releases path: ./releases @@ -62,17 +64,45 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: actions/download-artifact@v4 + submodules: recursive + - uses: actions/download-artifact@v7 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 f6c4d43..aca43b4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,12 +19,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 submodules: recursive - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@v5 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 + run: dotnet test --configuration Release --report-github \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8a30d25..7d20657 100644 --- a/.gitignore +++ b/.gitignore @@ -396,3 +396,6 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml +.idea + +.local_deploy \ No newline at end of file diff --git a/.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..5110cdb 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -12,7 +12,7 @@ ModVerify Alamo Engine Tools and Contributors - Copyright © 2025 Alamo Engine Tools and contributors. All rights reserved. + Copyright © 2026 Alamo Engine Tools and contributors. All rights reserved. https://github.com/AlamoEngine-Tools/ModVerify $(RepoRootPath)LICENSE MIT @@ -24,7 +24,7 @@ - latest + preview disable enable True @@ -33,13 +33,13 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all all - 3.7.115 + 3.9.50 diff --git a/LICENSE b/LICENSE index 7159126..23fe869 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Alamo Engine Tools +Copyright (c) 2026 Alamo Engine Tools Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/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/README.md b/README.md index 70b4f91..d3aa24c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ # ModVerify: A Mod Verification Tool - ModVerify is a command-line tool designed to analyze mods for the game Star Wars: Empire at War and its expansion Forces of Corruption for common errors in XML and other game files. @@ -8,6 +7,7 @@ for common errors in XML and other game files. - [Installation](#installation) - [Usage](#usage) +- [Getting Updates](#getting-updates) - [Options](#options) - [Available Checks](#available-checks) - [Creating a new Baseline](#creating-a-new-baseline) @@ -18,12 +18,15 @@ for common errors in XML and other game files. Download the latest release from the [releases page](https://github.com/AlamoEngine-Tools/ModVerify/releases). There are two versions of the application available. -1. `ModVerify.exe` is the default version. Use this if you simply want to verify your mods. This version only works on Windows. -2. `ModVerify-NetX.zip` is the cross-platform app. It works on Windows and Linux and is most likely the version you want to use to include it in some CI/CD scenarios. +1. `ModVerify.exe` (**recommended**): Use this if you simply want to verify your mods. This version only works on Windows. +2. `ModVerify-NetX.zip`: Cross-platform app. It works on Windows and Linux and is most likely the version you want to use to include it in some CI/CD scenarios. You can place the files anywhere on your system, eg. your Desktop. There is no need to place it inside a mod's directory. -***Note**: Both versions have the exact same feature set. They just target a different .NET runtime. Linux and CI/CD support is not fully tested yet. Current priority is on the Windows-only version.* +***Note**: Only the Windows version supports automatic updates. +Except for that both versions have the exact same feature set. +They just target a different .NET runtime. Linux and CI/CD support is not fully tested yet. +Current priority is on the Windows-only version.* --- @@ -31,38 +34,57 @@ You can place the files anywhere on your system, eg. your Desktop. There is no n Simply run the executable file `ModVerify.exe`. -When given no specific argument through the command line, ModVerify will ask you which game or mod you want to verify. When ModVerify is done analyzing, it will write the verification results into new files next to the executable. +When given no specific argument through the command line, ModVerify will ask you which game or mod you want to verify. +When ModVerify is done analyzing, it will write the verification results into a folder `ModVerifyResults` next to the executable. + +![ModVerify Image](./docs/ModVerify.png "ModVerify") A `.JSON` file contains all identified issues. The additional `.txt` files contain the same errors but are grouped by the verifier that reported the issue. -The text files may be easier to read, while the JSON file is more useful for 3rd party tool processing. +The text files may be easier to read, while the JSON file is more useful for 3rd party tool processing. -## Options +*Vanilla Empire at War is currently not supported.* + +--- -You can also run the tool with command line arguments to adjust the tool to your needs. +## Getting Updates -To see all available options, especially if you have custom folder setups, open the command line and type: +ModVerify automatically check for updates. + +*The following applies to the Windows (.NET Framework) version only* + +When executed with no arguments the application automatically checks for an available update and will ask you whether you want to update now. +Otherwise, the app only informs you whether an update is available. + +You can use the dedicated `updateApplication` option to trigger an update. ```bash -ModVerify.exe --help +.\ModVerify.exe updateApplication ``` -Here is a list of the most relevant options: +***Note:*** +You may be required to put ModVerify on your AntiVirus whitelist. + +--- + +## Options -### `--path` -Specifies a path that shall be analyzed. **There will be no user input required when using this option** +You can also run the tool with command line arguments to specify custom behavior. -### `--output` -Specified the output path where analysis result shall be written to. +To see all available options, especially if you have custom folder setups, open the command line and type: -### `--baseline` -Specifies a baseline file that shall be used to filter out known errors. You can download the [FoC baseline](focBaseline.json) which includes all errors produced by the vanilla game. +```bash +.\ModVerify.exe --help +``` +In general ModVerify has two operation mods. +1. `verify` Verifying a game or mod +2. `createBaseline` Creating a baseline for a game or mod, that can be used for further verifications in order to verify you did not add more errors to your mods. ### Example This is an example run configuration that analyzes a specific mod, uses a the FoC basline and writes the output into a dedicated directory: ```bash -ModVerify.exe --path "C:\My Games\FoC\Mods\MyMod" --output "C:\My Games\FoC\Mods\MyMod\verifyResults" --baseline focBaseline.json +.\ModVerify.exe verify --path "C:\My Games\FoC\Mods\MyMod" --outDir "C:\My Games\FoC\Mods\MyMod\verifyResults" --baseline ./focBaseline.json ``` --- @@ -80,12 +102,12 @@ The following verifiers are currently implemented: - Checks the referenced textures exist ### GameObjects -- Checks the referenced models for validity (textures, particles and shaders) +- Checks the referenced models for validity (ALO file, textures, particles and shaders) - Duplicates ### Engine - Performs assertion checks the Debug builds of the game are also doing (not complete) -- Sports XML errors and unexpected values +- XML errors and unexpected values --- @@ -95,5 +117,5 @@ If you want to create your own baseline use the `createBaseline` option. ### Example ```bash -ModVerify.exe createBaseline -o myBaseline.json --path "C:\My Games\FoC\Mods\MyMod" +ModVerify.exe createBaseline --outFile myBaseline.json --path "C:\My Games\FoC\Mods\MyMod" ``` diff --git a/deploy-local.ps1 b/deploy-local.ps1 new file mode 100644 index 0000000..740c579 --- /dev/null +++ b/deploy-local.ps1 @@ -0,0 +1,75 @@ +# Local deployment script for ModVerify to test the update feature. +# This script builds the application, creates an update manifest, and "deploys" it to a local directory. + +$ErrorActionPreference = "Stop" + +$root = $PSScriptRoot +if ([string]::IsNullOrEmpty($root)) { $root = Get-Location } + +$deployRoot = Join-Path $root ".local_deploy" +$stagingDir = Join-Path $deployRoot "staging" +$serverDir = Join-Path $deployRoot "server" +$installDir = Join-Path $deployRoot "install" + +$toolProj = Join-Path $root "src\ModVerify.CliApp\ModVerify.CliApp.csproj" +$creatorProj = Join-Path $root "modules\ModdingToolBase\src\AnakinApps\ApplicationManifestCreator\ApplicationManifestCreator.csproj" +$uploaderProj = Join-Path $root "modules\ModdingToolBase\src\AnakinApps\FtpUploader\FtpUploader.csproj" + +$toolExe = "ModVerify.exe" +$updaterExe = "AnakinRaW.ExternalUpdater.exe" +$manifestCreatorDll = "AnakinRaW.ApplicationManifestCreator.dll" +$uploaderDll = "AnakinRaW.FtpUploader.dll" + +# 1. Clean and Create directories +if (Test-Path $deployRoot) { Remove-Item -Recurse -Force $deployRoot } +New-Item -ItemType Directory -Path $stagingDir | Out-Null +New-Item -ItemType Directory -Path $serverDir | Out-Null +New-Item -ItemType Directory -Path $installDir | Out-Null + +Write-Host "--- Building ModVerify (net481) ---" -ForegroundColor Cyan +dotnet build $toolProj --configuration Release -f net481 --output "$deployRoot\bin\tool" /p:DebugType=None /p:DebugSymbols=false + +Write-Host "--- Building Manifest Creator ---" -ForegroundColor Cyan +dotnet build $creatorProj --configuration Release --output "$deployRoot\bin\creator" + +Write-Host "--- Building Local Uploader ---" -ForegroundColor Cyan +dotnet build $uploaderProj --configuration Release --output "$deployRoot\bin\uploader" + +# 2. Prepare staging +Write-Host "--- Preparing Staging ---" -ForegroundColor Cyan +Copy-Item "$deployRoot\bin\tool\$toolExe" $stagingDir +Copy-Item "$deployRoot\bin\tool\$updaterExe" $stagingDir + +# 3. Create Manifest +# Origin must be an absolute URI for the manifest creator. +# Using 127.0.0.1 and file:// is tricky with Flurl/DownloadManager sometimes. +# We'll use the local path and ensure it's formatted correctly. +$serverPath = (Resolve-Path $serverDir).Path +$serverUri = "file:///$($serverPath.Replace('\', '/'))" +# If we have 3 slashes, Flurl/DownloadManager might still fail on Windows if it expects a certain format. +# However, the ManifestCreator just needs a valid URI for the 'Origin' field in the manifest. +Write-Host "--- Creating Manifest (Origin: $serverUri) ---" -ForegroundColor Cyan +dotnet "$deployRoot\bin\creator\$manifestCreatorDll" ` + -a "$stagingDir\$toolExe" ` + --appDataFiles "$stagingDir\$updaterExe" ` + --origin "$serverUri" ` + -o "$stagingDir" ` + -b "beta" + +# 4. "Deploy" to server using the local uploader +Write-Host "--- Deploying to Local Server ---" -ForegroundColor Cyan +dotnet "$deployRoot\bin\uploader\$uploaderDll" local --base "$serverDir" --source "$stagingDir" + +# 5. Setup a "test" installation +Write-Host "--- Setting up Test Installation ---" -ForegroundColor Cyan +Copy-Item "$deployRoot\bin\tool\*" $installDir -Recurse + +Write-Host "`nLocal deployment complete!" -ForegroundColor Green +Write-Host "Server directory: $serverDir" +Write-Host "Install directory: $installDir" +Write-Host "`nTo test the update:" +Write-Host "1. (Optional) Modify the version in version.json and run this script again to 'push' a new version to the local server." +Write-Host "2. Run ModVerify from the install directory with the following command:" +Write-Host " cd '$installDir'" +Write-Host " .\ModVerify.exe updateApplication --updateManifestUrl '$serverUri'" +Write-Host "`n Note: You can also specify a different branch using --updateBranch if needed." diff --git a/docs/ModVerify.png b/docs/ModVerify.png new file mode 100644 index 0000000..babd53c Binary files /dev/null and b/docs/ModVerify.png differ diff --git a/global.json b/global.json new file mode 100644 index 0000000..802ab21 --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "test": { + "runner": "Microsoft.Testing.Platform" + } +} \ No newline at end of file diff --git a/modules/ModdingToolBase b/modules/ModdingToolBase new file mode 160000 index 0000000..5103bad --- /dev/null +++ b/modules/ModdingToolBase @@ -0,0 +1 @@ +Subproject commit 5103bad6f09ba88061ccbc36ee285ee9300744cc diff --git a/src/ModVerify.CliApp/App/CreateBaselineAction.cs b/src/ModVerify.CliApp/App/CreateBaselineAction.cs new file mode 100644 index 0000000..b0eb880 --- /dev/null +++ b/src/ModVerify.CliApp/App/CreateBaselineAction.cs @@ -0,0 +1,56 @@ +using AET.ModVerify.App.Reporting; +using AET.ModVerify.App.Settings; +using AET.ModVerify.App.Utilities; +using AET.ModVerify.Reporting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Threading.Tasks; + +namespace AET.ModVerify.App; + +internal sealed class CreateBaselineAction(AppBaselineSettings settings, IServiceProvider serviceProvider) + : ModVerifyApplicationAction(settings, serviceProvider) +{ + private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); + + protected override void PrintAction(VerificationTarget target) + { + Console.WriteLine(); + Console.ForegroundColor = ConsoleColor.DarkGreen; + Console.WriteLine($"Creating baseline for {target.Name}..."); + Console.WriteLine(); + ModVerifyConsoleUtilities.WriteSelectedTarget(target); + Console.WriteLine(); + } + + protected override async Task ProcessVerifyFindings( + VerificationTarget verificationTarget, + IReadOnlyCollection allErrors) + { + var baselineFactory = ServiceProvider.GetRequiredService(); + var baseline = baselineFactory.CreateBaseline(verificationTarget, Settings, allErrors); + + var fullPath = _fileSystem.Path.GetFullPath(Settings.NewBaselinePath); + Logger?.LogInformation(ModVerifyConstants.ConsoleEventId, + "Writing Baseline to '{FullPath}' with {Number} findings", fullPath, allErrors.Count); + + await baselineFactory.WriteBaselineAsync(baseline, Settings.NewBaselinePath); + + Logger?.LogDebug("Baseline successfully created."); + + Console.WriteLine(); + Console.ForegroundColor = ConsoleColor.DarkGreen; + Console.WriteLine($"Baseline for {verificationTarget.Name} created."); + Console.ResetColor(); + + return ModVerifyConstants.Success; + } + + protected override VerificationBaseline GetBaseline(VerificationTarget verificationTarget) + { + return VerificationBaseline.Empty; + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/App/IModVerifyAppAction.cs b/src/ModVerify.CliApp/App/IModVerifyAppAction.cs new file mode 100644 index 0000000..1a75b84 --- /dev/null +++ b/src/ModVerify.CliApp/App/IModVerifyAppAction.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace AET.ModVerify.App; + +internal interface IModVerifyAppAction +{ + Task ExecuteAsync(); +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/App/ModVerifyApplication.cs b/src/ModVerify.CliApp/App/ModVerifyApplication.cs new file mode 100644 index 0000000..eae7851 --- /dev/null +++ b/src/ModVerify.CliApp/App/ModVerifyApplication.cs @@ -0,0 +1,68 @@ +using AET.ModVerify.App.Settings; +using AnakinRaW.ApplicationBase; +using AnakinRaW.ApplicationBase.Utilities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Serilog; +using System; +using System.Threading.Tasks; +using ILogger = Microsoft.Extensions.Logging.ILogger; + +namespace AET.ModVerify.App; + +internal sealed class ModVerifyApplication(AppSettingsBase settings, IServiceProvider services) +{ + private readonly ILogger? _logger = services.GetService()?.CreateLogger(typeof(ModVerifyApplication)); + + public async Task RunAsync() + { + using (new UnhandledExceptionHandler(services)) + using (new UnobservedTaskExceptionHandler(services)) + return await RunCoreAsync().ConfigureAwait(false); + } + + private async Task RunCoreAsync() + { + _logger?.LogDebug("Raw command line: {CommandLine}", Environment.CommandLine); + + try + { + var action = CreateAppAction(); + return await action.ExecuteAsync().ConfigureAwait(false); + } + catch (Exception e) + { + _logger?.LogCritical(e, e.Message); + ConsoleUtilities.WriteApplicationFatalError(ModVerifyConstants.AppNameString, e); + return e.HResult; + } + finally + { +#if NET + await Log.CloseAndFlushAsync(); +#else + Log.CloseAndFlush(); +#endif + if (settings is AppVerifySettings { IsInteractive: true }) + { + Console.WriteLine(); + ConsoleUtilities.WriteHorizontalLine('-'); + Console.WriteLine("Press any key to exit"); + Console.ReadLine(); + } + } + } + + private IModVerifyAppAction CreateAppAction() + { + switch (settings) + { + case AppVerifySettings verifySettings: + return new VerifyAction(verifySettings, services); + case AppBaselineSettings baselineSettings: + return new CreateBaselineAction(baselineSettings, services); + default: + throw new InvalidOperationException("Unknown settings"); + } + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/App/ModVerifyApplicationAction.cs b/src/ModVerify.CliApp/App/ModVerifyApplicationAction.cs new file mode 100644 index 0000000..b3f12f7 --- /dev/null +++ b/src/ModVerify.CliApp/App/ModVerifyApplicationAction.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Threading.Tasks; +using AET.ModVerify.App.GameFinder; +using AET.ModVerify.App.Reporting; +using AET.ModVerify.App.Settings; +using AET.ModVerify.App.TargetSelectors; +using AET.ModVerify.Pipeline; +using AET.ModVerify.Reporting; +using AnakinRaW.ApplicationBase; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace AET.ModVerify.App; + +internal abstract class ModVerifyApplicationAction : IModVerifyAppAction where T : AppSettingsBase +{ + private readonly ModVerifyAppEnvironment _appEnvironment; + private readonly IFileSystem _fileSystem; + + protected T Settings { get; } + + protected IServiceProvider ServiceProvider { get; } + + protected ILogger? Logger { get; } + + protected ModVerifyApplicationAction(T settings, IServiceProvider serviceProvider) + { + Settings = settings ?? throw new ArgumentNullException(nameof(settings)); + ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + Logger = serviceProvider.GetService()?.CreateLogger(GetType()); + _appEnvironment = ServiceProvider.GetRequiredService(); + _fileSystem = ServiceProvider.GetRequiredService(); + } + + protected virtual void PrintAction(VerificationTarget target) + { + } + + public async Task ExecuteAsync() + { + VerificationTarget verificationTarget; + try + { + var targetSettings = Settings.VerificationTargetSettings; + verificationTarget = new VerificationTargetSelectorFactory(ServiceProvider) + .CreateSelector(targetSettings) + .Select(targetSettings); + } + catch (ArgumentException ex) + { + ConsoleUtilities.WriteApplicationFatalError(_appEnvironment.ApplicationName, + $"The specified arguments are not correct: {ex.Message}"); + Logger?.LogError(ex, "Invalid application arguments: {Message}", ex.Message); + return ex.HResult; + } + catch (TargetNotFoundException ex) + { + ConsoleUtilities.WriteApplicationFatalError(_appEnvironment.ApplicationName, ex.Message); + Logger?.LogError(ex, ex.Message); + return ex.HResult; + } + catch (GameNotFoundException ex) + { + ConsoleUtilities.WriteApplicationFatalError(_appEnvironment.ApplicationName, + "Unable to find an installation of Empire at War or Forces of Corruption."); + Logger?.LogError(ex, "Game not found: {Message}", ex.Message); + return ex.HResult; + } + + PrintAction(verificationTarget); + + var allErrors = await VerifyTargetAsync(verificationTarget) + .ConfigureAwait(false); + + return await ProcessVerifyFindings(verificationTarget, allErrors); + } + + protected abstract Task ProcessVerifyFindings( + VerificationTarget verificationTarget, + IReadOnlyCollection allErrors); + + protected abstract VerificationBaseline GetBaseline(VerificationTarget verificationTarget); + + private async Task> VerifyTargetAsync(VerificationTarget verificationTarget) + { + var progressReporter = new VerifyConsoleProgressReporter(verificationTarget.Name, Settings.ReportSettings); + + var baseline = GetBaseline(verificationTarget); + var suppressions = GetSuppressions(); + + using var verifyPipeline = new GameVerifyPipeline( + verificationTarget, + Settings.VerifyPipelineSettings, + progressReporter, + new EngineInitializeProgressReporter(verificationTarget.Engine), + baseline, + suppressions, + ServiceProvider); + + try + { + Logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Verifying '{Target}'...", verificationTarget.Name); + await verifyPipeline.RunAsync().ConfigureAwait(false); + progressReporter.Report(string.Empty, 1.0); + } + catch (OperationCanceledException) + { + Logger?.LogWarning(ModVerifyConstants.ConsoleEventId, "Verification stopped due to enabled failFast setting."); + } + catch (Exception e) + { + progressReporter.ReportError("Verification failed!", e.Message); + Logger?.LogError(e, "Verification failed: {Message}", e.Message); + throw; + } + finally + { + progressReporter.Dispose(); + } + + Logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Finished verification"); + return verifyPipeline.FilteredErrors; + } + + private SuppressionList GetSuppressions() + { + var suppressionsFile = Settings.ReportSettings.SuppressionsPath; + SuppressionList suppressions; + if (string.IsNullOrEmpty(suppressionsFile)) + suppressions = SuppressionList.Empty; + else + { + using var fileStream = _fileSystem.File.OpenRead(suppressionsFile!); + suppressions = SuppressionList.FromJson(fileStream); + if (suppressions.Count > 0) + Logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Using suppressions from '{Suppressions}'", suppressionsFile); + } + return suppressions; + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/App/VerifyAction.cs b/src/ModVerify.CliApp/App/VerifyAction.cs new file mode 100644 index 0000000..0cfea0e --- /dev/null +++ b/src/ModVerify.CliApp/App/VerifyAction.cs @@ -0,0 +1,60 @@ +using AET.ModVerify.App.Reporting; +using AET.ModVerify.App.Settings; +using AET.ModVerify.Reporting; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AET.ModVerify.App.Utilities; + +namespace AET.ModVerify.App; + +internal sealed class VerifyAction(AppVerifySettings settings, IServiceProvider services) + : ModVerifyApplicationAction(settings, services) +{ + protected override void PrintAction(VerificationTarget target) + { + Console.WriteLine(); + Console.ForegroundColor = ConsoleColor.DarkGreen; + Console.WriteLine($"Verifying {target.Name} for issues..."); + Console.WriteLine(); + ModVerifyConsoleUtilities.WriteSelectedTarget(target); + Console.WriteLine(); + } + + protected override async Task ProcessVerifyFindings( + VerificationTarget verificationTarget, + IReadOnlyCollection allErrors) + { + Logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Reporting Errors..."); + var reportBroker = new VerificationReportBroker(ServiceProvider); + await reportBroker.ReportAsync(allErrors); + + if (Settings.AppFailsOnMinimumSeverity.HasValue && + allErrors.Any(x => x.Severity >= Settings.AppFailsOnMinimumSeverity)) + { + Logger?.LogInformation(ModVerifyConstants.ConsoleEventId, + "The verification of {Target} completed with findings of the specified failure severity {Severity}", + verificationTarget.Name, Settings.AppFailsOnMinimumSeverity); + + return ModVerifyConstants.CompletedWithFindings; + } + + return ModVerifyConstants.Success; + } + + protected override VerificationBaseline GetBaseline(VerificationTarget verificationTarget) + { + var baselineSelector = new BaselineSelector(Settings, ServiceProvider); + var baseline = baselineSelector.SelectBaseline(verificationTarget, out var baselinePath); + if (!baseline.IsEmpty) + { + Console.WriteLine(); + ModVerifyConsoleUtilities.WriteBaselineInfo(baseline, baselinePath); + Logger?.LogDebug("Using baseline {Baseline} from location '{Path}'", baseline.ToString(), baselinePath); + Console.WriteLine(); + } + return baseline; + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/AppArgumentException.cs b/src/ModVerify.CliApp/AppArgumentException.cs new file mode 100644 index 0000000..8ead9ed --- /dev/null +++ b/src/ModVerify.CliApp/AppArgumentException.cs @@ -0,0 +1,5 @@ +using System; + +namespace AET.ModVerify.App; + +internal class AppArgumentException(string message) : ArgumentException(message); \ No newline at end of file diff --git a/src/ModVerify.CliApp/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..0ac8346 100644 --- a/src/ModVerify.CliApp/GameFinder/GameFinderService.cs +++ b/src/ModVerify.CliApp/GameFinder/GameFinderService.cs @@ -1,16 +1,22 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO.Abstractions; +using System.Runtime.InteropServices; +using AET.ModVerify.App.Utilities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine; using PG.StarWarsGame.Infrastructure.Clients.Steam; using PG.StarWarsGame.Infrastructure.Games; +using PG.StarWarsGame.Infrastructure.Games.Registry; using PG.StarWarsGame.Infrastructure.Mods; using PG.StarWarsGame.Infrastructure.Services; using PG.StarWarsGame.Infrastructure.Services.Detection; -namespace AET.ModVerifyTool.GameFinder; +namespace AET.ModVerify.App.GameFinder; internal class GameFinderService { @@ -29,7 +35,7 @@ public GameFinderService(IServiceProvider serviceProvider) _logger = _serviceProvider.GetService()?.CreateLogger(GetType()); } - public GameFinderResult FindGames() + public GameFinderResult FindGames(GameFinderSettings settings) { var detectors = new List { @@ -37,10 +43,47 @@ public GameFinderResult FindGames() new SteamPetroglyphStarWarsGameDetector(_serviceProvider), }; - return FindGames(detectors); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var registryFactory = _serviceProvider.GetRequiredService(); + detectors.Add(new RegistryGameDetector( + registryFactory.CreateRegistry(GameType.Eaw), + registryFactory.CreateRegistry(GameType.Foc), + false, _serviceProvider)); + } + + return FindGames(detectors, settings); + } + + public IGame FindGame(string gamePath, GameFinderSettings settings) + { + var detectors = new List + { + new DirectoryGameDetector(_fileSystem.DirectoryInfo.New(gamePath), _serviceProvider), + }; + return FindGames(detectors, settings).Game; } - public GameFinderResult FindGamesFromPathOrGlobal(string path) + public bool TryFindGame(string gamePath, GameFinderSettings settings, [NotNullWhen(true)]out IGame? game) + { + var detectors = new List + { + new DirectoryGameDetector(_fileSystem.DirectoryInfo.New(gamePath), _serviceProvider), + }; + + try + { + game = FindGames(detectors, settings).Game; + return true; + } + catch (GameNotFoundException) + { + game = null; + return false; + } + } + + public GameFinderResult FindGamesFromPathOrGlobal(string path, GameFinderSettings settings) { // There are four common situations: // 1. path points to the actual game directory @@ -49,85 +92,136 @@ public GameFinderResult FindGamesFromPathOrGlobal(string path) // 4. path points to a "detached mod" at a completely different location var givenDirectory = _fileSystem.DirectoryInfo.New(path); var possibleGameDir = givenDirectory.Parent?.Parent; + + + // We need to check the local paths first, before falling back to global detectors, + // to ensure that we always find the correct game installation, + // especially if the user did not request a specific engine. - var detectors = new List + var localDetectors = new List { - // Case 1 - new DirectoryGameDetector(givenDirectory, _serviceProvider) + new DirectoryGameDetector(givenDirectory, _serviceProvider), }; + if (possibleGameDir is not null) + localDetectors.Add(new DirectoryGameDetector(possibleGameDir, _serviceProvider)); - // Case 2 - if (possibleGameDir is not null) - detectors.Add(new DirectoryGameDetector(possibleGameDir, _serviceProvider)); + // Case 1 & 2 + if (TryFindGames(localDetectors, settings, out var finderResult)) + return finderResult; + + + // There is the rare scenario where the user specified a specific engine, + // but path points to a game with the opposite engine. + // This does not make sense and the global detectors can not handle this. + // Thus, we need to check against this. + if (settings.Engine is not null) + { + if (TryFindGame(path, new GameFinderSettings + { + Engine = settings.Engine.Value.Opposite(), + InitMods = false, + SearchFallbackGame = false + }, out _)) + { + var e = new GameNotFoundException( + $"The specified game engine '{settings.Engine.Value}' does not match engine of the specified path '{path}'."); + _logger?.LogTrace(e, e.Message); + throw e; + } + } // Cases 3 & 4 - detectors.Add(new SteamPetroglyphStarWarsGameDetector(_serviceProvider)); - return FindGames(detectors); + return FindGames(CreateGlobalDetectors(), settings); } - private bool TryDetectGame(GameType gameType, IList detectors, out GameDetectionResult result) + private bool TryFindGames(IList detectors, GameFinderSettings settings, + [NotNullWhen(true)] out GameFinderResult? finderResult) { - var gd = new CompositeGameDetector(detectors, _serviceProvider); - try { - result = gd.Detect(gameType); - if (result.GameLocation is null) - return false; + finderResult = FindGames(detectors, settings); return true; } - catch (Exception e) + catch (GameNotFoundException) { - result = GameDetectionResult.NotInstalled(gameType); - _logger?.LogTrace($"Unable to find game installation: {e.Message}"); + finderResult = null; return false; } } - private GameFinderResult FindGames(IList detectors) + private GameFinderResult FindGames(IList detectors, GameFinderSettings settings) { - // FoC needs to be tried first - if (!TryDetectGame(GameType.Foc, detectors, out var result)) + GameDetectionResult? detectionResult = null; + if (settings.Engine is GameEngineType.Eaw) { + _logger?.LogTrace("Trying to find requested EaW installation."); + if (!TryDetectGame(GameType.Eaw, detectors, out detectionResult)) + { + var e = new GameNotFoundException($"Unable to find requested game installation '{settings.Engine}'. Wrong install path?"); + _logger?.LogTrace(e, e.Message); + throw e; + } + } + + if (detectionResult is null && !TryDetectGame(GameType.Foc, detectors, out detectionResult)) + { + if (settings.Engine is GameEngineType.Foc) + { + var e = new GameNotFoundException($"Unable to find requested game installation '{settings.Engine}'. Wrong install path?"); + _logger?.LogTrace(e, e.Message); + throw e; + } + + // If the engine is unspecified, we also need to check for EaW. _logger?.LogTrace("Unable to find FoC installation. Trying again with EaW..."); - if (!TryDetectGame(GameType.Eaw, detectors, out result)) + if (!TryDetectGame(GameType.Eaw, detectors, out detectionResult)) throw new GameNotFoundException("Unable to find game installation: Wrong install path?"); } - if (result.GameLocation is null) - throw new GameNotFoundException("Unable to find game installation: Wrong install path?"); + Debug.Assert(detectionResult.GameLocation is not null); - _logger?.LogInformation($"Found game installation: {result.GameIdentity} at {result.GameLocation.FullName}"); + _logger?.LogDebug("Found game installation: {ResultGameIdentity} at {GameLocationFullName}", + detectionResult.GameIdentity, detectionResult.GameLocation!.FullName); - var game = _gameFactory.CreateGame(result, CultureInfo.InvariantCulture); - - SetupMods(game); + var game = _gameFactory.CreateGame(detectionResult, CultureInfo.InvariantCulture); + if (settings.InitMods) + SetupMods(game); IGame? fallbackGame = null; - // If the game is Foc we want to set up Eaw as well as the fallbackGame - if (game.Type == GameType.Foc) + if (SearchForFallbackGame(settings, detectionResult)) { - var fallbackDetectors = new List(); - - if (game.Platform == GamePlatform.SteamGold) - fallbackDetectors.Add(new SteamPetroglyphStarWarsGameDetector(_serviceProvider)); - else - throw new NotImplementedException("Searching fallback game for non-Steam games is currently is not yet implemented."); - - if (!TryDetectGame(GameType.Eaw, fallbackDetectors, out var fallbackResult) || fallbackResult.GameLocation is null) + if (!TryDetectGame(GameType.Eaw, CreateGlobalDetectors(), out var fallbackResult)) throw new GameNotFoundException("Unable to find fallback game installation: Wrong install path?"); - _logger?.LogInformation($"Found fallback game installation: {fallbackResult.GameIdentity} at {fallbackResult.GameLocation.FullName}"); + _logger?.LogDebug("Found fallback game installation: {FallbackResultGameIdentity} at {GameLocationFullName}", + fallbackResult.GameIdentity, fallbackResult.GameLocation!.FullName); fallbackGame = _gameFactory.CreateGame(fallbackResult, CultureInfo.InvariantCulture); - SetupMods(fallbackGame); + if (settings.InitMods) + SetupMods(fallbackGame); } return new GameFinderResult(game, fallbackGame); } + private static bool SearchForFallbackGame(GameFinderSettings settings, GameDetectionResult? foundGame) + { + if (settings.Engine is GameEngineType.Eaw) + return false; + if (foundGame is { Installed: true, GameIdentity.Type: GameType.Eaw }) + return false; + return settings.SearchFallbackGame; + } + + private bool TryDetectGame(GameType gameType, IList detectors, out GameDetectionResult result) + { + var gd = new CompositeGameDetector(detectors, _serviceProvider, true); + result = gd.Detect(gameType); + return result.GameLocation is not null; + } + private void SetupMods(IGame game) { var modFinder = _serviceProvider.GetRequiredService(); @@ -150,4 +244,21 @@ private void SetupMods(IGame game) mod.ResolveDependencies(); } } + + private IList CreateGlobalDetectors() + { + var detectors = new List + { + new SteamPetroglyphStarWarsGameDetector(_serviceProvider), + }; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var registryFactory = _serviceProvider.GetRequiredService(); + detectors.Add(new RegistryGameDetector( + registryFactory.CreateRegistry(GameType.Eaw), + registryFactory.CreateRegistry(GameType.Foc), + false, _serviceProvider)); + } + return detectors; + } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/GameFinder/GameFinderSettings.cs b/src/ModVerify.CliApp/GameFinder/GameFinderSettings.cs new file mode 100644 index 0000000..fc57b24 --- /dev/null +++ b/src/ModVerify.CliApp/GameFinder/GameFinderSettings.cs @@ -0,0 +1,14 @@ +using PG.StarWarsGame.Engine; + +namespace AET.ModVerify.App.GameFinder; + +internal sealed class GameFinderSettings +{ + internal static readonly GameFinderSettings Default = new(); + + public bool InitMods { get; init; } = true; + + public bool SearchFallbackGame { get; init; } = true; + + public GameEngineType? Engine { get; init; } = null; +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/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 deleted file mode 100644 index 00fc4ae..0000000 --- a/src/ModVerify.CliApp/ModSelectors/AutomaticModSelector.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.Globalization; -using System.IO.Abstractions; -using System.Linq; -using AET.ModVerifyTool.GameFinder; -using AET.ModVerifyTool.Options; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Infrastructure; -using PG.StarWarsGame.Infrastructure.Games; -using PG.StarWarsGame.Infrastructure.Mods; -using PG.StarWarsGame.Infrastructure.Services; -using PG.StarWarsGame.Infrastructure.Services.Detection; - -namespace AET.ModVerifyTool.ModSelectors; - -internal class AutomaticModSelector(IServiceProvider serviceProvider) : ModSelectorBase(serviceProvider) -{ - private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); - - public override GameLocations? Select( - GameInstallationsSettings settings, - out IPhysicalPlayableObject? targetObject, - out GameEngineType? actualEngineType) - { - var pathToVerify = settings.AutoPath; - if (pathToVerify is null) - throw new InvalidOperationException("path to verify cannot be null."); - - actualEngineType = settings.EngineType; - - GameFinderResult finderResult; - try - { - finderResult = GameFinderService.FindGamesFromPathOrGlobal(pathToVerify); - } - catch (GameNotFoundException) - { - Logger?.LogError($"Unable to find games based of the given location '{settings.GamePath}'. Consider specifying all paths manually."); - targetObject = null!; - return null; - } - - - var modOrGame = GetAttachedModOrGame(finderResult, actualEngineType, pathToVerify); - - if (modOrGame is not null) - { - var actualType = modOrGame.Game.Type.ToEngineType(); - actualEngineType ??= actualType; - if (actualEngineType != actualType) - throw new ArgumentException($"The specified game type '{actualEngineType}' does not match the actual type of the game or mod to verify."); - - targetObject = modOrGame; - return GetLocations(targetObject, finderResult, settings.AdditionalFallbackPaths); - } - - if (!settings.EngineType.HasValue) - throw new ArgumentException("Unable to determine game type. Use --type argument to set the game type."); - - Logger?.LogDebug($"The requested mod at '{pathToVerify}' is detached from its games."); - - // The path is a detached mod, that exists on a different location than the game. - var result = GetDetachedModLocations(pathToVerify, finderResult, settings, out var mod); - targetObject = mod; - return result; - } - - private IPhysicalPlayableObject? GetAttachedModOrGame(GameFinderResult finderResult, GameEngineType? requestedEngineType, string searchPath) - { - var fullSearchPath = _fileSystem.Path.GetFullPath(searchPath); - - if (finderResult.Game.Directory.FullName.Equals(fullSearchPath, StringComparison.OrdinalIgnoreCase)) - { - if (finderResult.Game.Type.ToEngineType() != requestedEngineType) - throw new ArgumentException($"The specified game type '{requestedEngineType}' does not match the actual type of the game '{searchPath}' to verify."); - return finderResult.Game; - } - - if (finderResult.FallbackGame is not null && - finderResult.FallbackGame.Directory.FullName.Equals(fullSearchPath, StringComparison.OrdinalIgnoreCase)) - { - if (finderResult.FallbackGame.Type.ToEngineType() != requestedEngineType) - throw new ArgumentException($"The specified game type '{requestedEngineType}' does not match the actual type of the game '{searchPath}' to verify."); - return finderResult.FallbackGame; - } - - return GetMatchingModFromGame(finderResult.Game, requestedEngineType, fullSearchPath) ?? - GetMatchingModFromGame(finderResult.FallbackGame, requestedEngineType, fullSearchPath); - } - - private GameLocations GetDetachedModLocations(string modPath, GameFinderResult gameResult, GameInstallationsSettings settings, out IPhysicalMod mod) - { - IGame game = null!; - - if (gameResult.Game.Type.ToEngineType() == settings.EngineType) - game = gameResult.Game; - if (gameResult.FallbackGame is not null && gameResult.FallbackGame.Type.ToEngineType() == settings.EngineType) - game = gameResult.FallbackGame; - - if (game is null) - throw new GameNotFoundException($"Unable to find game of type '{settings.EngineType}'"); - - var modFinder = ServiceProvider.GetRequiredService(); - var modRef = modFinder.FindMods(game, _fileSystem.DirectoryInfo.New(modPath)).FirstOrDefault(); - - if (modRef is null) - throw new NotSupportedException($"The mod at '{modPath}' is not compatible to the found game '{game}'."); - - var modFactory = ServiceProvider.GetRequiredService(); - mod = modFactory.CreatePhysicalMod(game, modRef, CultureInfo.InvariantCulture); - - game.AddMod(mod); - - mod.ResolveDependencies(); - - return GetLocations(mod, gameResult, settings.AdditionalFallbackPaths); - } - - private static IPhysicalMod? GetMatchingModFromGame(IGame? game, GameEngineType? requestedEngineType, string modPath) - { - if (game is null) - return null; - - var isGameSupported = !requestedEngineType.HasValue || game.Type.ToEngineType() == requestedEngineType; - foreach (var mod in game.Game.Mods) - { - if (mod is IPhysicalMod physicalMod) - { - if (physicalMod.Directory.FullName.Equals(modPath, StringComparison.OrdinalIgnoreCase)) - { - if (!isGameSupported) - throw new ArgumentException($"The specified game type '{requestedEngineType}' does not match the actual type of the mod '{modPath}' to verify."); - return physicalMod; - } - } - } - - return null; - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectors/IModSelector.cs b/src/ModVerify.CliApp/ModSelectors/IModSelector.cs deleted file mode 100644 index f0b30a0..0000000 --- a/src/ModVerify.CliApp/ModSelectors/IModSelector.cs +++ /dev/null @@ -1,13 +0,0 @@ -using AET.ModVerifyTool.Options; -using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Infrastructure; - -namespace AET.ModVerifyTool.ModSelectors; - -internal interface IModSelector -{ - GameLocations? Select( - GameInstallationsSettings settings, - out IPhysicalPlayableObject? targetObject, - out GameEngineType? actualEngineType); -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectors/ManualModSelector.cs b/src/ModVerify.CliApp/ModSelectors/ManualModSelector.cs deleted file mode 100644 index d5913c6..0000000 --- a/src/ModVerify.CliApp/ModSelectors/ManualModSelector.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using AET.ModVerifyTool.Options; -using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Infrastructure; - -namespace AET.ModVerifyTool.ModSelectors; - -internal class ManualModSelector(IServiceProvider serviceProvider) : ModSelectorBase(serviceProvider) -{ - public override GameLocations Select( - GameInstallationsSettings settings, - out IPhysicalPlayableObject? targetObject, - out GameEngineType? actualEngineType) - { - actualEngineType = settings.EngineType; - targetObject = null; - - if (!actualEngineType.HasValue) - throw new ArgumentException("Unable to determine game type. Use --type argument to set the game type."); - - if (string.IsNullOrEmpty(settings.GamePath)) - throw new ArgumentException("Argument --game must be set."); - - return new GameLocations( - settings.ModPaths, - settings.GamePath!, - GetFallbackPaths(settings.FallbackGamePath, settings.AdditionalFallbackPaths)); - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectors/ModSelectorBase.cs b/src/ModVerify.CliApp/ModSelectors/ModSelectorBase.cs deleted file mode 100644 index 0eec285..0000000 --- a/src/ModVerify.CliApp/ModSelectors/ModSelectorBase.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using AET.ModVerifyTool.GameFinder; -using AET.ModVerifyTool.Options; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Infrastructure; -using PG.StarWarsGame.Infrastructure.Mods; -using PG.StarWarsGame.Infrastructure.Services.Dependencies; - -namespace AET.ModVerifyTool.ModSelectors; - -internal abstract class ModSelectorBase : IModSelector -{ - protected readonly ILogger? Logger; - protected readonly GameFinderService GameFinderService; - protected readonly IServiceProvider ServiceProvider; - - protected ModSelectorBase(IServiceProvider serviceProvider) - { - ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - Logger = serviceProvider.GetService()?.CreateLogger(GetType()); - GameFinderService = new GameFinderService(serviceProvider); - } - - public abstract GameLocations? Select( - GameInstallationsSettings settings, - out IPhysicalPlayableObject? targetObject, - out GameEngineType? actualEngineType); - - protected GameLocations GetLocations(IPhysicalPlayableObject playableObject, GameFinderResult finderResult, IList additionalFallbackPaths) - { - var fallbacks = GetFallbackPaths(finderResult, playableObject, additionalFallbackPaths); - var modPaths = GetModPaths(playableObject); - return new GameLocations(modPaths, playableObject.Game.Directory.FullName, fallbacks); - } - - private static IList GetFallbackPaths(GameFinderResult finderResult, IPlayableObject gameOrMod, IList additionalFallbackPaths) - { - var coercedFallbackGame = finderResult.FallbackGame; - if (gameOrMod.Equals(finderResult.FallbackGame)) - coercedFallbackGame = null; - else if (gameOrMod.Game.Equals(finderResult.FallbackGame)) - coercedFallbackGame = null; - - return GetFallbackPaths(coercedFallbackGame?.Directory.FullName, additionalFallbackPaths); - } - - - protected static IList GetFallbackPaths(string? fallbackGame, IList additionalFallbackPaths) - { - var fallbacks = new List(); - if (fallbackGame is not null) - fallbacks.Add(fallbackGame); - foreach (var fallback in additionalFallbackPaths) - fallbacks.Add(fallback); - - return fallbacks; - } - - - private IList GetModPaths(IPhysicalPlayableObject modOrGame) - { - if (modOrGame is not IMod mod) - return Array.Empty(); - - var traverser = ServiceProvider.GetRequiredService(); - return traverser.Traverse(mod) - .OfType().Select(x => x.Directory.FullName) - .ToList(); - } - -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectors/ModSelectorFactory.cs b/src/ModVerify.CliApp/ModSelectors/ModSelectorFactory.cs deleted file mode 100644 index 8335a86..0000000 --- a/src/ModVerify.CliApp/ModSelectors/ModSelectorFactory.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using AET.ModVerifyTool.Options; - -namespace AET.ModVerifyTool.ModSelectors; - -internal class ModSelectorFactory(IServiceProvider serviceProvider) -{ - public IModSelector CreateSelector(GameInstallationsSettings settings) - { - if (settings.Interactive) - return new ConsoleModSelector(serviceProvider); - if (settings.UseAutoDetection) - return new AutomaticModSelector(serviceProvider); - if (settings.ManualSetup) - return new ManualModSelector(serviceProvider); - throw new ArgumentException("Unknown option configuration provided."); - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectors/SettingsBasedModSelector.cs b/src/ModVerify.CliApp/ModSelectors/SettingsBasedModSelector.cs deleted file mode 100644 index 12f7861..0000000 --- a/src/ModVerify.CliApp/ModSelectors/SettingsBasedModSelector.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Linq; -using AET.ModVerifyTool.Options; -using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Infrastructure; - -namespace AET.ModVerifyTool.ModSelectors; - -internal class SettingsBasedModSelector(IServiceProvider serviceProvider) -{ - public VerifyInstallationInformation CreateInstallationDataFromSettings(GameInstallationsSettings settings) - { - var gameLocations = new ModSelectorFactory(serviceProvider) - .CreateSelector(settings) - .Select(settings, out var targetObject, out var engineType); - - if (gameLocations is null) - throw new GameNotFoundException("Unable to get game locations"); - - if (engineType is null) - throw new InvalidOperationException("Engine type not specified."); - - return new VerifyInstallationInformation - { - EngineType = engineType.Value, - GameLocations = gameLocations, - Name = GetNameFromGameLocations(targetObject, gameLocations, engineType.Value) - }; - } - - private static string GetNameFromGameLocations(IPlayableObject? targetObject, GameLocations gameLocations, GameEngineType engineType) - { - if (targetObject is not null) - return targetObject.Name; - - var mod = gameLocations.ModPaths.FirstOrDefault(); - return mod ?? gameLocations.GamePath; - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModVerify.CliApp.csproj b/src/ModVerify.CliApp/ModVerify.CliApp.csproj index 3ca41b6..ec2fd46 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 @@ -21,55 +21,77 @@ en + + true + + - - - - - 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 - - - - true - + + + + compile + runtime; build; native; contentfiles; analyzers; buildtransitive + + + compile + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + - - + + + + + + diff --git a/src/ModVerify.CliApp/ModVerify.CliApp.csproj.DotSettings b/src/ModVerify.CliApp/ModVerify.CliApp.csproj.DotSettings new file mode 100644 index 0000000..0bcf4ed --- /dev/null +++ b/src/ModVerify.CliApp/ModVerify.CliApp.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/ModVerify.CliApp/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..192d97b --- /dev/null +++ b/src/ModVerify.CliApp/ModVerifyAppEnvironment.cs @@ -0,0 +1,89 @@ +using System.IO.Abstractions; +using System.Reflection; +using AnakinRaW.ApplicationBase.Environment; +#if !NET +using System; +using System.IO; +using System.Net; +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(CreateDebugPath()), +#endif + new($"https://republicatwar.com/downloads/{ModVerifyConstants.ModVerifyToolPath}") + }; + + private static string CreateDebugPath() + { + var dir = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "../../../../..")); + return Path.Combine(dir, ".local_deploy/server"); + } + + public override string UpdateRegistryPath => $@"SOFTWARE\{ModVerifyConstants.ModVerifyToolPath}\Update"; + +#if NETFRAMEWORK + static ModVerifyAppEnvironment() + { + // For some unknown reason, packaging dependencies into the app, may alter the used security protocols... + // This reverts the changes and forces secure settings + if (ServicePointManager.SecurityProtocol != SecurityProtocolType.SystemDefault) + ServicePointManager.SecurityProtocol = SecurityProtocolType.SystemDefault | SecurityProtocolType.Tls12; + } +#endif + + 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/ModVerifyConstants.cs b/src/ModVerify.CliApp/ModVerifyConstants.cs new file mode 100644 index 0000000..041a4a5 --- /dev/null +++ b/src/ModVerify.CliApp/ModVerifyConstants.cs @@ -0,0 +1,17 @@ +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 const int Success = 0; + public const int CompletedWithFindings = 1; + public const int ErrorBadArguments = 0xA0; + + public static readonly EventId ConsoleEventId = new(ConsoleEventIdValue, "LogToConsole"); +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Options/CommandLine/CreateBaselineVerbOption.cs b/src/ModVerify.CliApp/Options/CommandLine/CreateBaselineVerbOption.cs deleted file mode 100644 index 78132cc..0000000 --- a/src/ModVerify.CliApp/Options/CommandLine/CreateBaselineVerbOption.cs +++ /dev/null @@ -1,10 +0,0 @@ -using CommandLine; - -namespace AET.ModVerifyTool.Options.CommandLine; - -[Verb("createBaseline", HelpText = "Verifies the specified game and creates a new baseline file at the specified location.")] -internal class CreateBaselineVerbOption : BaseModVerifyOptions -{ - [Option('o', "outFile", Required = true, HelpText = "The file path of the new baseline file.")] - public string OutputFile { get; set; } -} \ 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/Options/GameInstallationsSettings.cs b/src/ModVerify.CliApp/Options/GameInstallationsSettings.cs deleted file mode 100644 index 667e2cf..0000000 --- a/src/ModVerify.CliApp/Options/GameInstallationsSettings.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using PG.StarWarsGame.Engine; - -namespace AET.ModVerifyTool.Options; - -internal record GameInstallationsSettings -{ - public bool Interactive => string.IsNullOrEmpty(AutoPath) && ModPaths.Count == 0 && string.IsNullOrEmpty(GamePath); - - [MemberNotNullWhen(true, nameof(AutoPath))] - public bool UseAutoDetection => !string.IsNullOrEmpty(AutoPath); - - [MemberNotNullWhen(true, nameof(GamePath))] - public bool ManualSetup => !string.IsNullOrEmpty(GamePath); - - public string? AutoPath { get; init; } - - public IList ModPaths { get; init; } = Array.Empty(); - - public string? GamePath { get; init; } - - public string? FallbackGamePath { get; init; } - - public IList AdditionalFallbackPaths { get; init; } = Array.Empty(); - - public GameEngineType? EngineType { get; init; } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Options/ModVerifyAppSettings.cs b/src/ModVerify.CliApp/Options/ModVerifyAppSettings.cs deleted file mode 100644 index 6a3b0bd..0000000 --- a/src/ModVerify.CliApp/Options/ModVerifyAppSettings.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using AET.ModVerify.Reporting; -using AET.ModVerify.Reporting.Settings; -using AET.ModVerify.Settings; - -namespace AET.ModVerifyTool.Options; - -internal sealed class ModVerifyAppSettings -{ - public bool Interactive => GameInstallationsSettings.Interactive; - - public required VerifyPipelineSettings VerifyPipelineSettings { get; init; } - - public required GlobalVerifyReportSettings GlobalReportSettings { 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/Program.cs b/src/ModVerify.CliApp/Program.cs index 3ac92ba..2d23b3b 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,207 @@ 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 AET.ModVerify.App.Reporting; 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 async Task Main(string[] args) + private static readonly CompiledExpression PrintToConsoleExpression = SerilogExpression.Compile($"EventId.Id = {ModVerifyConstants.ConsoleEventIdValue}"); + + private AppSettingsBase? _modVerifyAppSettings; + private ApplicationUpdateOptions _updateOptions = new(); + private bool _offlineMode; + private bool _verboseMode; + private bool _isLaunchedWithoutArguments; + + protected override async Task InitializeAppAsync(IReadOnlyList args, IServiceProvider bootstrapServices) { - ConsoleUtilities.WriteHeader(); - - var result = 0; + await base.InitializeAppAsync(args, bootstrapServices); - 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) - { - var coreServiceCollection = CreateCoreServices(options.Verbose); - var coreServices = coreServiceCollection.BuildServiceProvider(); - var logger = coreServices.GetService()?.CreateLogger(typeof(Program)); - - logger?.LogDebug($"Raw command line: {Environment.CommandLine}"); + ModVerifyConsoleUtilities.WriteHeader(ApplicationEnvironment.AssemblyInfo.InformationalVersion); - var interactive = false; + ModVerifyOptionsContainer parsedOptions; 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); + parsedOptions = new ModVerifyOptionsParser(ApplicationEnvironment, BootstrapLoggerFactory).Parse(args); } 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 - { -#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 static async Task CheckForUpdate(IServiceProvider services, Microsoft.Extensions.Logging.ILogger? logger) - { - var updateChecker = new ModVerifyUpdaterChecker(services); + if (!parsedOptions.HasOptions) + return ModVerifyConstants.ErrorBadArguments; + + if (parsedOptions.UpdateOptions is not null) + _updateOptions = parsedOptions.UpdateOptions; - logger?.LogDebug("Checking for available update"); + if (parsedOptions.ModVerifyOptions?.Verbose is true || parsedOptions.UpdateOptions?.Verbose is true) + _verboseMode = true; + + if (parsedOptions.ModVerifyOptions is null) + return ModVerifyConstants.Success; + + _offlineMode = parsedOptions.ModVerifyOptions.OfflineMode; + _isLaunchedWithoutArguments = parsedOptions.ModVerifyOptions.LaunchedWithoutArguments(); try { - 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(); - - } + _modVerifyAppSettings = new SettingsBuilder(bootstrapServices) + .BuildSettings(parsedOptions.ModVerifyOptions); } - catch(Exception e) + catch (AppArgumentException e) { - logger?.LogWarning($"Unable to check for updates due to an internal error: {e.Message}"); - logger?.LogTrace(e, "Checking for update failed: " + e.Message); + Logger?.LogCritical(e, "Invalid arguments specified by the user: {Message}", e.Message); + ConsoleUtilities.WriteApplicationFatalError(ModVerifyConstants.AppNameString, e.Message); + return e.HResult; + } + catch (Exception e) + { + Logger?.LogCritical(e, "Failed to create settings form commandline arguments: {Message}", e.Message); + ConsoleUtilities.WriteApplicationFatalError(ModVerifyConstants.AppNameString, e); + return e.HResult; } + + return ModVerifyConstants.Success; } - private static IServiceCollection CreateCoreServices(bool verboseLogging) + protected override void CreateAppServices(IServiceCollection services, IReadOnlyList args) { - var fileSystem = new RealFileSystem(); - var serviceCollection = new ServiceCollection(); + base.CreateAppServices(services, args); - serviceCollection.AddSingleton(new WindowsRegistry()); - serviceCollection.AddSingleton(fileSystem); + services.AddSingleton((ApplicationEnvironment as ModVerifyAppEnvironment)!); - serviceCollection.AddLogging(builder => ConfigureLogging(builder, fileSystem, verboseLogging)); + services.AddLogging(ConfigureLogging); - return serviceCollection; - } + services.AddSingleton(sp => new HashingService(sp)); - private static IServiceProvider CreateAppServices(IServiceCollection serviceCollection, ModVerifyAppSettings settings) - { - serviceCollection.AddSingleton(sp => new HashingService(sp)); + + if (IsUpdateableApplication) + { +#if NET + throw new NotSupportedException(); +#endif + services.MakeAppUpdateable( + UpdatableApplicationEnvironment, + sp => new CosturaApplicationProductService(ApplicationEnvironment, sp), + sp => new JsonManifestLoader(sp)); + } + + if (_modVerifyAppSettings is null) + return; - SteamAbstractionLayer.InitializeServices(serviceCollection); - PetroglyphGameInfrastructure.InitializeServices(serviceCollection); + SteamAbstractionLayer.InitializeServices(services); + PetroglyphGameInfrastructure.InitializeServices(services); - serviceCollection.SupportMTD(); - serviceCollection.SupportMEG(); - serviceCollection.SupportALO(); - serviceCollection.SupportXML(); - PetroglyphCommons.ContributeServices(serviceCollection); + services.SupportMTD(); + services.SupportMEG(); + services.SupportALO(); + services.SupportXML(); + PetroglyphCommons.ContributeServices(services); - PetroglyphEngineServiceContribution.ContributeServices(serviceCollection); - serviceCollection.RegisterVerifierCache(); + PetroglyphEngineServiceContribution.ContributeServices(services); + services.RegisterVerifierCache(); - SetupVerifyReporting(serviceCollection, settings); + services.AddSingleton(sp => new BaselineFactory(sp)); + + SetupVerifyReporting(services); - if (settings.Offline) + if (_offlineMode) { - serviceCollection.AddSingleton(sp => new OfflineModNameResolver(sp)); - serviceCollection.AddSingleton(sp => new OfflineModGameTypeResolver(sp)); + services.AddSingleton(sp => new OfflineModNameResolver(sp)); + services.AddSingleton(sp => new OfflineModGameTypeResolver(sp)); } else { - serviceCollection.AddSingleton(sp => new OnlineModNameResolver(sp)); - serviceCollection.AddSingleton(sp => new OnlineModGameTypeResolver(sp)); + services.AddSingleton(sp => new OnlineModNameResolver(sp)); + services.AddSingleton(sp => new OnlineModGameTypeResolver(sp)); } - - return serviceCollection.BuildServiceProvider(); } - private static void SetupVerifyReporting(IServiceCollection serviceCollection, ModVerifyAppSettings settings) + protected override ApplicationEnvironment CreateAppEnvironment() + { + return new ModVerifyAppEnvironment(typeof(Program).Assembly, FileSystem); + } + + protected override IFileSystem CreateFileSystem() + { + return new RealFileSystem(); + } + + protected override IRegistry CreateRegistry() + { + return !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? new InMemoryRegistry(InMemoryRegistryCreationFlags.WindowsLike) + : new WindowsRegistry(); + } + + protected override async Task RunAppAsync(string[] args, IServiceProvider appServiceProvider) { - var printOnlySummary = settings.CreateNewBaseline; - serviceCollection.RegisterConsoleReporter(new VerifyReportSettings + var result = await HandleUpdate(appServiceProvider); + if (result != 0 || _modVerifyAppSettings is null) + return result; + return await new ModVerifyApplication(_modVerifyAppSettings, appServiceProvider).RunAsync().ConfigureAwait(false); + } + + private void SetupVerifyReporting(IServiceCollection serviceCollection) + { + Debug.Assert(_modVerifyAppSettings is not null); + + var verifySettings = _modVerifyAppSettings as AppVerifySettings; + + // Console should be in minimal summary mode if we are in a different mode than verify. + serviceCollection.RegisterConsoleReporter(new ReporterSettings { - MinimumReportSeverity = VerificationSeverity.Error - }, printOnlySummary); + MinimumReportSeverity = verifySettings?.VerifyPipelineSettings.FailFastSettings.IsFailFast is true + ? VerificationSeverity.Information + : VerificationSeverity.Error + }, summaryOnly: verifySettings is null); - if (string.IsNullOrEmpty(settings.ReportOutput)) + if (verifySettings == null) return; + var outputDirectory = verifySettings.ReportDirectory; serviceCollection.RegisterJsonReporter(new JsonReporterSettings { - OutputDirectory = settings.ReportOutput!, - MinimumReportSeverity = settings.GlobalReportSettings.MinimumReportSeverity + OutputDirectory = outputDirectory!, + MinimumReportSeverity = _modVerifyAppSettings.ReportSettings.MinimumReportSeverity }); serviceCollection.RegisterTextFileReporter(new TextFileReporterSettings { - OutputDirectory = settings.ReportOutput!, - MinimumReportSeverity = settings.GlobalReportSettings.MinimumReportSeverity + OutputDirectory = outputDirectory!, + MinimumReportSeverity = _modVerifyAppSettings.ReportSettings.MinimumReportSeverity }); } - private static void ConfigureLogging(ILoggingBuilder loggingBuilder, IFileSystem fileSystem, bool verbose) + private void ConfigureLogging(ILoggingBuilder loggingBuilder) { loggingBuilder.ClearProviders(); @@ -226,53 +242,115 @@ private static void ConfigureLogging(ILoggingBuilder loggingBuilder, IFileSystem loggingBuilder.AddDebug(); #endif - if (verbose) + if (_verboseMode) { 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"); + if (_offlineMode) + { + Logger?.LogTrace("Running in offline mode. There will be nothing to update."); + return ModVerifyConstants.Success; + } - return new LoggerConfiguration() - .Enrich.FromLogContext() - .MinimumLevel.Is(minLevel) - .Filter.ByExcluding(IsXmlParserLogging) - .WriteTo.Async(c => + ModVerifyUpdateMode updateMode; + + if (_isLaunchedWithoutArguments) + updateMode = ModVerifyUpdateMode.InteractiveUpdate; + else + { + updateMode = _modVerifyAppSettings is not null + ? ModVerifyUpdateMode.CheckOnly + : 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 ModVerifyConstants.Success; + } + 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..299ce46 100644 --- a/src/ModVerify.CliApp/Properties/launchSettings.json +++ b/src/ModVerify.CliApp/Properties/launchSettings.json @@ -1,17 +1,20 @@ { "profiles": { - "Interactive Verify": { + "Verify": { "commandName": "Project", - "commandLineArgs": "verify -o verifyResults --minFailSeverity Information -v --baseline focBaseline.json --offline" + "commandLineArgs": "" }, - "Interactive Baseline": { + "Verify (Interactive)": { "commandName": "Project", - "commandLineArgs": "createBaseline -o focBaseline.json --offline" + "commandLineArgs": "verify -o verifyResults --offline --minFailSeverity Information" }, - - "FromModPath": { + "Verify (Automatic Target Selection)": { "commandName": "Project", - "commandLineArgs": "-o verifyResults --baseline focBaseline.json --path C:/test --type Foc" + "commandLineArgs": "verify -o verifyResults --path \"C:\\Program Files (x86)\\Steam\\steamapps\\common\\Star Wars Empire at War\\corruption\"" + }, + "Create Baseline Interactive": { + "commandName": "Project", + "commandLineArgs": "createBaseline -o baseline.json --offline --skipLocation" } } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Reporting/BaselineFactory.cs b/src/ModVerify.CliApp/Reporting/BaselineFactory.cs new file mode 100644 index 0000000..a83cea9 --- /dev/null +++ b/src/ModVerify.CliApp/Reporting/BaselineFactory.cs @@ -0,0 +1,121 @@ +using AET.ModVerify.App.Settings; +using AET.ModVerify.Reporting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using System.Threading.Tasks; +using PG.StarWarsGame.Engine; +using AET.ModVerify.App.Utilities; + +namespace AET.ModVerify.App.Reporting; + +internal sealed class BaselineFactory(IServiceProvider serviceProvider) : IBaselineFactory +{ + private readonly ILogger? _logger = serviceProvider.GetService()?.CreateLogger(typeof(BaselineFactory)); + private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); + + public bool TryFindBaselineInDirectory( + string directory, + Predicate baselineSelector, + [NotNullWhen(true)] out VerificationBaseline? baseline, + [NotNullWhen(true)] out string? path) + { + baseline = null; + 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 + { + var parsedBaseline = CreateBaselineFromFilePath(jsonFile); + if (!baselineSelector(parsedBaseline)) + { + _logger?.LogDebug("Baseline '{JsonFile}' was denied by selector.", jsonFile); + continue; + } + + baseline = parsedBaseline; + path = _fileSystem.Path.GetFullPath(jsonFile); + + _logger?.LogDebug("Create baseline from file '{JsonFile}'", jsonFile); + return true; + } + catch (InvalidBaselineException e) + { + _logger?.LogDebug("'{JsonFile}' is not a valid baseline file: {Message}", jsonFile, e.Message); + // Ignore this exception + } + } + + baseline = null; + path = null; + return false; + } + + public VerificationBaseline ParseBaseline(string filePath) + { + return CreateBaselineFromFilePath(filePath); + } + + public VerificationBaseline CreateBaseline( + VerificationTarget target, + AppBaselineSettings settings, + IEnumerable errors) + { + var baselineTarget = new BaselineVerificationTarget + { + Engine = target.Engine, + Name = target.Name, + Version = target.Version, + Location = settings.WriteLocations ? MaskUsername(target.Location) : null, + IsGame = target.IsGame, + }; + + return new VerificationBaseline(settings.ReportSettings.MinimumReportSeverity, errors, baselineTarget); + } + + private static GameLocations MaskUsername(GameLocations targetLocation) + { + return new GameLocations( + targetLocation.ModPaths.Select(PathUtilities.MaskUsername).ToList(), + PathUtilities.MaskUsername(targetLocation.GamePath), + targetLocation.FallbackPaths.Select(PathUtilities.MaskUsername).ToList()); + } + + public async Task WriteBaselineAsync(VerificationBaseline baseline, string filePath) + { +#if NET + await +#endif + using var fs = _fileSystem.FileStream.New(filePath, FileMode.Create, FileAccess.Write, FileShare.None); + await baseline.ToJsonAsync(fs); + } + + private VerificationBaseline CreateBaselineFromFilePath(string baselineFile) + { + using var fs = _fileSystem.FileStream.New(baselineFile, FileMode.Open, FileAccess.Read); + 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..791eaae --- /dev/null +++ b/src/ModVerify.CliApp/Reporting/BaselineSelector.cs @@ -0,0 +1,169 @@ +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; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace AET.ModVerify.App.Reporting; + +internal sealed class BaselineSelector(AppVerifySettings settings, IServiceProvider services) +{ + private readonly ILogger? _logger = services.GetService()?.CreateLogger(typeof(ModVerifyApplication)); + private readonly IBaselineFactory _baselineFactory = services.GetRequiredService(); + + public VerificationBaseline SelectBaseline(VerificationTarget verificationTarget, out string? usedBaselinePath) + { + var baselinePath = settings.ReportSettings.BaselinePath; + if (!string.IsNullOrEmpty(baselinePath)) + { + try + { + usedBaselinePath = baselinePath; + return _baselineFactory.ParseBaseline(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.IsInteractive) + return FindBaselineInteractive(verificationTarget, out usedBaselinePath); + + // If the application is not interactive, we only use a baseline file present in the directory of the verification target. + return FindBaselineNonInteractive(verificationTarget, out usedBaselinePath); + + } + + private VerificationBaseline FindBaselineInteractive(VerificationTarget verificationTarget, out string? baselinePath) + { + // The application is in interactive mode. We apply the following lookup: + // 1. Use a baseline found in the directory of the verification target. + // 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.TryFindBaselineInDirectory( + verificationTarget.Location.TargetPath, + b => IsBaselineCompatible(b, verificationTarget), + out var baseline, + out baselinePath)) + { + if (!_baselineFactory.TryFindBaselineInDirectory( + Environment.CurrentDirectory, + b => IsBaselineCompatible(b, verificationTarget), + out baseline, + out baselinePath)) + { + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine("No baseline found locally."); + Console.ResetColor(); + baselinePath = null; + TryGetDefaultBaseline(verificationTarget.Engine, out baseline); + return baseline ?? VerificationBaseline.Empty; + } + } + + Debug.Assert(baselinePath is not null && baseline is not null); + + return ShouldUseBaseline(baseline, baselinePath) + ? baseline + : VerificationBaseline.Empty; + } + + private static bool TryGetDefaultBaseline( + GameEngineType engineType, + [NotNullWhen(true)] out VerificationBaseline? baseline) + { + baseline = null; + if (engineType == GameEngineType.Eaw) + { + // TODO: EAW currently not implemented + return false; + } + + if (!ConsoleUtilities.UserYesNoQuestion($"Do you want to load the default baseline for game engine '{engineType}'?")) + return false; + + try + { + baseline = LoadEmbeddedBaseline(engineType); + return true; + } + catch (InvalidBaselineException) + { + throw new InvalidOperationException( + "Invalid baseline packed along ModVerify App. Please reach out to the creators. Thanks!"); + } + } + + internal static 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(VerificationTarget target, out string? usedPath) + { + if (_baselineFactory.TryFindBaselineInDirectory( + target.Location.TargetPath, + b => IsBaselineCompatible(b, target), + out var baseline, + out usedPath)) + { + _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Automatically applying local baseline file '{Path}'.", usedPath); + return baseline; + } + _logger?.LogTrace("No baseline file found in taget path '{TargetPath}'.", target.Location.TargetPath); + usedPath = null; + return VerificationBaseline.Empty; + } + + + private static bool IsBaselineCompatible(VerificationBaseline baseline, VerificationTarget target) + { + return baseline.Target?.Engine == target.Engine; + } + + private static bool ShouldUseBaseline(VerificationBaseline baseline, string baselinePath) + { + var sb = new StringBuilder("Found baseline "); + if (baseline.Target is not null) + sb.Append($"for '{baseline.Target.Name}' "); + + sb.Append($"at '{baselinePath}'."); + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine(sb.ToString()); + + return ConsoleUtilities.UserYesNoQuestion("Do you want to use it?"); + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Reporting/EngineInitializeProgressReporter.cs b/src/ModVerify.CliApp/Reporting/EngineInitializeProgressReporter.cs index 69413c0..d93462f 100644 --- a/src/ModVerify.CliApp/Reporting/EngineInitializeProgressReporter.cs +++ b/src/ModVerify.CliApp/Reporting/EngineInitializeProgressReporter.cs @@ -1,30 +1,25 @@ using System; +using PG.StarWarsGame.Engine; -namespace AET.ModVerifyTool.Reporting; +namespace AET.ModVerify.App.Reporting; -internal sealed class EngineInitializeProgressReporter : IDisposable -{ - private Progress? _progress; - - public EngineInitializeProgressReporter(Progress? progress) +internal sealed class EngineInitializeProgressReporter(GameEngineType engine) : IGameEngineInitializationReporter +{ + public void ReportProgress(string message) { - if (progress is null) - return; - progress.ProgressChanged += OnProgress; + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine(message); + Console.ResetColor(); } - private void OnProgress(object sender, string e) + public void ReportStarted() { - Console.ForegroundColor = ConsoleColor.DarkGray; - Console.WriteLine(e); - Console.ResetColor(); + Console.WriteLine($"Initializing game engine '{engine}'..."); } - public void Dispose() + public void ReportFinished() { + Console.WriteLine($"Game engine initialized."); Console.WriteLine(); - if (_progress is not null) - _progress.ProgressChanged -= OnProgress; - _progress = null; } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Reporting/IBaselineFactory.cs b/src/ModVerify.CliApp/Reporting/IBaselineFactory.cs new file mode 100644 index 0000000..721a7ce --- /dev/null +++ b/src/ModVerify.CliApp/Reporting/IBaselineFactory.cs @@ -0,0 +1,26 @@ +using AET.ModVerify.App.Settings; +using AET.ModVerify.Reporting; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +namespace AET.ModVerify.App.Reporting; + +internal interface IBaselineFactory +{ + bool TryFindBaselineInDirectory( + string directory, + Predicate baselineSelector, + [NotNullWhen(true)] out VerificationBaseline? baseline, + [NotNullWhen(true)] out string? path); + + VerificationBaseline ParseBaseline(string filePath); + + Task WriteBaselineAsync(VerificationBaseline baseline, string filePath); + + VerificationBaseline CreateBaseline( + VerificationTarget target, + AppBaselineSettings settings, + IEnumerable errors); +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs b/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs index 37d3ca2..b2ce170 100644 --- a/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs +++ b/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs @@ -1,13 +1,15 @@ -using AnakinRaW.CommonUtilities; -using AnakinRaW.CommonUtilities.SimplePipeline.Progress; -using ShellProgressBar; -using System; +using System; using System.Threading; +using AET.ModVerify.App.Settings; 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 +public sealed class VerifyConsoleProgressReporter(string toVerifyName, AppReportSettings reportSettings) + : DisposableObject, IVerifyProgressReporter { private static readonly ProgressBarOptions ProgressBarOptions = new() { @@ -17,6 +19,7 @@ public sealed class VerifyConsoleProgressReporter(string toVerifyName) : Disposa WriteQueuedMessage = WriteQueuedMessage, }; + private readonly bool _verbose = reportSettings.Verbose; private ProgressBar? _progressBar; public void ReportError(string message, string? errorLine) @@ -38,8 +41,8 @@ public void Report(double progress, string? progressText, ProgressType type, Ver var progressBar = EnsureProgressBar(); - // TODO: Only recognize detailed mode - progressBar.Message = progressText; + if (detailedProgress.IsDetailed) + progressBar.Message = progressText; if (progress >= 1.0) progressBar.Message = $"Verified '{toVerifyName}'"; @@ -47,8 +50,8 @@ public void Report(double progress, string? progressText, ProgressType type, Ver var cpb = progressBar.AsProgress(); cpb.Report(progress); - // TODO: Only in verbose mode - //progressBar.WriteLine(progressText); + if (_verbose) + progressBar.WriteLine(progressText); } protected override void DisposeResources() diff --git a/src/ModVerify.CliApp/Resources/Baselines/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..ce70f8a --- /dev/null +++ b/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json @@ -0,0 +1,3047 @@ +{ + "version": "2.1", + "target": { + "name": "Forces of Corruption (SteamGold)", + "engine": "Foc", + "isGame": true, + "version": "1.121.13.7360" + }, + "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": "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_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", + "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_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_Officer.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_Officer.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", + "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": "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_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": "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 \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": "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": "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.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_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": "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_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_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": "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": "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_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_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_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": "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 \u0027RV_nebulonb_D_death_00.ALO\u0027", + "severity": "Error", + "context": [], + "asset": "RV_nebulonb_D_death_00.ALO" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_02_STATION_D.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\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 \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_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": "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" + ], + "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": "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 \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": "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": "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": "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": "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 \u0027W_AllShaders.ALO\u0027", + "severity": "Error", + "context": [], + "asset": "W_AllShaders.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\\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 \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": "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", + "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", + "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_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": "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" + ], + "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 \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": "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_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": "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 \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" + ], + "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": "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" + ], + "message": "Unable to find .ALO file \u0027Cin_Shuttle_Tyderium.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_Shuttle_Tyderium.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027W_SwampGasEmit.ALO\u0027", + "severity": "Error", + "context": [], + "asset": "W_SwampGasEmit.ALO" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027P_heat_small01\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_VCH.ALO\u0027", + "severity": "Error", + "context": [ + "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 \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_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 \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": "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 \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_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 \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": "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": "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": "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": "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", + "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", + "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" + ], + "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_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": "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 \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": "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" + ], + "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_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": "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", + "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": "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": "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": "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 \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" + ], + "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_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", + "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": "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_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": "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" + ], + "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": "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": "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": "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": "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": "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 \u0027CIN_Rbel_Soldier_Group.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Rbel_Soldier_Group.alo" + }, + { + "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_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_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_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_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 \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_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_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 \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_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 \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_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 \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_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 \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_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_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_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_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_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_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_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_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 \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_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_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_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_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_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_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_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_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_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 \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_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_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 \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_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_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 \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 \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_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_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_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_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_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_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_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_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_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_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_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 \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_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.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_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 \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_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_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 \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_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 \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_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_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_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_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_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 \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 \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.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_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 \u0027underworld_logo_off.tga\u0027 at location \u0027MegaTexture\u0027.", + "severity": "Error", + "context": [ + "IDC_PLAY_FACTION_A_BUTTON_BIG", + "MegaTexture" + ], + "asset": "underworld_logo_off.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 \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": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_planet_land_forces\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_planet_land_forces" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_ground_sell\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_ground_sell" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_bracket_medium\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_bracket_medium" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027b_planet_right\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "b_planet_right" + }, + { + "id": "CMDBAR04", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_credit_bar\u0027 is not supported by the game.", + "severity": "Information", + "context": [], + "asset": "g_credit_bar" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027zoomed_header_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "zoomed_header_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_space_level_pips\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_space_level_pips" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027bribed_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "bribed_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027encyclopedia_header_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "encyclopedia_header_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tutorial_text_back\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tutorial_text_back" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027encyclopedia_back\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "encyclopedia_back" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027encyclopedia_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "encyclopedia_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027balance_pip\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "balance_pip" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027objective_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "objective_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027skirmish_upgrade\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "skirmish_upgrade" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027surface_mod_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "surface_mod_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_hero_health\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_hero_health" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027bribe_display\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "bribe_display" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_build\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_build" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027garrison_slot_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "garrison_slot_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_conflict\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_conflict" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tooltip_name\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tooltip_name" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027garrison_respawn_counter\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "garrison_respawn_counter" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_ability_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_ability_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_shields_medium\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_shields_medium" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_health\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_health" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_weather\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_weather" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_health_medium\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_health_medium" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_power\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_power" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_ground_level_pips\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_ground_level_pips" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027zoomed_cost_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "zoomed_cost_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027bm_title_4011\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "bm_title_4011" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_planet_name\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_planet_name" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_shields_large\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_shields_large" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_hero_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_hero_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027generic_flytext\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "generic_flytext" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027reinforcement_counter\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "reinforcement_counter" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_planet_value\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_planet_value" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027radar_blip\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "radar_blip" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_political_control\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_political_control" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_planet_ring\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_planet_ring" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_garrison_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_garrison_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027encyclopedia_right_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "encyclopedia_right_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027b_quick_ref\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "b_quick_ref" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027objective_back\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "objective_back" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027encyclopedia_center_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "encyclopedia_center_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_shields\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_shields" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_grab_bar\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_grab_bar" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_smuggler\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_smuggler" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_enemy_hero\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_enemy_hero" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027cs_ability_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "cs_ability_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027encyclopedia_cost_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "encyclopedia_cost_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_planet_ability\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_planet_ability" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_control_group\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_control_group" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027gui_dialog_tooltip\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "gui_dialog_tooltip" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027remote_bomb_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "remote_bomb_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tutorial_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tutorial_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_space_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_space_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_bracket_large\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_bracket_large" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027zoomed_back\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "zoomed_back" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027encyclopedia_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "encyclopedia_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027zoomed_right_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "zoomed_right_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027b_beacon_t\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "b_beacon_t" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_bounty_hunter\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_bounty_hunter" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_credit_bar\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_credit_bar" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_hero\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_hero" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027bm_title_4010\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "bm_title_4010" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_planet_fleet\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_planet_fleet" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_corruption_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_corruption_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_smuggled\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_smuggled" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027help_back\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "help_back" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_bracket_small\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_bracket_small" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027objective_header_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "objective_header_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_corruption_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_corruption_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_ground_level\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_ground_level" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027lt_weather_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "lt_weather_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027cs_ability_button\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "cs_ability_button" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_radar_view\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_radar_view" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027objective_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "objective_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tooltip_back\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tooltip_back" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027zoomed_center_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "zoomed_center_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_health_bar\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_health_bar" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027zoomed_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "zoomed_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027generic_collision\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "generic_collision" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tooltip_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tooltip_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_radar_blip\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_radar_blip" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_hero_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_hero_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_space_level\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_space_level" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027b_planet_left\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "b_planet_left" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tooltip_icon_land\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tooltip_icon_land" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_ground_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_ground_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tooltip_price\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tooltip_price" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tooltip_left_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tooltip_left_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_health_large\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_health_large" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tactical_sell\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tactical_sell" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_special_ability\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_special_ability" + } + ] +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Options/CommandLine/BaseModVerifyOptions.cs b/src/ModVerify.CliApp/Settings/CommandLine/BaseModVerifyOptions.cs similarity index 77% rename from src/ModVerify.CliApp/Options/CommandLine/BaseModVerifyOptions.cs rename to src/ModVerify.CliApp/Settings/CommandLine/BaseModVerifyOptions.cs index d239112..36c8509 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 and base games itself. " + "The argument cannot be combined with any of --mods, --game or --fallbackGame")] - public string? AutoPath { get; set; } + public string? TargetPath { get; init; } [Option("mods", SetName = "manualPaths", Required = false, Default = null, Separator = ';', HelpText = "The path of the mod to verify. To support submods, multiple paths can be separated using the ';' (semicolon) character. " + "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'. " + + [Option("engine", Required = false, Default = null, + HelpText = "The game engine of the target that shall be verified. Skip this value to auto-determine the type. Valid values are 'Eaw' and 'Foc'. " + "This argument is required, if the first mod of '--mods' points to a directory outside of the common folder hierarchy (e.g, /MODS/MOD_NAME or /32470/WORKSHOP_ID")] - public GameEngineType? GameType { get; set; } + public GameEngineType? Engine { 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/Settings/CommandLine/CreateBaselineVerbOption.cs b/src/ModVerify.CliApp/Settings/CommandLine/CreateBaselineVerbOption.cs new file mode 100644 index 0000000..6593245 --- /dev/null +++ b/src/ModVerify.CliApp/Settings/CommandLine/CreateBaselineVerbOption.cs @@ -0,0 +1,13 @@ +using 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 sealed class CreateBaselineVerbOption : BaseModVerifyOptions +{ + [Option('o', "outFile", Required = true, HelpText = "The file path of the new baseline file.")] + public required string OutputFile { get; init; } + + [Option("skipLocation", Required = false, HelpText = "Skips writing the target location to the baseline.")] + public bool SkipLocation { get; init; } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/CommandLine/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..e3be836 --- /dev/null +++ b/src/ModVerify.CliApp/Settings/CommandLine/VerifyVerbOption.cs @@ -0,0 +1,41 @@ +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 requires 'minFailSeverity' to be set.")] + public bool FailFast { get; init; } + + [Option("minFailSeverity", Required = false, Default = null, + HelpText = "When set, the application returns with an error, if any finding has at least the specified severity value.")] + public VerificationSeverity? MinimumFailureSeverity { get; set; } + + [Option("ignoreAsserts", Required = false, + HelpText = "When this flag is present, the application will not report engine assertions.")] + public bool IgnoreAsserts { get; init; } + + + [Option("baseline", SetName = "baselineSelection", Required = false, + HelpText = "Path to a JSON baseline file. Cannot be used together with --searchBaseline.")] + public string? Baseline { get; init; } + + [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/Settings/CommandLineHelper.cs b/src/ModVerify.CliApp/Settings/CommandLineHelper.cs new file mode 100644 index 0000000..f799591 --- /dev/null +++ b/src/ModVerify.CliApp/Settings/CommandLineHelper.cs @@ -0,0 +1,18 @@ +using System; +using System.Linq; +using System.Reflection; +using CommandLine; + +namespace AET.ModVerify.App.Settings; + +internal static class CommandLineHelper +{ + public static string GetOptionName(this Type type, string optionPropertyName) + { + var property = type.GetProperties().FirstOrDefault(p => p.Name.Equals(optionPropertyName)); + var optionAttribute = property?.GetCustomAttribute(); + return optionAttribute is null + ? throw new InvalidOperationException($"Unable to get option data for {type}:{optionAttribute}") + : $"--{optionAttribute.LongName}"; + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/ModVerifyAppSettings.cs b/src/ModVerify.CliApp/Settings/ModVerifyAppSettings.cs new file mode 100644 index 0000000..e4488a0 --- /dev/null +++ b/src/ModVerify.CliApp/Settings/ModVerifyAppSettings.cs @@ -0,0 +1,77 @@ +using System; +using AET.ModVerify.Reporting; +using AET.ModVerify.Settings; + +namespace AET.ModVerify.App.Settings; + +public class AppReportSettings +{ + public VerificationSeverity MinimumReportSeverity { get; init; } + + public string? SuppressionsPath { get; init; } + + public bool Verbose { get; init; } +} + +public sealed class VerifyReportSettings : AppReportSettings +{ + public string? BaselinePath { get; init; } + public bool SearchBaselineLocally { get; init; } +} + +internal abstract class AppSettingsBase(AppReportSettings reportSettings) +{ + public bool IsInteractive => VerificationTargetSettings.Interactive; + + public required VerificationTargetSettings VerificationTargetSettings + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } + + public required VerifyPipelineSettings VerifyPipelineSettings + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } + + public AppReportSettings ReportSettings { get; } = reportSettings ?? throw new ArgumentNullException(nameof(reportSettings)); +} + +internal abstract class AppSettingsBase(T reportSettings) : AppSettingsBase(reportSettings) + where T : AppReportSettings +{ + public new T ReportSettings { get; } = reportSettings ?? throw new ArgumentNullException(nameof(reportSettings)); +} + +internal sealed class AppVerifySettings(VerifyReportSettings reportSettings) : AppSettingsBase(reportSettings) +{ + public VerificationSeverity? AppFailsOnMinimumSeverity { get; init; } + + public required string ReportDirectory + { + get; + init + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentNullException(nameof(value)); + field = value; + } + } +} + +internal sealed class AppBaselineSettings(AppReportSettings reportSettings) : AppSettingsBase(reportSettings) +{ + public required string NewBaselinePath + { + get; + init + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentNullException(nameof(value)); + field = value; + } + } + + public bool WriteLocations { get; init; } = true; +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/SettingsBuilder.cs b/src/ModVerify.CliApp/Settings/SettingsBuilder.cs new file mode 100644 index 0000000..23a89ff --- /dev/null +++ b/src/ModVerify.CliApp/Settings/SettingsBuilder.cs @@ -0,0 +1,168 @@ +using AET.ModVerify.App.Settings.CommandLine; +using AET.ModVerify.Pipeline; +using AET.ModVerify.Settings; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.IO.Abstractions; + +namespace AET.ModVerify.App.Settings; + +internal sealed class SettingsBuilder(IServiceProvider serviceProvider) +{ + private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); + + public AppSettingsBase BuildSettings(BaseModVerifyOptions options) + { + switch (options) + { + case VerifyVerbOption verifyVerb: + return BuildFromVerifyVerb(verifyVerb); + case CreateBaselineVerbOption baselineVerb: + return BuildFromCreateBaselineVerb(baselineVerb); + } + throw new NotSupportedException($"The option '{options.GetType().Name}' is not supported!"); + } + + private AppVerifySettings BuildFromVerifyVerb(VerifyVerbOption verifyOptions) + { + ValidateVerb(); + var failFastSetting = GetFailFastSetting(); + return new AppVerifySettings(BuildReportSettings()) + { + ReportDirectory = GetReportDirectory(), + VerifyPipelineSettings = new VerifyPipelineSettings + { + ParallelVerifiers = verifyOptions.Parallel ? 4 : 1, + VerifiersProvider = new DefaultGameVerifiersProvider(), + FailFastSettings = failFastSetting, + GameVerifySettings = new GameVerifySettings + { + IgnoreAsserts = verifyOptions.IgnoreAsserts, + ThrowsOnMinimumSeverity = failFastSetting.IsFailFast + ? failFastSetting.MinumumSeverity + // The app shall not make a specific verifier throw, but it should always run to completion. + : null + } + }, + AppFailsOnMinimumSeverity = verifyOptions.MinimumFailureSeverity, + VerificationTargetSettings = BuildTargetSettings(verifyOptions), + }; + + void ValidateVerb() + { + if (verifyOptions.SearchBaselineLocally && !string.IsNullOrEmpty(verifyOptions.Baseline)) + { + var searchOption = typeof(VerifyVerbOption).GetOptionName(nameof(VerifyVerbOption.SearchBaselineLocally)); + var baselineOption = typeof(VerifyVerbOption).GetOptionName(nameof(VerifyVerbOption.Baseline)); + throw new AppArgumentException($"Options {searchOption} and {baselineOption} cannot be used together."); + } + + if (verifyOptions is { FailFast: true, MinimumFailureSeverity: null }) + { + var failFast = typeof(VerifyVerbOption).GetOptionName(nameof(VerifyVerbOption.FailFast)); + var minThrowSeverity = typeof(VerifyVerbOption).GetOptionName(nameof(VerifyVerbOption.MinimumFailureSeverity)); + throw new AppArgumentException($"Option {failFast} requires to set {minThrowSeverity}."); + } + } + + FailFastSetting GetFailFastSetting() + { + return !verifyOptions.FailFast + ? FailFastSetting.NoFailFast + : new FailFastSetting(verifyOptions.MinimumFailureSeverity!.Value); + } + + string GetReportDirectory() + { + return _fileSystem.Path.GetFullPath(_fileSystem.Path.Combine( + Environment.CurrentDirectory, + verifyOptions.OutputDirectory ?? "ModVerifyResults")); + } + + VerifyReportSettings BuildReportSettings() + { + return new VerifyReportSettings + { + BaselinePath = verifyOptions.Baseline, + MinimumReportSeverity = verifyOptions.MinimumSeverity, + SearchBaselineLocally = verifyOptions.SearchBaselineLocally, + SuppressionsPath = verifyOptions.Suppressions, + Verbose = verifyOptions.Verbose + }; + } + } + + private AppBaselineSettings BuildFromCreateBaselineVerb(CreateBaselineVerbOption baselineVerb) + { + return new AppBaselineSettings(BuildReportSettings()) + { + VerifyPipelineSettings = new VerifyPipelineSettings + { + ParallelVerifiers = baselineVerb.Parallel ? 4 : 1, + VerifiersProvider = new DefaultGameVerifiersProvider(), + GameVerifySettings = GameVerifySettings.Default, + FailFastSettings = FailFastSetting.NoFailFast, + }, + VerificationTargetSettings = BuildTargetSettings(baselineVerb), + NewBaselinePath = baselineVerb.OutputFile, + WriteLocations = !baselineVerb.SkipLocation + }; + + AppReportSettings BuildReportSettings() + { + return new AppReportSettings + { + MinimumReportSeverity = baselineVerb.MinimumSeverity, + SuppressionsPath = baselineVerb.Suppressions, + Verbose = baselineVerb.Verbose + }; + } + } + + private VerificationTargetSettings BuildTargetSettings(BaseModVerifyOptions options) + { + var modPaths = new List(); + if (options.ModPaths is not null) + { + foreach (var mod in options.ModPaths) + { + if (!string.IsNullOrEmpty(mod)) + modPaths.Add(_fileSystem.Path.GetFullPath(mod)); + } + } + + var fallbackPaths = new List(); + if (options.AdditionalFallbackPath is not null) + { + foreach (var fallback in options.AdditionalFallbackPath) + { + if (!string.IsNullOrEmpty(fallback)) + fallbackPaths.Add(_fileSystem.Path.GetFullPath(fallback)); + } + } + + var gamePath = options.GamePath; + if (!string.IsNullOrEmpty(gamePath)) + gamePath = _fileSystem.Path.GetFullPath(gamePath!); + + + string? fallbackGamePath = null; + if (!string.IsNullOrEmpty(gamePath) && !string.IsNullOrEmpty(options.FallbackGamePath)) + fallbackGamePath = _fileSystem.Path.GetFullPath(options.FallbackGamePath!); + + var targetPath = options.TargetPath; + if (!string.IsNullOrEmpty(targetPath)) + targetPath = _fileSystem.Path.GetFullPath(targetPath!); + + return new VerificationTargetSettings + { + TargetPath = targetPath, + ModPaths = modPaths, + GamePath = gamePath, + FallbackGamePath = fallbackGamePath, + AdditionalFallbackPaths = fallbackPaths, + Engine = options.Engine + }; + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/VerificationTargetSettings.cs b/src/ModVerify.CliApp/Settings/VerificationTargetSettings.cs new file mode 100644 index 0000000..b5e1a40 --- /dev/null +++ b/src/ModVerify.CliApp/Settings/VerificationTargetSettings.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using PG.StarWarsGame.Engine; + +namespace AET.ModVerify.App.Settings; + +internal sealed record VerificationTargetSettings +{ + public bool Interactive => string.IsNullOrEmpty(TargetPath) && ModPaths.Count == 0 && string.IsNullOrEmpty(GamePath) && string.IsNullOrEmpty(FallbackGamePath); + + [MemberNotNullWhen(true, nameof(TargetPath))] + public bool UseAutoDetection => !string.IsNullOrEmpty(TargetPath) && ModPaths.Count == 0 && string.IsNullOrEmpty(GamePath) && string.IsNullOrEmpty(FallbackGamePath); + + [MemberNotNullWhen(true, nameof(GamePath))] + public bool ManualSetup => !string.IsNullOrEmpty(GamePath); + + public string? TargetPath { get; init; } + + public IReadOnlyList ModPaths { get; init; } = []; + + public string? GamePath { get; init; } + + public string? FallbackGamePath { get; init; } + + public IReadOnlyList AdditionalFallbackPaths { get; init; } = []; + + public GameEngineType? Engine { get; init; } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/SettingsBuilder.cs b/src/ModVerify.CliApp/SettingsBuilder.cs deleted file mode 100644 index d2ba861..0000000 --- a/src/ModVerify.CliApp/SettingsBuilder.cs +++ /dev/null @@ -1,191 +0,0 @@ -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.Collections.Generic; -using System.IO; -using System.IO.Abstractions; -using AET.ModVerify.Pipeline; -using AET.ModVerifyTool.Options.CommandLine; -using Microsoft.Extensions.Logging; - -namespace AET.ModVerifyTool; - -internal sealed class SettingsBuilder(IServiceProvider services) -{ - private readonly IFileSystem _fileSystem = services.GetRequiredService(); - private readonly ILogger? _logger = - services.GetRequiredService()?.CreateLogger(typeof(SettingsBuilder)); - - public ModVerifyAppSettings BuildSettings(BaseModVerifyOptions options) - { - switch (options) - { - case VerifyVerbOption verifyVerb: - return BuildFromVerifyVerb(verifyVerb); - case CreateBaselineVerbOption baselineVerb: - return BuildFromCreateBaselineVerb(baselineVerb); - } - throw new NotSupportedException($"The option '{options.GetType().Name}' is not supported!"); - } - - 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 - { - ParallelVerifiers = verifyOptions.Parallel ? 4 : 1, - VerifiersProvider = new DefaultGameVerifiersProvider(), - FailFast = verifyOptions.FailFast, - GameVerifySettings = new GameVerifySettings - { - IgnoreAsserts = verifyOptions.IgnoreAsserts, - ThrowsOnMinimumSeverity = GetVerifierMinimumThrowSeverity() - } - }, - AppThrowsOnMinimumSeverity = verifyOptions.MinimumFailureSeverity, - GameInstallationsSettings = BuildInstallationSettings(verifyOptions), - GlobalReportSettings = BuilderGlobalReportSettings(verifyOptions), - ReportOutput = output, - Offline = verifyOptions.OfflineMode - }; - - VerificationSeverity? GetVerifierMinimumThrowSeverity() - { - var minFailSeverity = verifyOptions.MinimumFailureSeverity; - if (verifyOptions.FailFast) - { - if (minFailSeverity == null) - { - _logger?.LogWarning($"Verification is configured to fail fast but 'minFailSeverity' is not specified. " + - $"Using severity '{VerificationSeverity.Information}'."); - minFailSeverity = VerificationSeverity.Information; - } - - return minFailSeverity; - } - - // Only in a failFast scenario we want the verifier to throw. - // In a normal run, the verifier should simply store the error. - return null; - } - } - - private ModVerifyAppSettings BuildFromCreateBaselineVerb(CreateBaselineVerbOption baselineVerb) - { - return new ModVerifyAppSettings - { - VerifyPipelineSettings = new VerifyPipelineSettings - { - ParallelVerifiers = baselineVerb.Parallel ? 4 : 1, - GameVerifySettings = new GameVerifySettings - { - IgnoreAsserts = false, - ThrowsOnMinimumSeverity = null, - }, - VerifiersProvider = new DefaultGameVerifiersProvider(), - FailFast = false, - }, - AppThrowsOnMinimumSeverity = null, - GameInstallationsSettings = BuildInstallationSettings(baselineVerb), - GlobalReportSettings = BuilderGlobalReportSettings(baselineVerb), - NewBaselinePath = baselineVerb.OutputFile, - ReportOutput = null, - Offline = baselineVerb.OfflineMode - }; - } - - private GlobalVerifyReportSettings BuilderGlobalReportSettings(BaseModVerifyOptions options) - { - return new GlobalVerifyReportSettings - { - Baseline = CreateBaseline(), - Suppressions = CreateSuppressions(), - MinimumReportSeverity = options.MinimumSeverity, - }; - - 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() - { - if (options.Suppressions is null) - return SuppressionList.Empty; - using var fs = _fileSystem.FileStream.New(options.Suppressions, FileMode.Open, FileAccess.Read); - return SuppressionList.FromJson(fs); - } - } - - private GameInstallationsSettings BuildInstallationSettings(BaseModVerifyOptions options) - { - var modPaths = new List(); - if (options.ModPaths is not null) - { - foreach (var mod in options.ModPaths) - { - if (!string.IsNullOrEmpty(mod)) - modPaths.Add(_fileSystem.Path.GetFullPath(mod)); - } - } - - var fallbackPaths = new List(); - if (options.AdditionalFallbackPath is not null) - { - foreach (var fallback in options.AdditionalFallbackPath) - { - if (!string.IsNullOrEmpty(fallback)) - fallbackPaths.Add(_fileSystem.Path.GetFullPath(fallback)); - } - } - - var gamePath = options.GamePath; - if (!string.IsNullOrEmpty(gamePath)) - gamePath = _fileSystem.Path.GetFullPath(gamePath); - - - string? fallbackGamePath = null; - if (!string.IsNullOrEmpty(gamePath) && !string.IsNullOrEmpty(options.FallbackGamePath)) - fallbackGamePath = _fileSystem.Path.GetFullPath(options.FallbackGamePath); - - var autoPath = options.AutoPath; - if (!string.IsNullOrEmpty(autoPath)) - autoPath = _fileSystem.Path.GetFullPath(autoPath); - - return new GameInstallationsSettings - { - AutoPath = autoPath, - ModPaths = modPaths, - GamePath = gamePath, - FallbackGamePath = fallbackGamePath, - AdditionalFallbackPaths = fallbackPaths, - EngineType = options.GameType - }; - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/TargetSelectors/AutomaticSelector.cs b/src/ModVerify.CliApp/TargetSelectors/AutomaticSelector.cs new file mode 100644 index 0000000..5250ec4 --- /dev/null +++ b/src/ModVerify.CliApp/TargetSelectors/AutomaticSelector.cs @@ -0,0 +1,163 @@ +using AET.ModVerify.App.GameFinder; +using AET.ModVerify.App.Settings; +using AET.ModVerify.App.Utilities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Infrastructure; +using PG.StarWarsGame.Infrastructure.Games; +using PG.StarWarsGame.Infrastructure.Mods; +using PG.StarWarsGame.Infrastructure.Services; +using PG.StarWarsGame.Infrastructure.Services.Detection; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO.Abstractions; +using System.Linq; +using AnakinRaW.CommonUtilities.FileSystem; + +namespace AET.ModVerify.App.TargetSelectors; + +internal class AutomaticSelector(IServiceProvider serviceProvider) : VerificationTargetSelectorBase(serviceProvider) +{ + private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); + + internal override SelectionResult SelectTarget(VerificationTargetSettings settings) + { + if (!settings.UseAutoDetection) + throw new ArgumentException("wrong settings format provided.", nameof(settings)); + + var targetPath = settings.TargetPath; + if (!_fileSystem.Directory.Exists(targetPath)) + { + Logger?.LogError(ModVerifyConstants.ConsoleEventId, "The specified path '{Path}' does not exist.", targetPath); + throw new TargetNotFoundException(targetPath); + } + + var engine = settings.Engine; + + GameFinderResult finderResult; + try + { + var finderSettings = new GameFinderSettings + { + Engine = engine, + InitMods = true, + SearchFallbackGame = true + }; + finderResult = GameFinderService.FindGamesFromPathOrGlobal(targetPath, finderSettings); + } + catch (GameNotFoundException) + { + Logger?.LogError(ModVerifyConstants.ConsoleEventId, + "Unable to find games based of the specified target path '{Path}'. Consider specifying all paths manually.", targetPath); + throw; + } + + GameLocations locations; + + var targetObject = GetAttachedModOrGame(finderResult, targetPath, engine); + + if (targetObject is not null) + { + var actualType = targetObject.Game.Type; + Debug.Assert(IsEngineTypeSupported(engine, actualType)); + engine ??= actualType.ToEngineType(); + locations = GetLocations(targetObject, finderResult.FallbackGame, settings.AdditionalFallbackPaths); + } + else + { + if (!engine.HasValue) + throw new ArgumentException("Game engine not specified. Use --engine argument to set it."); + + Logger?.LogDebug("The requested mod at '{TargetPath}' is detached from its games.", targetPath); + + // The path is a detached mod, that exists on a different location than the game. + locations = GetDetachedModLocations(targetPath, finderResult, engine.Value, settings.AdditionalFallbackPaths, out var mod); + targetObject = mod; + } + + return new(locations, engine.Value, targetObject); + } + + private IPhysicalPlayableObject? GetAttachedModOrGame(GameFinderResult finderResult, string targetPath, GameEngineType? requestedEngineType) + { + var targetFullPath = _fileSystem.Path.GetFullPath(targetPath); + + IPhysicalPlayableObject? target = null; + + // If the target is the game directory itself. + if (targetFullPath.Equals(finderResult.Game.Directory.FullName, StringComparison.OrdinalIgnoreCase)) + target = finderResult.Game; + + target ??= GetMatchingModFromGame(finderResult.Game, targetFullPath, requestedEngineType) ?? + GetMatchingModFromGame(finderResult.FallbackGame, targetFullPath, requestedEngineType); + + return target; + } + + private GameLocations GetDetachedModLocations( + string modPath, + GameFinderResult gameResult, + GameEngineType requestedGameEngine, + IReadOnlyList additionalFallbackPaths, + out IPhysicalMod mod) + { + // Because requestedGameEngine must be set, GameFinderService already ensures + // gameResult.Game is the correct type. + var game = gameResult.Game; + var modFinder = ServiceProvider.GetRequiredService(); + var modRef = modFinder.FindMods(game, _fileSystem.DirectoryInfo.New(modPath)).FirstOrDefault(); + + if (modRef is null) + ThrowEngineNotSupported(requestedGameEngine, modPath); + + var modFactory = ServiceProvider.GetRequiredService(); + mod = modFactory.CreatePhysicalMod(game, modRef, CultureInfo.InvariantCulture); + + game.AddMod(mod); + + mod.ResolveDependencies(); + + return GetLocations(mod, gameResult.FallbackGame, additionalFallbackPaths); + } + + private IPhysicalMod? GetMatchingModFromGame(IGame? game, string modPath, GameEngineType? requestedEngineType) + { + if (game is null || !IsEngineTypeSupported(requestedEngineType, game.Type)) + return null; + + foreach (var mod in game.Game.Mods) + { + if (mod is not IPhysicalMod physicalMod) + continue; + + if (_fileSystem.Path.AreEqual(modPath, physicalMod.Directory.FullName)) + return physicalMod; + } + + return null; + } + + private static IGame? GetTargetGame(GameFinderResult finderResult, GameEngineType? requestedEngine) + { + if (finderResult.Game.Type.ToEngineType() == requestedEngine) + return finderResult.Game; + if (finderResult.FallbackGame is not null && finderResult.FallbackGame.Type.ToEngineType() == requestedEngine) + return finderResult.FallbackGame; + return null; + } + + private static bool IsEngineTypeSupported([NotNullWhen(false)] GameEngineType? requestedEngineType, GameType actualGameType) + { + return !requestedEngineType.HasValue || actualGameType.ToEngineType() == requestedEngineType; + } + + [DoesNotReturn] + private static void ThrowEngineNotSupported(GameEngineType requested, string targetPath) + { + throw new ArgumentException($"The specified game engine '{requested}' does not match engine of the verification target '{targetPath}'."); + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectors/ConsoleModSelector.cs b/src/ModVerify.CliApp/TargetSelectors/ConsoleSelector.cs similarity index 74% rename from src/ModVerify.CliApp/ModSelectors/ConsoleModSelector.cs rename to src/ModVerify.CliApp/TargetSelectors/ConsoleSelector.cs index 7a29368..17b9791 100644 --- a/src/ModVerify.CliApp/ModSelectors/ConsoleModSelector.cs +++ b/src/ModVerify.CliApp/TargetSelectors/ConsoleSelector.cs @@ -1,24 +1,25 @@ using System; using System.Collections.Generic; using AET.Modinfo.Spec; -using AET.ModVerifyTool.GameFinder; -using AET.ModVerifyTool.Options; -using PG.StarWarsGame.Engine; +using AET.ModVerify.App.GameFinder; +using AET.ModVerify.App.Settings; +using AET.ModVerify.App.Utilities; +using AnakinRaW.ApplicationBase; using PG.StarWarsGame.Infrastructure; using PG.StarWarsGame.Infrastructure.Games; using PG.StarWarsGame.Infrastructure.Mods; -namespace AET.ModVerifyTool.ModSelectors; +namespace AET.ModVerify.App.TargetSelectors; -internal class ConsoleModSelector(IServiceProvider serviceProvider) : ModSelectorBase(serviceProvider) +internal class ConsoleSelector(IServiceProvider serviceProvider) : VerificationTargetSelectorBase(serviceProvider) { - public override GameLocations Select(GameInstallationsSettings settings, out IPhysicalPlayableObject targetObject, - out GameEngineType? actualEngineType) + internal override SelectionResult SelectTarget(VerificationTargetSettings settings) { - var gameResult = GameFinderService.FindGames(); - targetObject = SelectPlayableObject(gameResult); - actualEngineType = targetObject.Game.Type.ToEngineType(); - return GetLocations(targetObject, gameResult, settings.AdditionalFallbackPaths); + var gameResult = GameFinderService.FindGames(GameFinderSettings.Default); + var targetObject = SelectPlayableObject(gameResult); + var engine = targetObject.Game.Type.ToEngineType(); + var locations = GetLocations(targetObject, gameResult.FallbackGame, settings.AdditionalFallbackPaths); + return new SelectionResult(locations, engine, targetObject); } private static IPhysicalPlayableObject SelectPlayableObject(GameFinderResult finderResult) @@ -29,6 +30,9 @@ private static IPhysicalPlayableObject SelectPlayableObject(GameFinderResult fin list.Add(finderResult.Game); Console.WriteLine(); + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine("Listing Games and Mods:"); + Console.ResetColor(); ConsoleUtilities.WriteHorizontalLine(); Console.WriteLine($"0: {game.Name}"); @@ -100,7 +104,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/TargetSelectors/IVerificationTargetSelector.cs b/src/ModVerify.CliApp/TargetSelectors/IVerificationTargetSelector.cs new file mode 100644 index 0000000..6d452e8 --- /dev/null +++ b/src/ModVerify.CliApp/TargetSelectors/IVerificationTargetSelector.cs @@ -0,0 +1,8 @@ +using AET.ModVerify.App.Settings; + +namespace AET.ModVerify.App.TargetSelectors; + +internal interface IVerificationTargetSelector +{ + VerificationTarget Select(VerificationTargetSettings settings); +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/TargetSelectors/ManualSelector.cs b/src/ModVerify.CliApp/TargetSelectors/ManualSelector.cs new file mode 100644 index 0000000..f963b4f --- /dev/null +++ b/src/ModVerify.CliApp/TargetSelectors/ManualSelector.cs @@ -0,0 +1,73 @@ +using System; +using System.Globalization; +using System.Linq; +using AET.ModVerify.App.GameFinder; +using AET.ModVerify.App.Settings; +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Infrastructure; +using PG.StarWarsGame.Infrastructure.Games; +using PG.StarWarsGame.Infrastructure.Services; +using PG.StarWarsGame.Infrastructure.Services.Detection; + +namespace AET.ModVerify.App.TargetSelectors; + +internal class ManualSelector(IServiceProvider serviceProvider) : VerificationTargetSelectorBase(serviceProvider) +{ + internal override SelectionResult SelectTarget(VerificationTargetSettings settings) + { + if (string.IsNullOrEmpty(settings.GamePath)) + throw new ArgumentException("Argument --game must be set."); + if (!settings.Engine.HasValue) + throw new ArgumentException("Unable to determine game type. Use --engine argument to set the game type."); + + var engine = settings.Engine.Value; + + var gameLocations = new GameLocations( + settings.ModPaths.ToList(), + settings.GamePath!, + GetFallbackPaths(settings.FallbackGamePath, settings.AdditionalFallbackPaths).ToList()); + + + // For the manual selector the whole game and mod detection is optional. + // This allows user to use the application for unusual scenarios, + // not known to the detection service. + if (!GameFinderService.TryFindGame(gameLocations.GamePath, + new GameFinderSettings { Engine = engine, InitMods = false, SearchFallbackGame = false }, + out var game)) + { + // TODO: Log + } + + // If the fallback game path is specified we simply try to detect the game and report a warning to the user if not found. + var fallbackGamePath = settings.FallbackGamePath; + if (!string.IsNullOrEmpty(fallbackGamePath)) + { + + if (!GameFinderService.TryFindGame(fallbackGamePath, + new GameFinderSettings { InitMods = false, SearchFallbackGame = false }, + out _)) + { + // TODO: Log + } + } + + var target = TryGetPlayableObject(game, gameLocations.ModPaths.FirstOrDefault()); + return new SelectionResult(gameLocations, engine, target); + } + + private IPhysicalPlayableObject? TryGetPlayableObject(IGame? game, string? modPath) + { + if (game is null) + return null; + if (string.IsNullOrEmpty(modPath)) + return game; + + var modFinder = ServiceProvider.GetRequiredService(); + var modFactory = ServiceProvider.GetRequiredService(); + + var mods = modFinder.FindMods(game, FileSystem.DirectoryInfo.New(modPath)); + var mod = modFactory.CreatePhysicalMod(game, mods.First(), CultureInfo.InvariantCulture); + return mod; + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/TargetSelectors/TargetNotFoundException.cs b/src/ModVerify.CliApp/TargetSelectors/TargetNotFoundException.cs new file mode 100644 index 0000000..d6b052e --- /dev/null +++ b/src/ModVerify.CliApp/TargetSelectors/TargetNotFoundException.cs @@ -0,0 +1,6 @@ +using System.IO; + +namespace AET.ModVerify.App.TargetSelectors; + +internal class TargetNotFoundException(string path) + : DirectoryNotFoundException($"The target path '{path}' does not exist"); \ No newline at end of file diff --git a/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorBase.cs b/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorBase.cs new file mode 100644 index 0000000..68f0f39 --- /dev/null +++ b/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorBase.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Linq; +using System.Runtime.InteropServices; +using AET.ModVerify.App.GameFinder; +using AET.ModVerify.App.Settings; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Infrastructure; +using PG.StarWarsGame.Infrastructure.Clients; +using PG.StarWarsGame.Infrastructure.Clients.Utilities; +using PG.StarWarsGame.Infrastructure.Games; +using PG.StarWarsGame.Infrastructure.Mods; +using PG.StarWarsGame.Infrastructure.Services.Dependencies; + +namespace AET.ModVerify.App.TargetSelectors; + +internal abstract class VerificationTargetSelectorBase : IVerificationTargetSelector +{ + internal sealed record SelectionResult( + GameLocations Locations, + GameEngineType Engine, + IPhysicalPlayableObject? Target); + + protected readonly ILogger? Logger; + protected readonly GameFinderService GameFinderService; + protected readonly IServiceProvider ServiceProvider; + protected readonly IFileSystem FileSystem; + + protected VerificationTargetSelectorBase(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + Logger = serviceProvider.GetService()?.CreateLogger(GetType()); + GameFinderService = new GameFinderService(serviceProvider); + FileSystem = serviceProvider.GetRequiredService(); + } + + public VerificationTarget Select(VerificationTargetSettings settings) + { + var selectedTarget = SelectTarget(settings); + + return new VerificationTarget + { + Location = selectedTarget.Locations, + Engine = selectedTarget.Engine, + Name = GetTargetName(selectedTarget.Target, selectedTarget.Locations), + Version = GetTargetVersion(selectedTarget.Target) + }; + } + + + internal abstract SelectionResult SelectTarget(VerificationTargetSettings settings); + + + protected GameLocations GetLocations( + IPhysicalPlayableObject target, + IGame? fallbackGame, + IReadOnlyList additionalFallbackPaths) + { + var fallbacks = GetFallbackPaths(target, fallbackGame, additionalFallbackPaths); + var modPaths = GetModPaths(target); + return new GameLocations(modPaths, target.Game.Directory.FullName, fallbacks); + } + + private static IReadOnlyList GetFallbackPaths(IPhysicalPlayableObject target, IGame? fallbackGame, IReadOnlyList additionalFallbackPaths) + { + var coercedFallbackGame = fallbackGame; + if (target is IGame tGame && tGame.Equals(fallbackGame)) + coercedFallbackGame = null; + else if (target.Game.Equals(fallbackGame)) + coercedFallbackGame = null; + return GetFallbackPaths(coercedFallbackGame?.Directory.FullName, additionalFallbackPaths); + } + + + protected static IReadOnlyList GetFallbackPaths(string? fallbackGame, IReadOnlyList additionalFallbackPaths) + { + var fallbacks = new List(); + if (fallbackGame is not null) + fallbacks.Add(fallbackGame); + foreach (var fallback in additionalFallbackPaths) + fallbacks.Add(fallback); + return fallbacks; + } + + + private IReadOnlyList GetModPaths(IPhysicalPlayableObject modOrGame) + { + if (modOrGame is not IMod mod) + return []; + + var traverser = ServiceProvider.GetRequiredService(); + return traverser.Traverse(mod) + .OfType().Select(x => x.Directory.FullName) + .ToList(); + } + + protected static string GetTargetName(IPhysicalPlayableObject? targetObject, GameLocations gameLocations) + { + if (targetObject is not null) + return targetObject.Name; + + // TODO: Reuse name beautifier from GameInfrastructure lib + var mod = gameLocations.ModPaths.FirstOrDefault(); + return mod ?? gameLocations.GamePath; + } + + protected string? GetTargetVersion(IPhysicalPlayableObject? targetObject) + { + return targetObject switch + { + IMod mod => mod.Version?.ToString(), + IGame game => GetVersionFromGame(game), + _ => null + }; + } + + private string? GetVersionFromGame(IGame game) + { + var exeFile = GameExecutableFileUtilities.GetExecutableForGame(game, GameBuildType.Release); + if (exeFile is null) + { + Logger?.LogWarning(ModVerifyConstants.ConsoleEventId, + "Unable to get game version of target path '{Path}'. Is this a game directory?", + game.Directory.FullName); + return null; + } + + var versionInfo = FileSystem.FileVersionInfo.GetVersionInfo(exeFile.FullName); + var version = + $"{versionInfo.FileMajorPart}.{versionInfo.FileMinorPart}.{versionInfo.FileBuildPart}.{versionInfo.FilePrivatePart}"; + return version; + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorFactory.cs b/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorFactory.cs new file mode 100644 index 0000000..5e106eb --- /dev/null +++ b/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorFactory.cs @@ -0,0 +1,18 @@ +using System; +using AET.ModVerify.App.Settings; + +namespace AET.ModVerify.App.TargetSelectors; + +internal sealed class VerificationTargetSelectorFactory(IServiceProvider serviceProvider) +{ + public IVerificationTargetSelector CreateSelector(VerificationTargetSettings settings) + { + if (settings.Interactive) + return new ConsoleSelector(serviceProvider); + if (settings.UseAutoDetection) + return new AutomaticSelector(serviceProvider); + if (settings.ManualSetup) + return new ManualSelector(serviceProvider); + throw new ArgumentException("Unknown option configuration provided."); + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/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..fc07469 --- /dev/null +++ b/src/ModVerify.CliApp/Utilities/ExtensionMethods.cs @@ -0,0 +1,48 @@ +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 +{ + extension(GameEngineType type) + { + public GameType FromEngineType() + { + return (GameType)(int)type; + } + + public GameEngineType Opposite() + { + return (GameEngineType)((int)type ^ 1); + } + } + + public static GameEngineType ToEngineType(this GameType type) + { + return (GameEngineType)(int)type; + } + 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..b9ebff0 --- /dev/null +++ b/src/ModVerify.CliApp/Utilities/ModVerifyConsoleUtilities.cs @@ -0,0 +1,90 @@ +using AnakinRaW.ApplicationBase; +using Figgle; +using System; +using System.Collections.Generic; +using AET.ModVerify.Reporting; + +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(); + } + + public static void WriteSelectedTarget(VerificationTarget target) + { + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine("Selected Target:"); + Console.ForegroundColor = ConsoleColor.DarkGray; + ConsoleUtilities.PrintAsTable([ + ("Name", target.Name), + ("Type", target.IsGame ? "Game" : "Mod"), + ("Engine", target.Engine), + ("Version", target.Version ?? "n/a"), + ("Location", target.Location.TargetPath), + ], 120); + Console.ResetColor(); + } + + public static void WriteBaselineInfo(VerificationBaseline baseline, string? filePath) + { + if (baseline.IsEmpty) + return; + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine("Using Baseline:"); + Console.ForegroundColor = ConsoleColor.DarkGray; + + IList<(string, object)> baselineData = + [ + ("Version", baseline.Version?.ToString(2) ?? "n/a"), + ("Is Default", filePath is null), + ("Minimum Severity", baseline.MinimumSeverity.ToString()), + ("Entries", baseline.Count.ToString()) + ]; + if (!string.IsNullOrEmpty(filePath)) + baselineData.Add(("File Path", filePath)); + + ConsoleUtilities.PrintAsTable(baselineData, 120); + + if (baseline.Target is not null) + { + Console.ForegroundColor = ConsoleColor.DarkMagenta; + Console.WriteLine("Baseline Target:"); + Console.ForegroundColor = ConsoleColor.DarkGray; + + IList<(string, object)> targetData = [ + ("Name", baseline.Target.Name), + ("Type", baseline.Target.IsGame ? "Game" : "Mod"), + ("Engine", baseline.Target.Engine), + ("Version", baseline.Target.Version ?? "n/a"), + ]; + + if (baseline.Target.Location is not null) + targetData.Add(("Location", baseline.Target.Location.TargetPath)); + + ConsoleUtilities.PrintAsTable(targetData, 120); + } + Console.ResetColor(); + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Utilities/PathUtilities.cs b/src/ModVerify.CliApp/Utilities/PathUtilities.cs new file mode 100644 index 0000000..561630f --- /dev/null +++ b/src/ModVerify.CliApp/Utilities/PathUtilities.cs @@ -0,0 +1,39 @@ +using System; +using System.Runtime.InteropServices; +using AnakinRaW.CommonUtilities.FileSystem.Normalization; + +namespace AET.ModVerify.App.Utilities; + +internal static class PathUtilities +{ + private static readonly string HomeVariable; + private static readonly string HomePath; + private static readonly StringComparison StringComparer; + + static PathUtilities() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + HomeVariable = "%USERPROFILE%"; + StringComparer = StringComparison.OrdinalIgnoreCase; + } + else + { + HomeVariable = "$HOME"; + StringComparer = StringComparison.Ordinal; + } + + HomePath = PathNormalizer.Normalize( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + PathNormalizeOptions.EnsureTrailingSeparator); + } + + internal static string MaskUsername(string path) + { + if (string.IsNullOrWhiteSpace(path)) + return path; + + var index = path.IndexOf(HomePath, StringComparer); + return index >= 0 ? path.Remove(index, HomePath.Length).Insert(index, HomeVariable) : path; + } +} \ No newline at end of file diff --git a/src/ModVerify.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.CliApp/VerifyInstallationInformation.cs b/src/ModVerify.CliApp/VerifyInstallationInformation.cs deleted file mode 100644 index 66816ab..0000000 --- a/src/ModVerify.CliApp/VerifyInstallationInformation.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Text; -using PG.StarWarsGame.Engine; - -namespace AET.ModVerifyTool; - -internal sealed class VerifyInstallationInformation -{ - public required string Name { get; init; } - - public required GameEngineType EngineType { get; init; } - - public required GameLocations GameLocations { get; init; } - - public override string ToString() - { - var sb = new StringBuilder(); - - sb.AppendLine($"ObjectToVerify={Name};EngineType={EngineType};Locations=["); - if (GameLocations.ModPaths.Count > 0) - sb.AppendLine($"Mods=[{string.Join(";", GameLocations.ModPaths)}];"); - sb.AppendLine($"Game=[{GameLocations.GamePath}];"); - if (GameLocations.FallbackPaths.Count > 0) - sb.AppendLine($"Fallbacks=[{string.Join(";", GameLocations.FallbackPaths)}];"); - sb.AppendLine("]"); - - return sb.ToString(); - } -} \ No newline at end of file diff --git a/src/ModVerify/GameVerificationException.cs b/src/ModVerify/GameVerificationException.cs index 1096179..dba312b 100644 --- a/src/ModVerify/GameVerificationException.cs +++ b/src/ModVerify/GameVerificationException.cs @@ -7,23 +7,21 @@ namespace AET.ModVerify; public sealed class GameVerificationException : Exception { - private readonly string? _errorMessage = null; - public IReadOnlyCollection Errors { get; } private string ErrorMessage { get { - if (_errorMessage != null) - return _errorMessage; + if (field != null) + return field; var stringBuilder = new StringBuilder(); foreach (var error in Errors) stringBuilder.AppendLine($"Verification error: {error.Id}: {error.Message};"); return stringBuilder.ToString().TrimEnd(';'); } - } + } = null; /// public override string Message => ErrorMessage; diff --git a/src/ModVerify/ModVerify.csproj b/src/ModVerify/ModVerify.csproj index fecceb2..f1415bc 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 - - @@ -45,7 +52,7 @@ - + diff --git a/src/ModVerify/Pipeline/GameVerifierPipelineStep.cs b/src/ModVerify/Pipeline/GameVerifierPipelineStep.cs index a27ee9b..e83cb25 100644 --- a/src/ModVerify/Pipeline/GameVerifierPipelineStep.cs +++ b/src/ModVerify/Pipeline/GameVerifierPipelineStep.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using System; using System.Threading; +using System.Threading.Tasks; using AET.ModVerify.Pipeline.Progress; namespace AET.ModVerify.Pipeline; @@ -22,18 +23,19 @@ public sealed class GameVerifierPipelineStep( public long Size => 1; - protected override void RunCore(CancellationToken token) + protected override Task RunCoreAsync(CancellationToken token) { try { - Logger?.LogDebug($"Running verifier '{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")); + return Task.CompletedTask; } finally { diff --git a/src/ModVerify/Pipeline/GameVerifyPipeline.cs b/src/ModVerify/Pipeline/GameVerifyPipeline.cs index b7ac00d..493aa8c 100644 --- a/src/ModVerify/Pipeline/GameVerifyPipeline.cs +++ b/src/ModVerify/Pipeline/GameVerifyPipeline.cs @@ -1,7 +1,6 @@ -using AET.ModVerify.Reporting; -using AET.ModVerify.Reporting.Settings; +using AET.ModVerify.Pipeline.Progress; +using AET.ModVerify.Reporting; using AET.ModVerify.Settings; -using AET.ModVerify.Utilities; using AET.ModVerify.Verifiers; using AnakinRaW.CommonUtilities.SimplePipeline; using AnakinRaW.CommonUtilities.SimplePipeline.Runners; @@ -12,112 +11,127 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using AET.ModVerify.Pipeline.Progress; +using AET.ModVerify.Utilities; +using Microsoft.Extensions.DependencyInjection; namespace AET.ModVerify.Pipeline; -public sealed class GameVerifyPipeline : AnakinRaW.CommonUtilities.SimplePipeline.Pipeline +public sealed class GameVerifyPipeline : StepRunnerPipelineBase { - private readonly List _verifiers = new(); - private readonly List _verificationSteps = new(); - private readonly StepRunnerBase _verifyRunner; - - private readonly IStarWarsGameEngine _gameEngine; - private readonly IGameEngineErrorCollection _engineErrors; + private readonly List _verifiers = []; + private readonly List _verificationSteps = []; + private readonly ConcurrentGameEngineErrorReporter _engineErrorReporter = new(); + private readonly VerificationTarget _verificationTarget; private readonly VerifyPipelineSettings _pipelineSettings; - private readonly GlobalVerifyReportSettings _reportSettings; - private readonly IVerifyProgressReporter _progressReporter; - - protected override bool FailFast { get; } + private readonly IGameEngineInitializationReporter? _engineInitializationReporter; + private readonly IPetroglyphStarWarsGameEngineService _gameEngineService; + private readonly ILogger? _logger; + private AggregatedVerifyProgressReporter? _aggregatedVerifyProgressReporter; public IReadOnlyCollection FilteredErrors { get; private set; } = []; - + public VerificationBaseline Baseline { get; } + public SuppressionList Suppressions { get; } + public GameVerifyPipeline( - IStarWarsGameEngine gameEngine, - IGameEngineErrorCollection engineErrors, - VerifyPipelineSettings pipelineSettings, - GlobalVerifyReportSettings reportSettings, + VerificationTarget verificationTarget, + VerifyPipelineSettings pipelineSettings, IVerifyProgressReporter progressReporter, + IGameEngineInitializationReporter? engineInitializationReporter, + VerificationBaseline baseline, + SuppressionList suppressions, IServiceProvider serviceProvider) : base(serviceProvider) { - _gameEngine = gameEngine ?? throw new ArgumentNullException(nameof(gameEngine)); - _engineErrors = engineErrors ?? throw new ArgumentNullException(nameof(gameEngine)); + Baseline = baseline ?? throw new ArgumentNullException(nameof(baseline)); + Suppressions = suppressions ?? throw new ArgumentNullException(nameof(suppressions)); + _verificationTarget = verificationTarget ?? throw new ArgumentNullException(nameof(verificationTarget)); _pipelineSettings = pipelineSettings ?? throw new ArgumentNullException(nameof(pipelineSettings)); - _reportSettings = reportSettings ?? throw new ArgumentNullException(nameof(reportSettings)); _progressReporter = progressReporter ?? throw new ArgumentNullException(nameof(progressReporter)); + _engineInitializationReporter = engineInitializationReporter; + _gameEngineService = serviceProvider.GetRequiredService(); + _logger = serviceProvider.GetService()?.CreateLogger(GetType()); - if (pipelineSettings.ParallelVerifiers is < 0 or > 64) - throw new ArgumentException("_pipelineSettings has invalid parallel worker number.", nameof(pipelineSettings)); - - if (pipelineSettings.ParallelVerifiers == 1) - _verifyRunner = new SequentialStepRunner(serviceProvider); - else - _verifyRunner = new ParallelStepRunner(pipelineSettings.ParallelVerifiers, serviceProvider); + FailFast = pipelineSettings.FailFastSettings.IsFailFast; + } - FailFast = pipelineSettings.FailFast; + protected override AsyncStepRunner CreateRunner() + { + var requestedRunnerCount = _pipelineSettings.ParallelVerifiers; + return requestedRunnerCount switch + { + < 0 or > 64 => throw new InvalidOperationException( + $"Invalid parallel worker count ({requestedRunnerCount}) specified in verifier settings."), + 1 => new SequentialStepRunner(ServiceProvider), + _ => new AsyncStepRunner(requestedRunnerCount, ServiceProvider) + }; } - protected override Task PrepareCoreAsync() + protected override async Task PrepareCoreAsync(CancellationToken token) { _verifiers.Clear(); - AddStep(new GameEngineErrorCollector(_engineErrors, _gameEngine, _pipelineSettings.GameVerifySettings, ServiceProvider)); - - foreach (var gameVerificationStep in CreateVerificationSteps(_gameEngine)) - AddStep(gameVerificationStep); - - return Task.FromResult(true); - } - - protected override async Task RunCoreAsync(CancellationToken token) - { - var aggregatedVerifyProgressReporter = new AggregatedVerifyProgressReporter(_progressReporter, _verificationSteps); + IStarWarsGameEngine gameEngine; try { - Logger?.LogInformation("Running game verifiers..."); - _verifyRunner.Error += OnError; - await _verifyRunner.RunAsync(token); + gameEngine = await _gameEngineService.InitializeAsync( + _verificationTarget.Engine, + _verificationTarget.Location, + _engineErrorReporter, + _engineInitializationReporter, + false, + CancellationToken.None).ConfigureAwait(false); } - finally + catch (Exception e) { - aggregatedVerifyProgressReporter.Dispose(); - _verifyRunner.Error -= OnError; - Logger?.LogDebug("Game verifiers finished."); + _logger?.LogError(e, "Creating game engine failed: {Message}", e.Message); + throw; } - token.ThrowIfCancellationRequested(); - - var failedSteps = _verifyRunner.ExecutedSteps.Where(p => - p.Error != null && !p.Error.IsExceptionType()).ToList(); + AddStep(new GameEngineErrorCollector(_engineErrorReporter, gameEngine, _pipelineSettings.GameVerifySettings, ServiceProvider)); - if (failedSteps.Count != 0) - throw new StepFailureException(failedSteps); + foreach (var gameVerificationStep in CreateVerificationSteps(gameEngine)) + AddStep(gameVerificationStep); + } + protected override void OnExecuteStarted() + { + Logger?.LogInformation("Running game verifiers..."); + _aggregatedVerifyProgressReporter = new AggregatedVerifyProgressReporter(_progressReporter, _verificationSteps); + _progressReporter.Report(0.0, $"Verifying {_verificationTarget.Name}...", VerifyProgress.ProgressType, default); + } + + protected override void OnExecuteCompleted() + { + Logger?.LogInformation("Game verifiers finished."); FilteredErrors = GetReportableErrors(_verifiers.SelectMany(s => s.VerifyErrors)).ToList(); + _progressReporter.Report(1.0, $"Finished Verifying {_verificationTarget.Name}", VerifyProgress.ProgressType, default); } - protected override void OnError(object sender, StepRunnerErrorEventArgs e) + protected override void OnRunnerExecutionError(object sender, StepRunnerErrorEventArgs e) { - if (FailFast && e.Exception is GameVerificationException v) + if (FailFast && e.Exception is GameVerificationException verificationException) { - if (v.Errors.All(error => _reportSettings.Baseline.Contains(error) || _reportSettings.Suppressions.Suppresses(error))) + var minSeverity = _pipelineSettings.FailFastSettings.MinumumSeverity; + var ignoreError = verificationException.Errors + .Where(error => error.Severity >= minSeverity) + .All(error => Baseline.Contains(error) || Suppressions.Suppresses(error)); + if (ignoreError) return; } - base.OnError(sender, e); + base.OnRunnerExecutionError(sender, e); } - private IEnumerable CreateVerificationSteps(IStarWarsGameEngine database) + protected override IEnumerable GetFailedSteps(IEnumerable steps) { - return _pipelineSettings.VerifiersProvider.GetVerifiers(database, _pipelineSettings.GameVerifySettings, ServiceProvider); + return base.GetFailedSteps(steps).Where(s => s.Error is not GameVerificationException); } private void AddStep(GameVerifier verifier) { var verificationStep = new GameVerifierPipelineStep(verifier, ServiceProvider); - _verifyRunner.AddStep(verificationStep); + StepRunner.AddStep(verificationStep); _verificationSteps.Add(verificationStep); _verifiers.Add(verifier); } @@ -127,7 +141,18 @@ private IEnumerable GetReportableErrors(IEnumerable CreateVerificationSteps(IStarWarsGameEngine engine) + { + return _pipelineSettings.VerifiersProvider + .GetVerifiers(engine, _pipelineSettings.GameVerifySettings, ServiceProvider); + } + + protected override void DisposeResources() + { + base.DisposeResources(); + _aggregatedVerifyProgressReporter?.Dispose(); } } \ No newline at end of file diff --git a/src/ModVerify/Pipeline/Progress/AggregatedVerifyProgressReporter.cs b/src/ModVerify/Pipeline/Progress/AggregatedVerifyProgressReporter.cs index 361febe..cfdae25 100644 --- a/src/ModVerify/Pipeline/Progress/AggregatedVerifyProgressReporter.cs +++ b/src/ModVerify/Pipeline/Progress/AggregatedVerifyProgressReporter.cs @@ -59,6 +59,7 @@ protected override ProgressEventArgs CalculateAggregatedProg var progressInfo = new VerifyProgressInfo { TotalVerifiers = TotalStepCount, + IsDetailed = true }; return new ProgressEventArgs(totalProgress, progress.ProgressText, progressInfo); } diff --git a/src/ModVerify/Pipeline/Progress/VerifyProgressInfo.cs b/src/ModVerify/Pipeline/Progress/VerifyProgressInfo.cs index 1409239..cadeb0d 100644 --- a/src/ModVerify/Pipeline/Progress/VerifyProgressInfo.cs +++ b/src/ModVerify/Pipeline/Progress/VerifyProgressInfo.cs @@ -4,5 +4,5 @@ public struct VerifyProgressInfo { public bool IsDetailed { get; init; } - public int TotalVerifiers { get; internal set; } + public int TotalVerifiers { get; internal init; } } \ No newline at end of file diff --git a/src/ModVerify/Reporting/BaselineVerificationTarget.cs b/src/ModVerify/Reporting/BaselineVerificationTarget.cs new file mode 100644 index 0000000..6e21ddd --- /dev/null +++ b/src/ModVerify/Reporting/BaselineVerificationTarget.cs @@ -0,0 +1,23 @@ +using System.Text; +using PG.StarWarsGame.Engine; + +namespace AET.ModVerify.Reporting; + +public sealed class BaselineVerificationTarget +{ + public required GameEngineType Engine { get; init; } + public required string Name { get; init; } + public GameLocations? Location { get; init; } // Optional compared to Verification Target + public string? Version { get; init; } + public bool IsGame { get; init; } + + public override string ToString() + { + var sb = new StringBuilder($"[Name={Name};EngineType={Engine};IsGame={IsGame};"); + if (!string.IsNullOrEmpty(Version)) sb.Append($"Version={Version};"); + if (Location is not null) + sb.Append($"Location={Location};"); + sb.Append(']'); + return sb.ToString(); + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/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..669ef53 --- /dev/null +++ b/src/ModVerify/Reporting/Json/JsonBaselineParser.cs @@ -0,0 +1,36 @@ +using System; +using System.IO; +using System.Text.Json; + +namespace AET.ModVerify.Reporting.Json; + +internal static class JsonBaselineParser +{ + public static VerificationBaseline Parse(Stream dataStream) + { + if (dataStream == null) + throw new ArgumentNullException(nameof(dataStream)); + try + { + var jsonNode = JsonDocument.Parse(dataStream); + var jsonBaseline = EvaluateAndDeserialize(jsonNode); + + if (jsonBaseline is null) + throw new InvalidBaselineException($"Unable to parse input from stream to {nameof(VerificationBaseline)}. Unknown Error!"); + + return new VerificationBaseline(jsonBaseline); + } + catch (JsonException cause) + { + throw new InvalidBaselineException(cause.Message, cause); + } + } + + private static JsonVerificationBaseline? EvaluateAndDeserialize(JsonDocument? json) + { + if (json is null) + return null; + JsonBaselineSchema.Evaluate(json.RootElement); + return json.Deserialize(); + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonBaselineSchema.cs b/src/ModVerify/Reporting/Json/JsonBaselineSchema.cs new file mode 100644 index 0000000..12e3705 --- /dev/null +++ b/src/ModVerify/Reporting/Json/JsonBaselineSchema.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Text.Json; +using Json.Schema; +using Json.Schema.Keywords; + +namespace AET.ModVerify.Reporting.Json; + +public static class JsonBaselineSchema +{ + private static readonly JsonSchema Schema; + private static readonly EvaluationOptions EvaluationOptions; + private static readonly BuildOptions BuildOptions; + + static JsonBaselineSchema() + { + BuildOptions = new BuildOptions + { + Dialect = Dialect.Draft202012 + }; + + Schema = GetCurrentSchema(); + EvaluationOptions = new EvaluationOptions + { + OutputFormat = OutputFormat.Hierarchical + }; + } + + /// + /// Evaluates a JSON node against the ModVerify Baseline JSON schema. + /// + /// The JSON node to evaluate. + /// is not valid against the baseline JSON schema. + public static void Evaluate(JsonElement 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.Errors is not null) + return result.Errors.First(); + + if (result.Details is not null) + { + 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 json = JsonDocument.Parse(resourceStream!).RootElement; + var schema = JsonSchema.Build(json, BuildOptions); + + + if (schema.Root.Keywords.FirstOrDefault(x => x.Handler is IdKeyword)?.Value is not Uri id + || !UriContainsVersion(id, VerificationBaseline.LatestVersionString)) + throw new InvalidOperationException("Internal error: The embedded schema version does not match the expected baseline version!"); + + return schema; + } + + 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/JsonGameLocation.cs b/src/ModVerify/Reporting/Json/JsonGameLocation.cs new file mode 100644 index 0000000..bd51993 --- /dev/null +++ b/src/ModVerify/Reporting/Json/JsonGameLocation.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; +using PG.StarWarsGame.Engine; + +namespace AET.ModVerify.Reporting.Json; + +internal class JsonGameLocation +{ + [JsonPropertyName("modPaths")] + public IReadOnlyList ModPaths { get; } + + [JsonPropertyName("gamePath")] + public string GamePath { get; } + + [JsonPropertyName("fallbackPaths")] + public IReadOnlyList FallbackPaths { get; } + + [JsonConstructor] + private JsonGameLocation(IReadOnlyList modPaths, string gamePath, IReadOnlyList fallbackPaths) + { + ModPaths = modPaths; + GamePath = gamePath; + FallbackPaths = fallbackPaths; + } + + public JsonGameLocation(GameLocations location) + { + ModPaths = location.ModPaths.ToArray(); + GamePath = location.GamePath; + FallbackPaths = location.FallbackPaths.ToArray(); + } + + public static GameLocations? ToLocation(JsonGameLocation? jsonLocation) + { + return jsonLocation is null + ? null + : new GameLocations(jsonLocation.ModPaths, jsonLocation.GamePath, jsonLocation.FallbackPaths); + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonVerificationBaseline.cs b/src/ModVerify/Reporting/Json/JsonVerificationBaseline.cs index 4688c98..0d9a1b7 100644 --- a/src/ModVerify/Reporting/Json/JsonVerificationBaseline.cs +++ b/src/ModVerify/Reporting/Json/JsonVerificationBaseline.cs @@ -10,6 +10,10 @@ internal class JsonVerificationBaseline [JsonPropertyName("version")] public Version? Version { get; } + [JsonPropertyName("target")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public JsonVerificationTarget? Target { get; } + [JsonPropertyName("minSeverity")] [JsonConverter(typeof(JsonStringEnumConverter))] public VerificationSeverity MinimumSeverity { get; } @@ -22,11 +26,17 @@ public JsonVerificationBaseline(VerificationBaseline baseline) Errors = baseline.Select(x => new JsonVerificationError(x)); Version = baseline.Version; MinimumSeverity = baseline.MinimumSeverity; + Target = baseline.Target is not null ? new JsonVerificationTarget(baseline.Target) : null; } [JsonConstructor] - private JsonVerificationBaseline(Version version, VerificationSeverity minimumSeverity, IEnumerable errors) + private JsonVerificationBaseline( + JsonVerificationTarget target, + Version version, + VerificationSeverity minimumSeverity, + IEnumerable errors) { + Target = target; Errors = errors; Version = version; MinimumSeverity = minimumSeverity; diff --git a/src/ModVerify/Reporting/Json/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/Json/JsonVerificationTarget.cs b/src/ModVerify/Reporting/Json/JsonVerificationTarget.cs new file mode 100644 index 0000000..9495456 --- /dev/null +++ b/src/ModVerify/Reporting/Json/JsonVerificationTarget.cs @@ -0,0 +1,63 @@ +using System.Text.Json.Serialization; +using PG.StarWarsGame.Engine; + +namespace AET.ModVerify.Reporting.Json; + +internal class JsonVerificationTarget +{ + [JsonPropertyName("name")] + public string Name { get; } + + [JsonPropertyName("engine")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public GameEngineType Engine { get; } + + [JsonPropertyName("isGame")] + public bool IsGame { get; } + + [JsonPropertyName("version")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Version{ get; } + + [JsonPropertyName("location")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public JsonGameLocation? Location { get; } + + [JsonConstructor] + private JsonVerificationTarget( + string name, + string? version, + JsonGameLocation? location, + GameEngineType engine, + bool isGame) + { + Name = name; + Version = version; + Engine = engine; + Location = location; + IsGame = isGame; + } + + public JsonVerificationTarget(BaselineVerificationTarget target) + { + Name = target.Name; + Version = target.Version; + Engine = target.Engine; + Location = target.Location is null ? null : new JsonGameLocation(target.Location); + IsGame = target.IsGame; + } + + public static BaselineVerificationTarget? ToTarget(JsonVerificationTarget? jsonTarget) + { + if (jsonTarget is null) + return null; + return new BaselineVerificationTarget + { + Engine = jsonTarget.Engine, + Name = jsonTarget.Name, + Location = JsonGameLocation.ToLocation(jsonTarget.Location), + Version = jsonTarget.Version, + IsGame = jsonTarget.IsGame + }; + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/ConsoleReporter.cs b/src/ModVerify/Reporting/Reporters/ConsoleReporter.cs index 7ee435d..dbee50d 100644 --- a/src/ModVerify/Reporting/Reporters/ConsoleReporter.cs +++ b/src/ModVerify/Reporting/Reporters/ConsoleReporter.cs @@ -7,10 +7,10 @@ namespace AET.ModVerify.Reporting.Reporters; internal class ConsoleReporter( - VerifyReportSettings settings, + ReporterSettings settings, bool summaryOnly, IServiceProvider serviceProvider) : - ReporterBase(settings, serviceProvider) + ReporterBase(settings, serviceProvider) { public override Task ReportAsync(IReadOnlyCollection errors) { diff --git a/src/ModVerify/Reporting/Reporters/Engine/EngineErrorReporterBase.cs b/src/ModVerify/Reporting/Reporters/Engine/EngineErrorReporterBase.cs index 5ecbb6f..455212d 100644 --- a/src/ModVerify/Reporting/Reporters/Engine/EngineErrorReporterBase.cs +++ b/src/ModVerify/Reporting/Reporters/Engine/EngineErrorReporterBase.cs @@ -38,12 +38,13 @@ public ErrorData(string identifier, string message, IEnumerable context, ThrowHelper.ThrowIfNullOrEmpty(message); Identifier = identifier; Message = message; - Context = context; + Context = context ?? throw new ArgumentNullException(nameof(context)); Asset = asset; Severity = severity; } - public ErrorData(string identifier, string message, string asset, VerificationSeverity severity) : this(identifier, message, [], asset, severity) + public ErrorData(string identifier, string message, string asset, VerificationSeverity severity) + : this(identifier, message, [], asset, severity) { } } diff --git a/src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs b/src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs index a2a00c5..6c5fb17 100644 --- a/src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs +++ b/src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs @@ -15,16 +15,9 @@ internal sealed class GameAssertErrorReporter(IGameRepository gameRepository, IS protected override ErrorData CreateError(EngineAssert assert) { var context = new List(); - - if (assert.Value is not null) - context.Add($"value='{assert.Value}'"); - if (assert.Context is not null) - context.Add($"context='{assert.Context}'"); - - // The location is the only identifiable thing of an assert. 'Value' might be null, thus we cannot use it. - var asset = GetLocation(assert); - - return new ErrorData(GetIdFromError(assert.Kind), assert.Message, asset, VerificationSeverity.Warning); + context.AddRange(assert.Context); + context.Add($"location='{GetLocation(assert)}'"); + return new ErrorData(GetIdFromError(assert.Kind), assert.Message, context, assert.Value, VerificationSeverity.Warning); } private static string GetLocation(EngineAssert assert) diff --git a/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs b/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs index 941a9d5..348fbb1 100644 --- a/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs +++ b/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs @@ -12,7 +12,6 @@ internal class JsonReporter(JsonReporterSettings settings, IServiceProvider serv { public const string FileName = "VerificationResult.json"; - public override async Task ReportAsync(IReadOnlyCollection errors) { var report = new JsonVerificationReport(errors.Select(x => new JsonVerificationError(x))); diff --git a/src/ModVerify/Reporting/Reporters/ReporterBase.cs b/src/ModVerify/Reporting/Reporters/ReporterBase.cs index fd86119..df444e3 100644 --- a/src/ModVerify/Reporting/Reporters/ReporterBase.cs +++ b/src/ModVerify/Reporting/Reporters/ReporterBase.cs @@ -6,7 +6,7 @@ namespace AET.ModVerify.Reporting.Reporters; -public abstract class ReporterBase(T settings, IServiceProvider serviceProvider) : IVerificationReporter where T : VerifyReportSettings +public abstract class ReporterBase(T settings, IServiceProvider serviceProvider) : IVerificationReporter where T : ReporterSettings { protected IServiceProvider ServiceProvider { get; } = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); diff --git a/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs b/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs index 601e424..b8906ac 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 serviceCollection.RegisterJsonReporter(new JsonReporterSettings + { + OutputDirectory = "." + }); + } - public static IServiceCollection RegisterTextFileReporter(this IServiceCollection serviceCollection) - { - return RegisterTextFileReporter(serviceCollection, new TextFileReporterSettings + public IServiceCollection RegisterTextFileReporter() { - OutputDirectory = "." - }); - } + return serviceCollection.RegisterTextFileReporter(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 serviceCollection.RegisterConsoleReporter(new ReporterSettings + { + 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(ReporterSettings 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/Settings/FileBasedReporterSettings.cs b/src/ModVerify/Reporting/Settings/FileBasedReporterSettings.cs index c6233a1..759a6ab 100644 --- a/src/ModVerify/Reporting/Settings/FileBasedReporterSettings.cs +++ b/src/ModVerify/Reporting/Settings/FileBasedReporterSettings.cs @@ -2,13 +2,11 @@ namespace AET.ModVerify.Reporting.Settings; -public record FileBasedReporterSettings : VerifyReportSettings +public record FileBasedReporterSettings : ReporterSettings { - private readonly string _outputDirectory = Environment.CurrentDirectory; - public string OutputDirectory { - get => _outputDirectory; - init => _outputDirectory = string.IsNullOrEmpty(value) ? Environment.CurrentDirectory : value; - } + get; + init => field = string.IsNullOrEmpty(value) ? Environment.CurrentDirectory : value; + } = Environment.CurrentDirectory; } \ No newline at end of file diff --git a/src/ModVerify/Reporting/Settings/GlobalVerifyReportSettings.cs b/src/ModVerify/Reporting/Settings/GlobalVerifyReportSettings.cs index b376fe3..5a51436 100644 --- a/src/ModVerify/Reporting/Settings/GlobalVerifyReportSettings.cs +++ b/src/ModVerify/Reporting/Settings/GlobalVerifyReportSettings.cs @@ -1,8 +1,10 @@ namespace AET.ModVerify.Reporting.Settings; -public record GlobalVerifyReportSettings : VerifyReportSettings -{ - public VerificationBaseline Baseline { get; init; } = VerificationBaseline.Empty; +//public record GlobalVerifyReportSettings +//{ +// //public VerificationSeverity MinimumReportSeverity { get; init; } = VerificationSeverity.Information; - public SuppressionList Suppressions { get; init; } = SuppressionList.Empty; -} \ No newline at end of file +// public VerificationBaseline Baseline { get; init; } = VerificationBaseline.Empty; + +// public SuppressionList Suppressions { get; init; } = SuppressionList.Empty; +//} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Settings/VerifyReportSettings.cs b/src/ModVerify/Reporting/Settings/ReporterSettings.cs similarity index 81% rename from src/ModVerify/Reporting/Settings/VerifyReportSettings.cs rename to src/ModVerify/Reporting/Settings/ReporterSettings.cs index 1289822..ef33857 100644 --- a/src/ModVerify/Reporting/Settings/VerifyReportSettings.cs +++ b/src/ModVerify/Reporting/Settings/ReporterSettings.cs @@ -1,6 +1,6 @@ namespace AET.ModVerify.Reporting.Settings; -public record VerifyReportSettings +public record ReporterSettings { public VerificationSeverity MinimumReportSeverity { get; init; } = VerificationSeverity.Information; } \ No newline at end of file diff --git a/src/ModVerify/Reporting/VerificationBaseline.cs b/src/ModVerify/Reporting/VerificationBaseline.cs index 55a8973..3f9c274 100644 --- a/src/ModVerify/Reporting/VerificationBaseline.cs +++ b/src/ModVerify/Reporting/VerificationBaseline.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using System.Text.Json; using System.Threading.Tasks; using AET.ModVerify.Reporting.Json; @@ -11,12 +12,15 @@ namespace AET.ModVerify.Reporting; public sealed class VerificationBaseline : IReadOnlyCollection { - private static readonly Version LatestVersion = new(2, 0); + public static readonly Version LatestVersion = new(2, 1); + public static readonly string LatestVersionString = LatestVersion.ToString(2); - public static readonly VerificationBaseline Empty = new(VerificationSeverity.Information, []); + public static readonly VerificationBaseline Empty = new(VerificationSeverity.Information, [], null); private readonly HashSet _errors; + public BaselineVerificationTarget? Target { get; } + public Version? Version { get; } public VerificationSeverity MinimumSeverity { get; } @@ -24,18 +28,22 @@ public sealed class VerificationBaseline : IReadOnlyCollection public int Count => _errors.Count; + public bool IsEmpty => Count == 0; + internal VerificationBaseline(JsonVerificationBaseline baseline) { _errors = [..baseline.Errors.Select(x => new VerificationError(x))]; Version = baseline.Version; MinimumSeverity = baseline.MinimumSeverity; + Target = JsonVerificationTarget.ToTarget(baseline.Target); } - public VerificationBaseline(VerificationSeverity minimumSeverity, IEnumerable errors) + public VerificationBaseline(VerificationSeverity minimumSeverity, IEnumerable errors, BaselineVerificationTarget? target) { _errors = [..errors]; Version = LatestVersion; MinimumSeverity = minimumSeverity; + Target = target; } public bool Contains(VerificationError error) @@ -60,14 +68,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); } /// @@ -83,6 +84,10 @@ IEnumerator IEnumerable.GetEnumerator() public override string ToString() { - return $"Baseline [Version={Version}, MinSeverity={MinimumSeverity}, NumErrors={Count}]"; + var sb = new StringBuilder($"Baseline [Version={Version}, MinSeverity={MinimumSeverity}, NumErrors={Count}"); + if (Target is not null) + sb.Append($", Target={Target}"); + sb.Append(']'); + return sb.ToString(); } } \ No newline at end of file diff --git a/src/ModVerify/Reporting/VerificationError.cs b/src/ModVerify/Reporting/VerificationError.cs index 4174f32..538a8fd 100644 --- a/src/ModVerify/Reporting/VerificationError.cs +++ b/src/ModVerify/Reporting/VerificationError.cs @@ -117,6 +117,7 @@ public override int GetHashCode() public override string ToString() { - return $"[{Severity}] [{string.Join(" --> ", VerifierChain)}] {Id}: Message={Message}; Asset='{Asset}'; Context=[{string.Join(",", ContextEntries)}];"; + return $"[{Severity}] [{string.Join(" --> ", VerifierChain)}] " + + $"{Id}: Message={Message}; Asset='{Asset}'; Context=[{string.Join(",", ContextEntries)}];"; } } \ No newline at end of file diff --git a/src/ModVerify/Resources/Schemas/2.1/baseline.json b/src/ModVerify/Resources/Schemas/2.1/baseline.json new file mode 100644 index 0000000..da37c4d --- /dev/null +++ b/src/ModVerify/Resources/Schemas/2.1/baseline.json @@ -0,0 +1,124 @@ +{ + "$id": "https://AlamoEngine-Tools.github.io/schemas/mod-verify/2.1/baseline", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Represents a baseline for AET ModVerify", + "type": "object", + "$defs": { + "location": { + "type": "object", + "properties": { + "modPaths": { + "type": "array", + "items": { + "type": "string" + } + }, + "gamePath": { + "type": "string" + }, + "fallbackPaths": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "modPaths", + "gamePath", + "fallbackPaths" + ], + "additionalProperties": false + }, + "target": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "engine": { + "enum": [ "Eaw", "Foc" ] + }, + "location": { + "$ref": "#/$defs/location" + }, + "isGame": { + "type": "boolean" + } + }, + "required": [ + "name", + "engine" + ], + "additionalProperties": false + }, + "severity": { + "enum": [ "Information", "Warning", "Error", "Critical" ] + }, + "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.1" + }, + "minSeverity": { + "$ref": "#/$defs/severity" + }, + "errors": { + "type": "array", + "items": { + "$ref": "#/$defs/error" + }, + "additionalItems": false + }, + "target": { + "$ref": "#/$defs/target" + } + }, + "required": [ + "version", + "minSeverity", + "errors" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/src/ModVerify/Settings/FailFastSetting.cs b/src/ModVerify/Settings/FailFastSetting.cs new file mode 100644 index 0000000..44d2b43 --- /dev/null +++ b/src/ModVerify/Settings/FailFastSetting.cs @@ -0,0 +1,18 @@ +using AET.ModVerify.Reporting; + +namespace AET.ModVerify.Settings; + +public readonly struct FailFastSetting +{ + public static readonly FailFastSetting NoFailFast = default; + + public readonly bool IsFailFast; + + public readonly VerificationSeverity MinumumSeverity; + + public FailFastSetting(VerificationSeverity severity) + { + IsFailFast = true; + MinumumSeverity = severity; + } +} \ No newline at end of file diff --git a/src/ModVerify/Settings/GameVerifySettings.cs b/src/ModVerify/Settings/GameVerifySettings.cs index 3f84670..59fc7be 100644 --- a/src/ModVerify/Settings/GameVerifySettings.cs +++ b/src/ModVerify/Settings/GameVerifySettings.cs @@ -2,7 +2,7 @@ namespace AET.ModVerify.Settings; -public record GameVerifySettings +public sealed record GameVerifySettings { public static readonly GameVerifySettings Default = new() { diff --git a/src/ModVerify/Settings/VerifyPipelineSettings.cs b/src/ModVerify/Settings/VerifyPipelineSettings.cs index 2e11c74..46fc997 100644 --- a/src/ModVerify/Settings/VerifyPipelineSettings.cs +++ b/src/ModVerify/Settings/VerifyPipelineSettings.cs @@ -8,7 +8,7 @@ public sealed class VerifyPipelineSettings public required IGameVerifiersProvider VerifiersProvider { get; init; } - public bool FailFast { get; init; } + public FailFastSetting FailFastSettings { get; init; } = FailFastSetting.NoFailFast; public int ParallelVerifiers { get; init; } = 4; } \ 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/ModVerify/VerificationTarget.cs b/src/ModVerify/VerificationTarget.cs new file mode 100644 index 0000000..c0d5b97 --- /dev/null +++ b/src/ModVerify/VerificationTarget.cs @@ -0,0 +1,41 @@ +using System; +using System.Text; +using PG.StarWarsGame.Engine; + +namespace AET.ModVerify; + +public sealed class VerificationTarget +{ + public required GameEngineType Engine { get; init; } + + public required string Name + { + get; + init + { + if (string.IsNullOrEmpty(value)) + throw new ArgumentNullException(nameof(value)); + field = value; + } + } + + public required GameLocations Location + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } + + public string? Version { get; init; } + + public bool IsGame => Location.ModPaths.Count == 0; + + public override string ToString() + { + var sb = new StringBuilder($"[Name={Name};EngineType={Engine};"); + if (!string.IsNullOrEmpty(Version)) + sb.Append($"Version={Version};"); + sb.Append($"Location={Location};"); + sb.Append(']'); + return sb.ToString(); + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/AudioFilesVerifier.cs b/src/ModVerify/Verifiers/AudioFilesVerifier.cs index f7ad3b6..ddd9252 100644 --- a/src/ModVerify/Verifiers/AudioFilesVerifier.cs +++ b/src/ModVerify/Verifiers/AudioFilesVerifier.cs @@ -14,7 +14,6 @@ using PG.StarWarsGame.Engine; using PG.StarWarsGame.Engine.Audio.Sfx; using PG.StarWarsGame.Engine.Localization; -using PG.StarWarsGame.Files.MEG.Services.Builder.Normalization; #if NETSTANDARD2_0 using AnakinRaW.CommonUtilities.FileSystem; #endif @@ -30,7 +29,6 @@ public class AudioFilesVerifier : GameVerifier UnifyDirectorySeparators = true }; - private readonly EmpireAtWarMegDataEntryPathNormalizer _pathNormalizer = EmpireAtWarMegDataEntryPathNormalizer.Instance; private readonly ICrc32HashingService _hashingService; private readonly IFileSystem _fileSystem; private readonly IGameLanguageManager _languageManager; diff --git a/src/ModVerify/Verifiers/DuplicateNameFinder.cs b/src/ModVerify/Verifiers/DuplicateNameFinder.cs index 23ab1b8..0b3b585 100644 --- a/src/ModVerify/Verifiers/DuplicateNameFinder.cs +++ b/src/ModVerify/Verifiers/DuplicateNameFinder.cs @@ -40,10 +40,10 @@ private void CheckForDuplicateCrcEntries( string sourceName, TSource source, Func> crcSelector, - Func> entrySelector, + Func> entrySelector, Func entryToStringSelector, - Func, IEnumerable> contextSelector, - Func, string, string> errorMessageCreator) + Func, IEnumerable> contextSelector, + Func, string, string> errorMessageCreator) { foreach (var crc32 in crcSelector(source)) { @@ -87,13 +87,13 @@ private void CheckXmlObjectsForDuplicates(string databaseName, IGameManager entries, string fileName) + private static string CreateDuplicateMtdErrorMessage(ImmutableFrugalList entries, string fileName) { var firstEntry = entries.First(); return $"MTD File '{fileName}' has duplicate definitions for CRC ({firstEntry}): {string.Join(",", entries.Select(x => x.FileName))}"; } - private static string CreateDuplicateXmlErrorMessage(ReadOnlyFrugalList entries, string databaseName) where T : NamedXmlObject + private static string CreateDuplicateXmlErrorMessage(ImmutableFrugalList entries, string databaseName) where T : NamedXmlObject { var firstEntry = entries.First(); var message = $"{databaseName} '{firstEntry.Name}' ({firstEntry.Crc32}) has duplicate definitions: "; diff --git a/src/ModVerify/Verifiers/GameVerifierBase.cs b/src/ModVerify/Verifiers/GameVerifierBase.cs index 8c9d67d..02bacc4 100644 --- a/src/ModVerify/Verifiers/GameVerifierBase.cs +++ b/src/ModVerify/Verifiers/GameVerifierBase.cs @@ -15,10 +15,8 @@ namespace AET.ModVerify.Verifiers; public abstract class GameVerifierBase : IGameVerifierInfo { public event EventHandler? Error; - public event EventHandler>? Progress; - private readonly IStarWarsGameEngine _gameEngine; private readonly ConcurrentDictionary _verifyErrors = new(); protected readonly IFileSystem FileSystem; @@ -35,7 +33,7 @@ public abstract class GameVerifierBase : IGameVerifierInfo protected IStarWarsGameEngine GameEngine { get; } - protected IGameRepository Repository => _gameEngine.GameRepository; + protected IGameRepository Repository => GameEngine.GameRepository; protected IReadOnlyList VerifierChain { get; } @@ -49,7 +47,6 @@ protected GameVerifierBase( throw new ArgumentNullException(nameof(serviceProvider)); FileSystem = serviceProvider.GetRequiredService(); Services = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - _gameEngine = gameEngine ?? throw new ArgumentNullException(nameof(gameEngine)); Parent = parent; Settings = settings ?? throw new ArgumentNullException(nameof(settings)); GameEngine = gameEngine ?? throw new ArgumentNullException(nameof(gameEngine)); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager.cs index 311c212..0b8687e 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using PG.Commons.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.CommandBar.Components; using PG.StarWarsGame.Engine.CommandBar.Xml; @@ -18,6 +17,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using AnakinRaW.CommonUtilities.Collections; namespace PG.StarWarsGame.Engine.CommandBar; @@ -77,7 +77,7 @@ protected override async Task InitializeCoreAsync(CancellationToken token) var contentParser = new XmlContainerContentParser(ServiceProvider, ErrorReporter); contentParser.XmlParseError += OnParseError; - var parsedCommandBarComponents = new ValueListDictionary(); + var parsedCommandBarComponents = new FrugalValueListDictionary(); try { @@ -213,8 +213,8 @@ private void SetDefaultFont() if (_defaultFont is null) { // TODO: From GameConstants - string fontName = PGConstants.DefaultUnicodeFontName; - int size = 11; + var fontName = PGConstants.DefaultUnicodeFontName; + var size = 11; var font = fontManager.CreateFont(fontName, size, true, false, false, 1.0f); if (font is null) ErrorReporter.Assert(EngineAssert.FromNullOrEmpty([ToString()], $"Unable to create Default from name {fontName}")); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameLocations.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameLocations.cs index 18539a7..f57b605 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameLocations.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameLocations.cs @@ -1,12 +1,18 @@ -using System; +using AnakinRaW.CommonUtilities; +using System; using System.Collections.Generic; using System.Linq; -using AnakinRaW.CommonUtilities; +using System.Text; 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; } @@ -22,13 +28,13 @@ public GameLocations(string modPath, string gamePath, string fallbackGamePath) : ThrowHelper.ThrowIfNullOrEmpty(modPath); } - public GameLocations(IList modPaths, string gamePath, string fallbackGamePath) : this(modPaths, - gamePath, [fallbackGamePath]) + public GameLocations(IReadOnlyList modPaths, string gamePath, string fallbackGamePath) + : this(modPaths, gamePath, [fallbackGamePath]) { ThrowHelper.ThrowIfNullOrEmpty(fallbackGamePath); } - public GameLocations(IList modPaths, string gamePath, IList fallbackPaths) + public GameLocations(IReadOnlyList modPaths, string gamePath, IReadOnlyList fallbackPaths) { if (modPaths == null) throw new ArgumentNullException(nameof(modPaths)); @@ -38,5 +44,24 @@ public GameLocations(IList modPaths, string gamePath, IList fall ModPaths = modPaths.ToList(); GamePath = gamePath; FallbackPaths = fallbackPaths.ToList(); + + TargetPath = ModPaths.Count > 0 + ? ModPaths[0] + : GamePath; + } + + public override string ToString() + { + var sb = new StringBuilder(); + + sb.AppendLine("GameLocation=["); + if (ModPaths.Count > 0) + sb.AppendLine($"Mods=[{string.Join(";", ModPaths)}];"); + sb.AppendLine($"Game=[{GamePath}];"); + if (FallbackPaths.Count > 0) + sb.AppendLine($"Fallbacks=[{string.Join(";", FallbackPaths)}];"); + sb.AppendLine("]"); + + return sb.ToString(); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameManagerBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameManagerBase.cs index 7fa4d02..607121d 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameManagerBase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameManagerBase.cs @@ -6,7 +6,6 @@ using AnakinRaW.CommonUtilities.Collections; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using PG.Commons.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.ErrorReporting; using PG.StarWarsGame.Engine.IO.Repositories; @@ -16,13 +15,13 @@ namespace PG.StarWarsGame.Engine; internal abstract class GameManagerBase(GameRepository repository, GameEngineErrorReporterWrapper errorReporter, IServiceProvider serviceProvider) : GameManagerBase(repository, errorReporter, serviceProvider), IGameManager { - protected readonly ValueListDictionary NamedEntries = new(); + protected readonly FrugalValueListDictionary NamedEntries = new(); public ICollection Entries => NamedEntries.Values; public ICollection EntryKeys => NamedEntries.Keys; - public ReadOnlyFrugalList GetEntries(Crc32 key) + public ImmutableFrugalList GetEntries(Crc32 key) { return NamedEntries.GetValues(key); } @@ -62,7 +61,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/GuiDialog/Xml/XmlComponentTextureData.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/XmlComponentTextureData.cs index 2ed95ce..f102457 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/XmlComponentTextureData.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/XmlComponentTextureData.cs @@ -1,14 +1,14 @@ using System; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.StarWarsGame.Engine.Xml; using PG.StarWarsGame.Files.XML; namespace PG.StarWarsGame.Engine.GuiDialog.Xml; -public class XmlComponentTextureData(string componentId, IReadOnlyValueListDictionary textures, XmlLocationInfo location) +public class XmlComponentTextureData(string componentId, IReadOnlyFrugalValueListDictionary textures, XmlLocationInfo location) : XmlObject(location) { public string Component { get; } = componentId ?? throw new ArgumentNullException(componentId); - public IReadOnlyValueListDictionary Textures { get; } = textures ?? throw new ArgumentNullException(nameof(textures)); + public IReadOnlyFrugalValueListDictionary Textures { get; } = textures ?? throw new ArgumentNullException(nameof(textures)); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IGameEngineInitializationReporter.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IGameEngineInitializationReporter.cs new file mode 100644 index 0000000..2ff43bd --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IGameEngineInitializationReporter.cs @@ -0,0 +1,10 @@ +namespace PG.StarWarsGame.Engine; + +public interface IGameEngineInitializationReporter +{ + void ReportProgress(string message); + + void ReportStarted(); + + void ReportFinished(); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IGameManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IGameManager.cs index b9f60a7..750f00c 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IGameManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IGameManager.cs @@ -10,5 +10,5 @@ public interface IGameManager ICollection EntryKeys { get; } - ReadOnlyFrugalList GetEntries(Crc32 key); + ImmutableFrugalList GetEntries(Crc32 key); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs index 1304a89..7abd1b9 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; } @@ -192,7 +192,7 @@ private static bool PathStartsWithDataDirectory(ReadOnlySpan path, out int return null; if (fileFoundInfo.InMeg) - return _megExtractor.GetFileData(fileFoundInfo.MegDataEntryReference.Location); + return _megExtractor.GetData(fileFoundInfo.MegDataEntryReference.Location); return FileSystem.FileStream.New(fileFoundInfo.FilePath.ToString(), FileMode.Open, FileAccess.Read, FileShare.Read); } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.cs index 42bfa54..ec40d64 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.cs @@ -23,7 +23,7 @@ internal abstract partial class GameRepository : ServiceBase, IGameRepository { private readonly IMegFileService _megFileService; private readonly IMegFileExtractor _megExtractor; - private readonly PetroglyphDataEntryPathNormalizer _megPathNormalizer; + private readonly PetroglyphMegDataEntryPathNormalizer _megPathNormalizer; private readonly ICrc32HashingService _crc32HashingService; private readonly IVirtualMegArchiveBuilder _virtualMegBuilder; private readonly IGameLanguageManagerProvider _languageManagerProvider; @@ -56,7 +56,7 @@ protected GameRepository(GameLocations gameLocations, GameEngineErrorReporterWra _megFileService = serviceProvider.GetRequiredService(); _virtualMegBuilder = serviceProvider.GetRequiredService(); _crc32HashingService = serviceProvider.GetRequiredService(); - _megPathNormalizer = EmpireAtWarMegDataEntryPathNormalizer.Instance; + _megPathNormalizer = new EmpireAtWarMegDataEntryPathNormalizer(); _languageManagerProvider = serviceProvider.GetRequiredService(); _errorReporter = errorReporter; @@ -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/IPetroglyphStarWarsGameEngineService.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IPetroglyphStarWarsGameEngineService.cs index 314ce06..80aaa61 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IPetroglyphStarWarsGameEngineService.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IPetroglyphStarWarsGameEngineService.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading; +using System.Threading; using System.Threading.Tasks; using PG.StarWarsGame.Engine.ErrorReporting; @@ -11,7 +10,7 @@ public Task InitializeAsync( GameEngineType engineType, GameLocations gameLocations, IGameEngineErrorReporter? errorReporter = null, - IProgress? initProgress = null, + IGameEngineInitializationReporter? initReporter = null, bool cancelOnInitializationError = false, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj index 6055649..326fd1d 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 - @@ -46,6 +40,6 @@ - + \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs index d2acfba..02a8d9f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs @@ -27,7 +27,7 @@ public async Task InitializeAsync( GameEngineType engineType, GameLocations gameLocations, IGameEngineErrorReporter? errorReporter = null, - IProgress? progress = null, + IGameEngineInitializationReporter? initReporter = null, bool cancelOnInitializationError = false, CancellationToken cancellationToken = default) @@ -39,7 +39,7 @@ public async Task InitializeAsync( try { - return await InitializeEngine(engineType, gameLocations, errorListenerWrapper, progress, cts.Token) + return await InitializeEngineAsync(engineType, gameLocations, errorListenerWrapper, initReporter, cts.Token) .ConfigureAwait(false); } finally @@ -56,16 +56,17 @@ void OnInitializationError(object sender, InitializationError e) } } - private async Task InitializeEngine( + private async Task InitializeEngineAsync( GameEngineType engineType, GameLocations gameLocations, GameEngineErrorReporterWrapper errorReporter, - IProgress? progress, + IGameEngineInitializationReporter? initReporter, CancellationToken token) { try { - _logger?.LogInformation($"Initializing game engine for type '{engineType}'."); + _logger?.LogInformation("Initializing game engine for type '{GameEngineType}'.", engineType); + initReporter?.ReportStarted(); var repoFactory = _serviceProvider.GetRequiredService(); var repository = repoFactory.Create(engineType, gameLocations, errorReporter); @@ -73,7 +74,7 @@ private async Task InitializeEngine( var pgRender = new PGRender(repository, errorReporter, serviceProvider); var gameConstants = new GameConstants.GameConstants(repository, errorReporter, serviceProvider); - progress?.Report("Initializing GameConstants"); + initReporter?.ReportProgress("Initializing GameConstants"); await gameConstants.InitializeAsync(token); // AudioConstants @@ -81,23 +82,23 @@ private async Task InitializeEngine( // MousePointer var fontManger = new FontManager(repository, errorReporter, serviceProvider); - progress?.Report("Initializing FontManager"); + initReporter?.ReportProgress("Initializing FontManager"); await fontManger.InitializeAsync(token); var guiDialogs = new GuiDialogGameManager(repository, errorReporter, serviceProvider); - progress?.Report("Initializing GUIDialogManager"); + initReporter?.ReportProgress("Initializing GUIDialogManager"); await guiDialogs.InitializeAsync(token); var sfxGameManager = new SfxEventGameManager(repository, errorReporter, serviceProvider); - progress?.Report("Initializing SFXManager"); + initReporter?.ReportProgress("Initializing SFXManager"); await sfxGameManager.InitializeAsync(token); var commandBarManager = new CommandBarGameManager(repository, pgRender, gameConstants, fontManger, errorReporter, serviceProvider); - progress?.Report("Initializing CommandBar"); + initReporter?.ReportProgress("Initializing CommandBar"); await commandBarManager.InitializeAsync(token); var gameObjetTypeManager = new GameObjectTypeGameManager(repository, errorReporter, serviceProvider); - progress?.Report("Initializing GameObjectTypeManager"); + initReporter?.ReportProgress("Initializing GameObjectTypeManager"); await gameObjetTypeManager.InitializeAsync(token); token.ThrowIfCancellationRequested(); @@ -120,6 +121,7 @@ private async Task InitializeEngine( } finally { + initReporter?.ReportFinished(); _logger?.LogDebug("Finished initializing game database."); } } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/AnimationCollection.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/AnimationCollection.cs index 03916fe..dfd43aa 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/AnimationCollection.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/AnimationCollection.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using AnakinRaW.CommonUtilities; using AnakinRaW.CommonUtilities.Collections; -using PG.Commons.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Files.ALO.Files.Animations; @@ -13,10 +12,10 @@ public sealed class AnimationCollection : DisposableObject, IEnumerable _animations = new(); - private readonly ValueListDictionary _animationCrc = new(); + private readonly FrugalValueListDictionary _animations = new(); + private readonly FrugalValueListDictionary _animationCrc = new(); - public int Cout => _animations.Count; + public int Cout => _animations.ValueCount; public Crc32 GetAnimationCrc(ModelAnimationType type, int subIndex) { @@ -28,12 +27,12 @@ public Crc32 GetAnimationCrc(ModelAnimationType type, int subIndex) return checksumsForType[subIndex]; } - public ReadOnlyFrugalList GetAnimations(ModelAnimationType type) + public ImmutableFrugalList GetAnimations(ModelAnimationType type) { return _animations.GetValues(type); } - public bool TryGetAnimations(ModelAnimationType type, out ReadOnlyFrugalList animations) + public bool TryGetAnimations(ModelAnimationType type, out ImmutableFrugalList animations) { return _animations.TryGetValues(type, out animations); } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/WindowsFontManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/WindowsFontManager.cs index f11374e..fe8f546 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/WindowsFontManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/WindowsFontManager.cs @@ -28,8 +28,8 @@ public IEnumerable GetFontFamilies() return fonts; } - static IEnumerable<(Gdi32.ENUMLOGFONTEXDV lpelfe, Gdi32.ENUMTEXTMETRIC lpntme, Gdi32.FontType FontType)> GetFonts(Gdi32.SafeHDC hdc) + static IEnumerable<(Gdi32.ENUMLOGFONTEXDV lpelfe, Gdi32.ENUMTEXTMETRIC _, Gdi32.FontType __)> GetFonts(Gdi32.SafeHDC hdc) { - return Gdi32.EnumFontFamiliesEx(hdc, CharacterSet.DEFAULT_CHARSET); + return Gdi32.EnumFontFamiliesEx(hdc, lfCharSet: CharacterSet.DEFAULT_CHARSET); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/CommandBarComponentParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/CommandBarComponentParser.cs index 1e20a8b..297b474 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/CommandBarComponentParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/CommandBarComponentParser.cs @@ -1,7 +1,7 @@ using System; using System.Collections.ObjectModel; using System.Xml.Linq; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.CommandBar.Xml; using PG.StarWarsGame.Engine.Xml.Tags; @@ -12,7 +12,7 @@ namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; public sealed class CommandBarComponentParser( - IReadOnlyValueListDictionary parsedElements, + IReadOnlyFrugalValueListDictionary parsedElements, IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : XmlObjectParser(parsedElements, serviceProvider, errorReporter) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs index 4f445a7..5cac3e2 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs @@ -1,6 +1,6 @@ using System; using System.Xml.Linq; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.GameObjects; using PG.StarWarsGame.Files.XML; @@ -26,7 +26,7 @@ public static class GameObjectXmlTags } public sealed class GameObjectParser( - IReadOnlyValueListDictionary parsedElements, + IReadOnlyFrugalValueListDictionary parsedElements, IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : XmlObjectParser(parsedElements, serviceProvider, errorReporter) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs index 4e3a5cb..bc3439f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs @@ -1,7 +1,7 @@ using System; using System.Collections.ObjectModel; using System.Xml.Linq; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.Audio.Sfx; using PG.StarWarsGame.Engine.Xml.Tags; @@ -12,7 +12,7 @@ namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; public sealed class SfxEventParser( - IReadOnlyValueListDictionary parsedElements, + IReadOnlyFrugalValueListDictionary parsedElements, IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : XmlObjectParser(parsedElements, serviceProvider, errorReporter) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/CommandBarComponentFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/CommandBarComponentFileParser.cs index a32ff09..763d244 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/CommandBarComponentFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/CommandBarComponentFileParser.cs @@ -1,6 +1,6 @@ using System; using System.Xml.Linq; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.CommandBar.Xml; using PG.StarWarsGame.Engine.Xml.Parsers.Data; @@ -12,7 +12,7 @@ namespace PG.StarWarsGame.Engine.Xml.Parsers.File; internal class CommandBarComponentFileParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : PetroglyphXmlFileContainerParser(serviceProvider, errorReporter) { - protected override void Parse(XElement element, IValueListDictionary parsedElements, string fileName) + protected override void Parse(XElement element, IFrugalValueListDictionary parsedElements, string fileName) { var parser = new CommandBarComponentParser(parsedElements, ServiceProvider, ErrorReporter); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileParser.cs index d2abfed..ffeafff 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileParser.cs @@ -1,6 +1,6 @@ using System; using System.Xml.Linq; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.GameObjects; using PG.StarWarsGame.Engine.Xml.Parsers.Data; @@ -12,7 +12,7 @@ namespace PG.StarWarsGame.Engine.Xml.Parsers.File; internal class GameObjectFileParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : PetroglyphXmlFileContainerParser(serviceProvider, errorReporter) { - protected override void Parse(XElement element, IValueListDictionary parsedElements, string fileName) + protected override void Parse(XElement element, IFrugalValueListDictionary parsedElements, string fileName) { var parser = new GameObjectParser(parsedElements, ServiceProvider, ErrorReporter); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GuiDialogParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GuiDialogParser.cs index 7c23dd6..851aa9f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GuiDialogParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GuiDialogParser.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Xml.Linq; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.StarWarsGame.Engine.GuiDialog.Xml; using PG.StarWarsGame.Files.XML; using PG.StarWarsGame.Files.XML.ErrorHandling; @@ -49,7 +49,7 @@ private GuiDialogsXmlTextureData ParseTextures(XElement? element, string fileNam private XmlComponentTextureData ParseTexture(XElement texture) { var componentId = GetTagName(texture); - var textures = new ValueListDictionary(); + var textures = new FrugalValueListDictionary(); foreach (var entry in texture.Elements()) textures.Add(entry.Name.ToString(), PetroglyphXmlStringParser.Instance.Parse(entry)); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs index f40dd1a..841d805 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs @@ -1,6 +1,6 @@ using System; using System.Xml.Linq; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.Audio.Sfx; using PG.StarWarsGame.Engine.Xml.Parsers.Data; @@ -12,7 +12,7 @@ namespace PG.StarWarsGame.Engine.Xml.Parsers.File; internal class SfxEventFileParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : PetroglyphXmlFileContainerParser(serviceProvider, errorReporter) { - protected override void Parse(XElement element, IValueListDictionary parsedElements, string fileName) + protected override void Parse(XElement element, IFrugalValueListDictionary parsedElements, string fileName) { var parser = new SfxEventParser(parsedElements, ServiceProvider, ErrorReporter); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs index 5d5f98a..00f4f61 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs @@ -1,9 +1,9 @@ using System; using System.Linq; using System.Xml; +using AnakinRaW.CommonUtilities.Collections; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using PG.Commons.Collections; using PG.Commons.Hashing; using PG.Commons.Services; using PG.StarWarsGame.Engine.IO; @@ -34,16 +34,16 @@ public void ParseEntriesFromFileListXml( string xmlFile, IGameRepository gameRepository, string lookupPath, - ValueListDictionary entries, + FrugalValueListDictionary entries, Action? onFileParseAction = null) where T : notnull { - Logger.LogDebug($"Parsing container data '{xmlFile}'"); + 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.Engine/Xml/Parsers/XmlObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs index fead02d..1565e9e 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs @@ -1,7 +1,7 @@ using System; using System.Xml.Linq; +using AnakinRaW.CommonUtilities.Collections; using Microsoft.Extensions.DependencyInjection; -using PG.Commons.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Files.XML.ErrorHandling; using PG.StarWarsGame.Files.XML.Parsers; @@ -9,7 +9,7 @@ namespace PG.StarWarsGame.Engine.Xml.Parsers; public abstract class XmlObjectParser( - IReadOnlyValueListDictionary parsedElements, + IReadOnlyFrugalValueListDictionary parsedElements, IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : XmlObjectParser(parsedElements, serviceProvider, errorReporter) where TObject : XmlObject @@ -34,12 +34,12 @@ public readonly struct EmptyParseState public abstract class XmlObjectParser( - IReadOnlyValueListDictionary parsedElements, + IReadOnlyFrugalValueListDictionary parsedElements, IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : PetroglyphXmlElementParser(errorReporter) where TObject : XmlObject { - protected IReadOnlyValueListDictionary ParsedElements { get; } = + protected IReadOnlyFrugalValueListDictionary ParsedElements { get; } = parsedElements ?? throw new ArgumentNullException(nameof(parsedElements)); protected ICrc32HashingService HashingService { get; } = serviceProvider.GetRequiredService(); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/PG.StarWarsGame.Files.ALO.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/PG.StarWarsGame.Files.ALO.csproj index 4fd3dd4..0d071ca 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 @@ -29,6 +25,6 @@ - + \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj index d6a7939..5df0123 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,11 +14,12 @@ true snupkg + preview - + - + \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/PG.StarWarsGame.Files.XML.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/PG.StarWarsGame.Files.XML.csproj index 066e32d..be85ee8 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,18 @@ true snupkg true + preview - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlFileContainerParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlFileContainerParser.cs index f42a9a9..cd21e00 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlFileContainerParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlFileContainerParser.cs @@ -1,10 +1,10 @@ using System.IO; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; namespace PG.StarWarsGame.Files.XML.Parsers; public interface IPetroglyphXmlFileContainerParser : IPetroglyphXmlParser where T : notnull { - void ParseFile(Stream xmlStream, IValueListDictionary parsedEntries); + void ParseFile(Stream xmlStream, IFrugalValueListDictionary parsedEntries); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileContainerParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileContainerParser.cs index 4e371e2..d2b862a 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileContainerParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileContainerParser.cs @@ -1,7 +1,7 @@ using System; using System.IO; using System.Xml.Linq; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Files.XML.ErrorHandling; @@ -10,12 +10,12 @@ namespace PG.StarWarsGame.Files.XML.Parsers; public abstract class PetroglyphXmlFileContainerParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? listener = null) : PetroglyphXmlFileParserBase(serviceProvider, listener), IPetroglyphXmlFileContainerParser where T : notnull { - public void ParseFile(Stream xmlStream, IValueListDictionary parsedEntries) + public void ParseFile(Stream xmlStream, IFrugalValueListDictionary parsedEntries) { var root = GetRootElement(xmlStream, out var fileName); if (root is not null) Parse(root, parsedEntries, fileName); } - protected abstract void Parse(XElement element, IValueListDictionary parsedElements, string fileName); + protected abstract void Parse(XElement element, IFrugalValueListDictionary parsedElements, string fileName); } \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/CommonTestBase.cs b/test/ModVerify.CliApp.Test/CommonTestBase.cs new file mode 100644 index 0000000..8a93507 --- /dev/null +++ b/test/ModVerify.CliApp.Test/CommonTestBase.cs @@ -0,0 +1,22 @@ +using AET.SteamAbstraction; +using AnakinRaW.CommonUtilities.Hashing; +using AnakinRaW.CommonUtilities.Testing; +using Microsoft.Extensions.DependencyInjection; +using PG.Commons; +using PG.StarWarsGame.Infrastructure; +using PG.StarWarsGame.Infrastructure.Clients.Steam; + +namespace ModVerify.CliApp.Test; + +public abstract class CommonTestBase : TestBaseWithFileSystem +{ + protected override void SetupServices(IServiceCollection serviceCollection) + { + base.SetupServices(serviceCollection); + serviceCollection.AddSingleton(sp => new HashingService(sp)); + PetroglyphCommons.ContributeServices(serviceCollection); + PetroglyphGameInfrastructure.InitializeServices(serviceCollection); + SteamAbstractionLayer.InitializeServices(serviceCollection); + SteamPetroglyphStarWarsGameClients.InitializeServices(serviceCollection); + } +} \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/EmbeddedBaselineTest.cs b/test/ModVerify.CliApp.Test/EmbeddedBaselineTest.cs new file mode 100644 index 0000000..ecf3253 --- /dev/null +++ b/test/ModVerify.CliApp.Test/EmbeddedBaselineTest.cs @@ -0,0 +1,34 @@ +using AET.ModVerify.App; +using AET.ModVerify.App.Reporting; +using AnakinRaW.ApplicationBase.Environment; +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Engine; +using System; +using System.IO.Abstractions; +using Testably.Abstractions; +using Xunit; + +namespace ModVerify.CliApp.Test; + +public class BaselineSelectorTest +{ + private static readonly IFileSystem FileSystem = new RealFileSystem(); + 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. + BaselineSelector.LoadEmbeddedBaseline(engineType); + } +} \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj b/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj new file mode 100644 index 0000000..ef3c9ef --- /dev/null +++ b/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj @@ -0,0 +1,55 @@ + + + + net10.0 + $(TargetFrameworks);net481 + preview + + + + false + true + Exe + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + true + true + + + + Windows + + + Linux + + + + diff --git a/test/ModVerify.CliApp.Test/ModVerifyOptionsParserTest.cs b/test/ModVerify.CliApp.Test/ModVerifyOptionsParserTest.cs new file mode 100644 index 0000000..c5caf92 --- /dev/null +++ b/test/ModVerify.CliApp.Test/ModVerifyOptionsParserTest.cs @@ -0,0 +1,214 @@ +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 Xunit; +#if NETFRAMEWORK +using ModVerify.CliApp.Test.Utilities; +#endif + + +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/TargetSelectors/AutomaticSelectorTest.cs b/test/ModVerify.CliApp.Test/TargetSelectors/AutomaticSelectorTest.cs new file mode 100644 index 0000000..7d02b8f --- /dev/null +++ b/test/ModVerify.CliApp.Test/TargetSelectors/AutomaticSelectorTest.cs @@ -0,0 +1,408 @@ +#if Windows +using AET.Modinfo.Model; +using AET.Modinfo.Spec.Steam; +using AET.ModVerify.App.GameFinder; +using AET.ModVerify.App.Settings; +using AET.ModVerify.App.TargetSelectors; +using AET.ModVerify.App.Utilities; +using AET.SteamAbstraction.Testing; +using AnakinRaW.CommonUtilities.Registry; +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Infrastructure.Games; +using PG.StarWarsGame.Infrastructure.Testing; +using PG.StarWarsGame.Infrastructure.Testing.Installations; +using PG.StarWarsGame.Infrastructure.Testing.Installations.Game; +using System; +using Xunit; + +namespace ModVerify.CliApp.Test.TargetSelectors; + +public class AutomaticSelectorTest : CommonTestBase +{ + private readonly AutomaticSelector _selector; + private readonly IRegistry _registry = new InMemoryRegistry(InMemoryRegistryCreationFlags.WindowsLike); + + public AutomaticSelectorTest() + { + _selector = new AutomaticSelector(ServiceProvider); + } + + protected override void SetupServices(IServiceCollection serviceCollection) + { + base.SetupServices(serviceCollection); + serviceCollection.AddSingleton(_registry); + } + + [Fact] + public void Test_SelectTarget_GameNotInstalled() + { + var settings = new VerificationTargetSettings + { + TargetPath = "/test", + }; + Assert.Throws(() => _selector.SelectTarget(settings)); + } + + [Fact] + public void Test_SelectTarget_WrongSettings() + { + var settings = new VerificationTargetSettings + { + TargetPath = "/test", + FallbackGamePath = "does/not/exist", + ModPaths = ["also/does/not/exist"], + GamePath = "not/found" + }; + Assert.Throws(() => _selector.SelectTarget(settings)); + } + + [Theory] + [MemberData(nameof(GITestUtilities.RealGameIdentities), MemberType = typeof(GITestUtilities))] + public void Test_SelectTarget_FromGamePath(IGameIdentity identity) + { + TestSelectTarget( + identity, + i => i, + gi => new VerificationTargetSettings + { + TargetPath = gi.PlayableObject.Directory.FullName, + Engine = null + }); + TestSelectTarget( + identity, + i => i, + gi => new VerificationTargetSettings + { + TargetPath = gi.PlayableObject.Directory.FullName, + Engine = gi.PlayableObject.Game.Type.ToEngineType() + }); + } + + [Theory] + [MemberData(nameof(GITestUtilities.RealGameIdentities), MemberType = typeof(GITestUtilities))] + public void Test_SelectTarget_FromGamePath_OppositeEngine_ThrowsGameNotFoundException(IGameIdentity identity) + { + TestSelectTarget(identity, + i => i, + gi => new VerificationTargetSettings + { + TargetPath = gi.PlayableObject.Directory.FullName, + Engine = gi.PlayableObject.Game.Type.Opposite().ToEngineType(), + }, + typeof(GameNotFoundException)); + } + + [Theory] + [MemberData(nameof(GITestUtilities.RealGameIdentities), MemberType = typeof(GITestUtilities))] + public void Test_SelectTarget_ModInModsDir(IGameIdentity identity) + { + TestSelectTarget( + identity, + gameInstallation => gameInstallation.InstallMod("MyMod", false), + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = null + }); + TestSelectTarget( + identity, + gameInstallation => gameInstallation.InstallMod("MyMod", false), + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = ti.GameInstallation.Game.Type.ToEngineType() + }); + } + + [Theory] + [MemberData(nameof(GITestUtilities.RealGameIdentities), MemberType = typeof(GITestUtilities))] + public void Test_SelectTarget_ModInModsDir_WrongGameEngine_Throws(IGameIdentity identity) + { + TestSelectTarget( + identity, + gameInstallation => gameInstallation.InstallMod("MyMod", false), + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = ti.GameInstallation.Game.Type.Opposite().ToEngineType() + }, typeof(ArgumentException)); + } + + + [Theory] + [InlineData(GameType.Eaw)] + [InlineData(GameType.Foc)] + public void Test_SelectTarget_Workshops_UnknownModEngineType(GameType gameType) + { + var identity = new GameIdentity(gameType, GamePlatform.SteamGold); + TestSelectTarget( + identity, + gameInstallation => gameInstallation.InstallMod("MyMod", true), + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = ti.GameInstallation.Game.Type.ToEngineType() + }); + + TestSelectTarget( + identity, + gameInstallation => gameInstallation.InstallMod("MyMod", true), + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = null // Causes fallback to FoC + }, + overrideAssertData: new OverrideAssertData { GameType = GameType.Foc }); + } + + [Theory] + [InlineData(GameType.Eaw)] + [InlineData(GameType.Foc)] + public void Test_SelectTarget_Workshops_UnspecifiedEngineIsCorrectEngineTyp_KnownModEngineType(GameType gameType) + { + var identity = new GameIdentity(gameType, GamePlatform.SteamGold); + + var modinfo = new ModinfoData("MyMod") + { + SteamData = new SteamData("123456", "123456", SteamWorkshopVisibility.Public, "MyMod", + [gameType.ToString().ToUpper()]) + }; + + TestSelectTarget( + identity, + gameInstallation => + { + var modInstallation = gameInstallation.InstallMod(modinfo, true); + modInstallation.InstallModinfoFile(modinfo); + return modInstallation; + }, + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = null + }); + } + + [Theory] + [InlineData(GameType.Eaw)] + [InlineData(GameType.Foc)] + public void Test_SelectTarget_Workshops_IncompatibleKnownModEngineType_Throws(GameType gameType) + { + var identity = new GameIdentity(gameType, GamePlatform.SteamGold); + + var modinfo = new ModinfoData("MyMod") + { + SteamData = new SteamData("123456", "123456", SteamWorkshopVisibility.Public, "MyMod", + [gameType.ToString().ToUpper()]) + }; + + TestSelectTarget( + identity, + gameInstallation => + { + var modInstallation = gameInstallation.InstallMod(modinfo, true); + modInstallation.InstallModinfoFile(modinfo); + return modInstallation; + }, + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = gameType.Opposite().ToEngineType() + }, + typeof(ArgumentException)); + } + + [Theory] + [InlineData(GameType.Eaw)] + [InlineData(GameType.Foc)] + public void Test_SelectTarget_Workshops_MultipleKnownModEngineTypes(GameType gameType) + { + var identity = new GameIdentity(gameType, GamePlatform.SteamGold); + + var modinfo = new ModinfoData("MyMod") + { + SteamData = new SteamData("123456", "123456", SteamWorkshopVisibility.Public, "MyMod", + [gameType.ToString().ToUpper(), gameType.Opposite().ToString().ToUpper()]) + }; + + TestSelectTarget( + identity, + gameInstallation => + { + var modInstallation = gameInstallation.InstallMod(modinfo, true); + modInstallation.InstallModinfoFile(modinfo); + return modInstallation; + }, + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = gameType.ToEngineType() + }); + + TestSelectTarget( + identity, + gameInstallation => + { + var modInstallation = gameInstallation.InstallMod(modinfo, true); + modInstallation.InstallModinfoFile(modinfo); + return modInstallation; + }, + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = null // Causes fallback to FoC + }, + overrideAssertData: new OverrideAssertData{GameType = GameType.Foc}); + } + + [Theory] + [MemberData(nameof(GITestUtilities.RealGameIdentities), MemberType = typeof(GITestUtilities))] + public void Test_SelectTarget_DetachedMod_NoEngineSpecified_Throws(IGameIdentity identity) + { + TestSelectTarget( + identity, + gameInstallation => + { + var modinfo = new ModinfoData("DetachedMod"); + var modPath = FileSystem.Directory.CreateDirectory("/detachedMod"); + return gameInstallation.InstallMod(modinfo, modPath, false); + }, + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = null // No Engine means we cannot proceed + }, + expectedExceptionType: typeof(ArgumentException)); + } + + [Theory] + [MemberData(nameof(GITestUtilities.RealGameIdentities), MemberType = typeof(GITestUtilities))] + public void Test_SelectTarget_DetachedMod(IGameIdentity identity) + { + TestSelectTarget( + identity, + gameInstallation => + { + var modinfo = new ModinfoData("DetachedMod"); + var modPath = FileSystem.Directory.CreateDirectory("/detachedMod"); + return gameInstallation.InstallMod(modinfo, modPath, false); + }, + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = identity.Type.ToEngineType() + }); + } + + [Theory] + [MemberData(nameof(GITestUtilities.RealGameIdentities), MemberType = typeof(GITestUtilities))] + public void Test_SelectTarget_AttachedMod_DoesNotExist(IGameIdentity identity) + { + // Currently, only Steam is supported for detached mods. + TestSelectTarget( + identity, + gameInstallation => + { + var modInstallation = gameInstallation.InstallMod("MyMod", GITestUtilities.GetRandomWorkshopFlag(identity)); + modInstallation.Mod.Directory.Delete(true); + return modInstallation; + }, + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = identity.Type.ToEngineType() + }, + typeof(TargetNotFoundException)); + } + + [Theory] + [MemberData(nameof(GITestUtilities.RealGameIdentities), MemberType = typeof(GITestUtilities))] + public void Test_SelectTarget_DetachedMod_DoesNotExist(IGameIdentity identity) + { + TestSelectTarget( + identity, + gameInstallation => + { + var modinfo = new ModinfoData("DetachedMod"); + var modPath = FileSystem.DirectoryInfo.New("/detachedMod"); + var modInstallation = gameInstallation.InstallMod(modinfo, modPath, false); + modPath.Delete(true); + return modInstallation; + }, + ti => new VerificationTargetSettings + { + TargetPath = ti.PlayableObject.Directory.FullName, + Engine = identity.Type.ToEngineType() + }, + typeof(TargetNotFoundException)); + } + + private void TestSelectTarget( + IGameIdentity identity, + Func targetFactory, + Func settingsFactory, + Type? expectedExceptionType = null, + OverrideAssertData? overrideAssertData = null) + { + var (eaw, foc) = InstallGames(identity.Platform); + var gameInstallation = identity.Type switch + { + GameType.Eaw => eaw, + GameType.Foc => foc, + _ => throw new ArgumentOutOfRangeException() + }; + + var targetInstallation = targetFactory(gameInstallation); + + var settings = settingsFactory(targetInstallation); + + if (expectedExceptionType is not null) + { + Assert.Throws(expectedExceptionType, () => _selector.SelectTarget(settings)); + return; + } + + var result = _selector.SelectTarget(settings); + Assert.Equal(overrideAssertData?.GameType ?? identity.Type, result.Engine.FromEngineType()); + Assert.Equal(targetInstallation.PlayableObject.GetType(), result.Target!.GetType()); + Assert.Equal(targetInstallation.PlayableObject.Directory.FullName, result.Locations.TargetPath); + + if (result.Engine == GameEngineType.Foc) + Assert.NotEmpty(result.Locations.FallbackPaths); + else + Assert.Empty(result.Locations.FallbackPaths); + } + + private (ITestingGameInstallation eaw, ITestingGameInstallation foc) InstallGames(GamePlatform platform) + { + var eaw = GameInfrastructureTesting.Game(new GameIdentity(GameType.Eaw, platform), ServiceProvider); + var foc = GameInfrastructureTesting.Game(new GameIdentity(GameType.Foc, platform), ServiceProvider); + + GameInfrastructureTesting.Registry(ServiceProvider).CreateInstalled(eaw.Game); + GameInfrastructureTesting.Registry(ServiceProvider).CreateInstalled(foc.Game); + + eaw.InstallMod("OtherEawMod"); + foc.InstallMod("OtherFocMod"); + + if (platform == GamePlatform.SteamGold) + InstallSteam(); + + return (eaw, foc); + } + + private void InstallSteam() + { + var steam = SteamTesting.Steam(ServiceProvider); + steam.Install(); + // Register Game to Steam + var lib = steam.InstallDefaultLibrary(); + lib.InstallGame(32470, "Star Wars Empire at War", [32472]); + } + + private class OverrideAssertData + { + public GameType? GameType { get; init; } + } +} +#endif \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/TargetSelectors/GameFinderServiceTest.cs b/test/ModVerify.CliApp.Test/TargetSelectors/GameFinderServiceTest.cs new file mode 100644 index 0000000..3f9fd6c --- /dev/null +++ b/test/ModVerify.CliApp.Test/TargetSelectors/GameFinderServiceTest.cs @@ -0,0 +1,74 @@ +#if Windows +using AET.ModVerify.App.GameFinder; +using AnakinRaW.CommonUtilities.Registry; +using AnakinRaW.CommonUtilities.Testing.Extensions; +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Infrastructure.Games; +using PG.StarWarsGame.Infrastructure.Testing; +using PG.StarWarsGame.Infrastructure.Testing.Installations; +using Xunit; + +namespace ModVerify.CliApp.Test.TargetSelectors; + +public class GameFinderServiceTest : CommonTestBase +{ + private readonly IRegistry _registry = new InMemoryRegistry(InMemoryRegistryCreationFlags.WindowsLike); + private readonly GameFinderService _finderService; + public GameFinderServiceTest() + { + _finderService = new GameFinderService(ServiceProvider); + } + protected override void SetupServices(IServiceCollection serviceCollection) + { + base.SetupServices(serviceCollection); + serviceCollection.AddSingleton(_registry); + } + + [Theory] + [MemberData(nameof(GITestUtilities.RealGameIdentities), MemberType = typeof(GITestUtilities))] + public void FindGame_SearchFallbackGame_FocInstalledButEawNot_ThrowsGameNotFoundException(IGameIdentity identity) + { + if (identity.Type == GameType.Eaw) + return; + + var foc = GameInfrastructureTesting.Game(identity, ServiceProvider); + GameInfrastructureTesting.Registry(ServiceProvider).CreateInstalled(foc.Game); + + Assert.Throws(() => _finderService.FindGame(foc.Game.Directory.FullName, new GameFinderSettings + { + Engine = GameEngineType.Foc, + SearchFallbackGame = true + })); + + Assert.Throws(() => _finderService.FindGame(foc.Game.Directory.FullName, new GameFinderSettings + { + Engine = null, + SearchFallbackGame = true + })); + } + + [Theory] + [MemberData(nameof(GITestUtilities.RealGameIdentities), MemberType = typeof(GITestUtilities))] + public void FindGame_EngineEaw_SearchFallbackGameIsIgnored(IGameIdentity identity) + { + if (identity.Type == GameType.Foc) + return; + + var eaw = GameInfrastructureTesting.Game(identity, ServiceProvider); + GameInfrastructureTesting.Registry(ServiceProvider).CreateInstalled(eaw.Game); + + Assert.DoesNotThrow(() => _finderService.FindGame(eaw.Game.Directory.FullName, new GameFinderSettings + { + Engine = GameEngineType.Eaw, + SearchFallbackGame = true + })); + + Assert.DoesNotThrow(() => _finderService.FindGame(eaw.Game.Directory.FullName, new GameFinderSettings + { + Engine = null, + SearchFallbackGame = true + })); + } +} +#endif 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$" ],