From bc75fa46d42b3a6d0ecec502322ff00b4a21b2e5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 17 Aug 2025 10:43:17 +0200 Subject: [PATCH 1/7] Bump the actions-deps group across 1 directory with 2 updates (#23) Bumps the actions-deps group with 2 updates in the / directory: [actions/checkout](https://github.com/actions/checkout) and [actions/download-artifact](https://github.com/actions/download-artifact). Updates `actions/checkout` from 4 to 5 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) Updates `actions/download-artifact` from 4 to 5 - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps - dependency-name: actions/download-artifact dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 6 +++--- .github/workflows/test.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7ff6b7d..7369175 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,7 +35,7 @@ jobs: runs-on: windows-latest steps: - name: Checkout sources - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 submodules: recursive @@ -62,10 +62,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: name: Binary Releases path: ./releases diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f6c4d43..ed2af48 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 submodules: recursive From 1dd0ca4d6c7cbe04e9c91d7a9390388ff646ae94 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Mon, 8 Dec 2025 18:31:21 +0100 Subject: [PATCH 2/7] Auto update (#27) * add submodul * update deps * update deps * integrate moddingtoolbase code * implement and test argument parser * update namespace * move option parsing from boostrap to app code * update deps * update subs * update sub * update deps * add settings build exception handling * add json schema for baseline * fix tests * update subs * start feature: searching for local baselines * update deps * update sub * minor simplifications * update module * test on .net framework too * simplify * reenable update check and added default baseline * update sub * print used baseline file * make marker static * fix test * some cli improvements * update sub * update module * update logging * update * update deps * update sub * start update impl * horizontal frame supports native console * update sub * update sub * update sub * git pull * update dependencies * update logging * update modules * update logging * update module * update ci to .net 10 * fix test * mograte to new solution file * update module * fix ci * deploy to server * update config * new publish * try fix deploy * fix launch of uploader * update module * udpate version * update dependencies * logging again * logigng * logging der 5. --- .github/workflows/release.yml | 56 +- .github/workflows/test.yml | 4 +- .gitmodules | 3 + Directory.Build.props | 4 +- ModVerify.sln | 63 - ModVerify.slnx | 28 + modules/ModdingToolBase | 1 + src/ModVerify.CliApp/ConsoleUtilities.cs | 94 - src/ModVerify.CliApp/ExtensionMethods.cs | 17 - .../GameFinder/GameFinderResult.cs | 2 +- .../GameFinder/GameFinderService.cs | 10 +- .../{ => GameFinder}/GameNotFoundException.cs | 2 +- .../ModSelectors/AutomaticModSelector.cs | 11 +- .../ModSelectors/ConsoleModSelector.cs | 10 +- .../ModSelectors/IModSelector.cs | 4 +- .../ModSelectors/ManualModSelector.cs | 4 +- .../ModSelectors/ModSelectorBase.cs | 6 +- .../ModSelectors/ModSelectorFactory.cs | 4 +- .../ModSelectors/SettingsBasedModSelector.cs | 9 +- .../VerifyInstallationData.cs} | 4 +- src/ModVerify.CliApp/ModVerify.CliApp.csproj | 71 +- src/ModVerify.CliApp/ModVerifyApp.cs | 155 -- .../ModVerifyAppEnvironment.cs | 71 + src/ModVerify.CliApp/ModVerifyApplication.cs | 251 ++ src/ModVerify.CliApp/ModVerifyConstants.cs | 13 + .../Options/CommandLine/VerifyVerbOption.cs | 26 - src/ModVerify.CliApp/Program.cs | 381 +-- .../Properties/AssemblyAttributes.cs | 3 + .../Properties/launchSettings.json | 6 +- .../Reporting/BaselineFactory.cs | 71 + .../Reporting/BaselineSelector.cs | 139 ++ .../EngineInitializeProgressReporter.cs | 2 +- .../VerifyConsoleProgressReporter.cs | 10 +- .../Resources/Baselines/BaselineResources.cs | 4 + .../Resources/Baselines/baseline-foc.json | 2081 +++++++++++++++++ .../CommandLine/BaseModVerifyOptions.cs | 26 +- .../CommandLine/CreateBaselineVerbOption.cs | 6 +- .../CommandLine/ModVerifyOptionsContainer.cs | 12 + .../CommandLine/ModVerifyOptionsParser.cs | 95 + .../Settings/CommandLine/VerifyVerbOption.cs | 39 + .../GameInstallationsSettings.cs | 11 +- .../ModVerifyAppSettings.cs | 9 +- .../Settings/ModVerifyReportSettings.cs | 14 + .../{ => Settings}/SettingsBuilder.cs | 88 +- .../{ => Github}/GithubReleaseEntry.cs | 2 +- .../Updates/{ => Github}/GithubReleaseList.cs | 2 +- .../GithubUpdateChecker.cs} | 32 +- .../Updates/Github/GithubUpdateConstants.cs | 9 + .../GithubUpdateInfo.cs} | 4 +- .../Updates/ModVerifyUpdateMode.cs | 8 + .../Updates/ModVerifyUpdater.cs | 149 ++ .../Updates/ModVerifyUpdaterInformation.cs | 22 - .../Updates/SelfUpdate/AssemblyInfo.cs | 6 + .../SelfUpdate/ModVerifyApplicationUpdater.cs | 40 + .../ModVerifyUpdateResultHandler.cs | 28 + .../Utilities/ExtensionMethods.cs | 41 + .../Utilities/ModVerifyConsoleUtilities.cs | 30 + src/ModVerify.CliApp/Utilities/Spinner.cs | 172 ++ src/ModVerify/ModVerify.csproj | 25 +- .../Pipeline/GameVerifierPipelineStep.cs | 4 +- src/ModVerify/Pipeline/GameVerifyPipeline.cs | 1 + .../IncompatibleBaselineException.cs | 8 - .../Reporting/InvalidBaselineException.cs | 14 + .../Reporting/Json/JsonBaselineParser.cs | 38 + .../Reporting/Json/JsonBaselineSchema.cs | 123 + .../Reporting/Json/JsonVerificationError.cs | 2 +- .../Engine/GameAssertErrorReporter.cs | 1 + .../VerificationReportersExtensions.cs | 67 +- .../Reporting/VerificationBaseline.cs | 12 +- .../Resources/Schemas/2.0/baseline.json | 70 + .../Utilities/VerificationErrorExtensions.cs | 33 +- .../PG.StarWarsGame.Engine/GameLocations.cs | 9 + .../PG.StarWarsGame.Engine/GameManagerBase.cs | 2 +- .../GuiDialog/GuiDialogGameManager.cs | 4 +- .../IO/Repositories/GameRepository.Files.cs | 4 +- .../IO/Repositories/GameRepository.cs | 10 +- .../PG.StarWarsGame.Engine.csproj | 22 +- .../PetroglyphStarWarsGameEngineService.cs | 2 +- .../Xml/Parsers/XmlContainerContentParser.cs | 8 +- .../PG.StarWarsGame.Files.ALO.csproj | 6 +- .../PG.StarWarsGame.Files.ChunkFiles.csproj | 3 +- .../PG.StarWarsGame.Files.XML.csproj | 11 +- test/ModVerify.CliApp.Test/CommonTestBase.cs | 29 + .../EmbeddedBaselineTest.cs | 47 + .../ModVerify.CliApp.Test.csproj | 33 + .../ModVerifyOptionsParserTest.cs | 210 ++ .../TestData/NoVerifierProvider.cs | 16 + .../ModVerify.CliApp.Test/TestData/TestEnv.cs | 11 + .../TestData/UpdatableEnv.cs | 20 + .../Utilities/StringExtensions.cs | 13 + version.json | 2 +- 91 files changed, 4474 insertions(+), 851 deletions(-) create mode 100644 .gitmodules delete mode 100644 ModVerify.sln create mode 100644 ModVerify.slnx create mode 160000 modules/ModdingToolBase delete mode 100644 src/ModVerify.CliApp/ConsoleUtilities.cs delete mode 100644 src/ModVerify.CliApp/ExtensionMethods.cs rename src/ModVerify.CliApp/{ => GameFinder}/GameNotFoundException.cs (76%) rename src/ModVerify.CliApp/{VerifyInstallationInformation.cs => ModSelectors/VerifyInstallationData.cs} (90%) delete mode 100644 src/ModVerify.CliApp/ModVerifyApp.cs create mode 100644 src/ModVerify.CliApp/ModVerifyAppEnvironment.cs create mode 100644 src/ModVerify.CliApp/ModVerifyApplication.cs create mode 100644 src/ModVerify.CliApp/ModVerifyConstants.cs delete mode 100644 src/ModVerify.CliApp/Options/CommandLine/VerifyVerbOption.cs create mode 100644 src/ModVerify.CliApp/Properties/AssemblyAttributes.cs create mode 100644 src/ModVerify.CliApp/Reporting/BaselineFactory.cs create mode 100644 src/ModVerify.CliApp/Reporting/BaselineSelector.cs create mode 100644 src/ModVerify.CliApp/Resources/Baselines/BaselineResources.cs create mode 100644 src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json rename src/ModVerify.CliApp/{Options => Settings}/CommandLine/BaseModVerifyOptions.cs (83%) rename src/ModVerify.CliApp/{Options => Settings}/CommandLine/CreateBaselineVerbOption.cs (59%) create mode 100644 src/ModVerify.CliApp/Settings/CommandLine/ModVerifyOptionsContainer.cs create mode 100644 src/ModVerify.CliApp/Settings/CommandLine/ModVerifyOptionsParser.cs create mode 100644 src/ModVerify.CliApp/Settings/CommandLine/VerifyVerbOption.cs rename src/ModVerify.CliApp/{Options => Settings}/GameInstallationsSettings.cs (74%) rename src/ModVerify.CliApp/{Options => Settings}/ModVerifyAppSettings.cs (72%) create mode 100644 src/ModVerify.CliApp/Settings/ModVerifyReportSettings.cs rename src/ModVerify.CliApp/{ => Settings}/SettingsBuilder.cs (60%) rename src/ModVerify.CliApp/Updates/{ => Github}/GithubReleaseEntry.cs (91%) rename src/ModVerify.CliApp/Updates/{ => Github}/GithubReleaseList.cs (70%) rename src/ModVerify.CliApp/Updates/{ModVerifyUpdaterChecker.cs => Github/GithubUpdateChecker.cs} (60%) create mode 100644 src/ModVerify.CliApp/Updates/Github/GithubUpdateConstants.cs rename src/ModVerify.CliApp/Updates/{UpdateInfo.cs => Github/GithubUpdateInfo.cs} (75%) create mode 100644 src/ModVerify.CliApp/Updates/ModVerifyUpdateMode.cs create mode 100644 src/ModVerify.CliApp/Updates/ModVerifyUpdater.cs delete mode 100644 src/ModVerify.CliApp/Updates/ModVerifyUpdaterInformation.cs create mode 100644 src/ModVerify.CliApp/Updates/SelfUpdate/AssemblyInfo.cs create mode 100644 src/ModVerify.CliApp/Updates/SelfUpdate/ModVerifyApplicationUpdater.cs create mode 100644 src/ModVerify.CliApp/Updates/SelfUpdate/ModVerifyUpdateResultHandler.cs create mode 100644 src/ModVerify.CliApp/Utilities/ExtensionMethods.cs create mode 100644 src/ModVerify.CliApp/Utilities/ModVerifyConsoleUtilities.cs create mode 100644 src/ModVerify.CliApp/Utilities/Spinner.cs delete mode 100644 src/ModVerify/Reporting/IncompatibleBaselineException.cs create mode 100644 src/ModVerify/Reporting/InvalidBaselineException.cs create mode 100644 src/ModVerify/Reporting/Json/JsonBaselineParser.cs create mode 100644 src/ModVerify/Reporting/Json/JsonBaselineSchema.cs create mode 100644 src/ModVerify/Resources/Schemas/2.0/baseline.json create mode 100644 test/ModVerify.CliApp.Test/CommonTestBase.cs create mode 100644 test/ModVerify.CliApp.Test/EmbeddedBaselineTest.cs create mode 100644 test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj create mode 100644 test/ModVerify.CliApp.Test/ModVerifyOptionsParserTest.cs create mode 100644 test/ModVerify.CliApp.Test/TestData/NoVerifierProvider.cs create mode 100644 test/ModVerify.CliApp.Test/TestData/TestEnv.cs create mode 100644 test/ModVerify.CliApp.Test/TestData/UpdatableEnv.cs create mode 100644 test/ModVerify.CliApp.Test/Utilities/StringExtensions.cs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7369175..7a9b844 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,8 +13,8 @@ on: env: TOOL_PROJ_PATH: ./src/ModVerify.CliApp/ModVerify.CliApp.csproj - CREATOR_PROJ_PATH: ./Modules/ModdingToolBase/src/AnakinApps/ApplicationManifestCreator/ApplicationManifestCreator.csproj - UPLOADER_PROJ_PATH: ./Modules/ModdingToolBase/src/AnakinApps/FtpUploader/FtpUploader.csproj + CREATOR_PROJ_PATH: ./modules/ModdingToolBase/src/AnakinApps/ApplicationManifestCreator/ApplicationManifestCreator.csproj + UPLOADER_PROJ_PATH: ./modules/ModdingToolBase/src/AnakinApps/FtpUploader/FtpUploader.csproj TOOL_EXE: ModVerify.exe UPDATER_EXE: AnakinRaW.ExternalUpdater.exe MANIFEST_CREATOR: AnakinRaW.ApplicationManifestCreator.dll @@ -35,18 +35,20 @@ jobs: runs-on: windows-latest steps: - name: Checkout sources - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 submodules: recursive - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 - name: Create NetFramework Release - run: dotnet publish ${{ env.TOOL_PROJ_PATH }} --configuration Release -f net48 --output ./releases/net48 /p:DebugType=None /p:DebugSymbols=false + # use build for .NET Framework to enusre external updatere .EXE is included + run: dotnet build ${{ env.TOOL_PROJ_PATH }} --configuration Release -f net481 --output ./releases/net481 /p:DebugType=None /p:DebugSymbols=false - name: Create Net Core Release - run: dotnet publish ${{ env.TOOL_PROJ_PATH }} --configuration Release -f net9.0 --output ./releases/net9.0 /p:DebugType=None /p:DebugSymbols=false + # use publish for .NET Core + run: dotnet publish ${{ env.TOOL_PROJ_PATH }} --configuration Release -f net10.0 --output ./releases/net10.0 /p:DebugType=None /p:DebugSymbols=false - name: Upload a Build Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Binary Releases path: ./releases @@ -62,17 +64,45 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: actions/download-artifact@v5 + submodules: recursive + - uses: actions/download-artifact@v6 with: name: Binary Releases path: ./releases + + # Deploy .NET Framework self-update release + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + - name: Build Creator + run: dotnet build ${{env.CREATOR_PROJ_PATH}} --configuration Release --output ./dev + - name: Build Uploader + run: dotnet build ${{env.UPLOADER_PROJ_PATH}} --configuration Release --output ./dev + - name: Create binaries directory + run: mkdir -p ./deploy + - name: Copy self-update files + run: | + cp ./releases/net481/${{env.TOOL_EXE}} ./deploy/ + cp ./releases/net481/${{env.UPDATER_EXE}} ./deploy/ + - name: Create Manifest + run: dotnet ./dev/${{env.MANIFEST_CREATOR}} -a deploy/${{env.TOOL_EXE}} --appDataFiles deploy/${{env.UPDATER_EXE}} --origin ${{env.ORIGIN_BASE}} -o ./deploy -b ${{env.BRANCH_NAME}} + - name: Upload Build + run: dotnet ./dev/${{env.SFTP_UPLOADER}} ftp --host $host --port $port -u ${{secrets.SFTP_USER}} -p ${{secrets.SFTP_PASSWORD}} --base $base_path -s $source + env: + host: republicatwar.com + port: 1579 + base_path: ${{env.ORIGIN_BASE_PART}} + source: ./deploy + + # Deploy .NET Core and .NET Framework apps to Github - name: Create NET Core .zip # Change into the artifacts directory to avoid including the directory itself in the zip archive - working-directory: ./releases/net9.0 - run: zip -r ../ModVerify-Net9.zip . + working-directory: ./releases/net10.0 + run: zip -r ../ModVerify-Net10.zip . - uses: dotnet/nbgv@v0.4.2 id: nbgv - name: Create GitHub release @@ -86,5 +116,5 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} generate_release_notes: true files: | - ./releases/net48/ModVerify.exe - ./releases/ModVerify-Net9.zip \ No newline at end of file + ./releases/net481/ModVerify.exe + ./releases/ModVerify-Net10.zip \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ed2af48..6e60fde 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,12 +19,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 submodules: recursive - uses: actions/setup-dotnet@v4 with: - dotnet-version: 9.0.x + dotnet-version: 10.0.x - name: Build & Test in Release Mode run: dotnet test --configuration Release \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..87124a4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "modules/ModdingToolBase"] + path = modules/ModdingToolBase + url = https://github.com/AnakinRaW/ModdingToolBase diff --git a/Directory.Build.props b/Directory.Build.props index 49ca26f..d475e7f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -24,7 +24,7 @@ - latest + preview disable enable True @@ -39,7 +39,7 @@ all - 3.7.115 + 3.9.50 diff --git a/ModVerify.sln b/ModVerify.sln deleted file mode 100644 index d09a64e..0000000 --- a/ModVerify.sln +++ /dev/null @@ -1,63 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.11.34909.67 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PetroglyphTools", "PetroglyphTools", "{15F8B753-814A-406E-9147-EB048DADAC96}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModVerify", "src\ModVerify\ModVerify.csproj", "{22ED0E2C-FF3B-40EB-9CE2-DCDE65CDF31B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModVerify.CliApp", "src\ModVerify.CliApp\ModVerify.CliApp.csproj", "{84479931-A329-4113-9BE5-90B71E5486E6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PG.StarWarsGame.Files.ChunkFiles", "src\PetroglyphTools\PG.StarWarsGame.Files.ChunkFiles\PG.StarWarsGame.Files.ChunkFiles.csproj", "{92F2A0C8-61B6-424B-99D5-7898CDBA7CA6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PG.StarWarsGame.Files.ALO", "src\PetroglyphTools\PG.StarWarsGame.Files.ALO\PG.StarWarsGame.Files.ALO.csproj", "{DF76A383-C94E-4D03-A07C-22D61ED37059}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PG.StarWarsGame.Files.XML", "src\PetroglyphTools\PG.StarWarsGame.Files.XML\PG.StarWarsGame.Files.XML.csproj", "{418C68FA-531B-432E-8459-6433181C8AD3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PG.StarWarsGame.Engine", "src\PetroglyphTools\PG.StarWarsGame.Engine\PG.StarWarsGame.Engine.csproj", "{DFD62F61-3455-44BE-BB7C-E954FF48534B}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {22ED0E2C-FF3B-40EB-9CE2-DCDE65CDF31B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {22ED0E2C-FF3B-40EB-9CE2-DCDE65CDF31B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {22ED0E2C-FF3B-40EB-9CE2-DCDE65CDF31B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {22ED0E2C-FF3B-40EB-9CE2-DCDE65CDF31B}.Release|Any CPU.Build.0 = Release|Any CPU - {84479931-A329-4113-9BE5-90B71E5486E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {84479931-A329-4113-9BE5-90B71E5486E6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {84479931-A329-4113-9BE5-90B71E5486E6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {84479931-A329-4113-9BE5-90B71E5486E6}.Release|Any CPU.Build.0 = Release|Any CPU - {92F2A0C8-61B6-424B-99D5-7898CDBA7CA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {92F2A0C8-61B6-424B-99D5-7898CDBA7CA6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {92F2A0C8-61B6-424B-99D5-7898CDBA7CA6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {92F2A0C8-61B6-424B-99D5-7898CDBA7CA6}.Release|Any CPU.Build.0 = Release|Any CPU - {DF76A383-C94E-4D03-A07C-22D61ED37059}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DF76A383-C94E-4D03-A07C-22D61ED37059}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DF76A383-C94E-4D03-A07C-22D61ED37059}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DF76A383-C94E-4D03-A07C-22D61ED37059}.Release|Any CPU.Build.0 = Release|Any CPU - {418C68FA-531B-432E-8459-6433181C8AD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {418C68FA-531B-432E-8459-6433181C8AD3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {418C68FA-531B-432E-8459-6433181C8AD3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {418C68FA-531B-432E-8459-6433181C8AD3}.Release|Any CPU.Build.0 = Release|Any CPU - {DFD62F61-3455-44BE-BB7C-E954FF48534B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DFD62F61-3455-44BE-BB7C-E954FF48534B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DFD62F61-3455-44BE-BB7C-E954FF48534B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DFD62F61-3455-44BE-BB7C-E954FF48534B}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {92F2A0C8-61B6-424B-99D5-7898CDBA7CA6} = {15F8B753-814A-406E-9147-EB048DADAC96} - {DF76A383-C94E-4D03-A07C-22D61ED37059} = {15F8B753-814A-406E-9147-EB048DADAC96} - {418C68FA-531B-432E-8459-6433181C8AD3} = {15F8B753-814A-406E-9147-EB048DADAC96} - {DFD62F61-3455-44BE-BB7C-E954FF48534B} = {15F8B753-814A-406E-9147-EB048DADAC96} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {D74A22E2-91F1-4BC7-9630-3CF930B45408} - EndGlobalSection -EndGlobal diff --git a/ModVerify.slnx b/ModVerify.slnx new file mode 100644 index 0000000..3527ff4 --- /dev/null +++ b/ModVerify.slnx @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/ModdingToolBase b/modules/ModdingToolBase new file mode 160000 index 0000000..479a088 --- /dev/null +++ b/modules/ModdingToolBase @@ -0,0 +1 @@ +Subproject commit 479a088a2b26dd4a3e2342b2e34f5359b0252e88 diff --git a/src/ModVerify.CliApp/ConsoleUtilities.cs b/src/ModVerify.CliApp/ConsoleUtilities.cs deleted file mode 100644 index db3e741..0000000 --- a/src/ModVerify.CliApp/ConsoleUtilities.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; - -namespace AET.ModVerifyTool; - -internal static class ConsoleUtilities -{ - public delegate bool ConsoleQuestionValueFactory(string input, out T value); - - public static void WriteHorizontalLine(char lineChar = '─', int length = 20) - { - var line = new string(lineChar, length); - Console.WriteLine(line); - } - - public static void WriteHeader() - { - Console.WriteLine("***********************************"); - Console.WriteLine("***********************************"); - Console.WriteLine(Figgle.FiggleFonts.Standard.Render("Mod Verify")); - Console.WriteLine("***********************************"); - Console.WriteLine("***********************************"); - Console.WriteLine(" by AnakinRaW"); - Console.WriteLine(); - Console.WriteLine(); - } - - public static void WriteApplicationFailure() - { - Console.WriteLine(); - WriteHorizontalLine('*'); - Console.ForegroundColor = ConsoleColor.DarkRed; - Console.WriteLine(" ModVerify Failure! "); - Console.ResetColor(); - WriteHorizontalLine('*'); - Console.WriteLine(); - Console.WriteLine("The application encountered an unexpected error and will terminate now!"); - Console.WriteLine(); - } - - public static T UserQuestionOnSameLine(string question, ConsoleQuestionValueFactory inputCorrect) - { - while (true) - { - var promptLeft = 0; - var promptTop = Console.CursorTop; - - Console.SetCursorPosition(promptLeft, promptTop); - Console.Write(question); - Console.SetCursorPosition(promptLeft + question.Length, promptTop); - - var input = ReadLineInline(); - - if (!inputCorrect(input, out var result)) - { - Console.SetCursorPosition(0, promptTop); - Console.Write(new string(' ', Console.WindowWidth - 1)); - continue; - } - - Console.WriteLine(); - return result; - } - } - - private static string ReadLineInline() - { - var input = ""; - while (true) - { - var key = Console.ReadKey(intercept: true); - - if (key.Key == ConsoleKey.Enter) - break; - - if (key.Key == ConsoleKey.Backspace) - { - if (input.Length > 0) - { - input = input[..^1]; - Console.SetCursorPosition(Console.CursorLeft - 1, Console.CursorTop); - Console.Write(' '); - Console.SetCursorPosition(Console.CursorLeft - 1, Console.CursorTop); - } - } - else if (!char.IsControl(key.KeyChar)) - { - input += key.KeyChar; - Console.Write(key.KeyChar); - } - } - - return input; - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ExtensionMethods.cs b/src/ModVerify.CliApp/ExtensionMethods.cs deleted file mode 100644 index b7e20c8..0000000 --- a/src/ModVerify.CliApp/ExtensionMethods.cs +++ /dev/null @@ -1,17 +0,0 @@ -using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Infrastructure.Games; - -namespace AET.ModVerifyTool; - -internal static class ExtensionMethods -{ - public static GameEngineType ToEngineType(this GameType type) - { - return type == GameType.Foc ? GameEngineType.Foc : GameEngineType.Eaw; - } - - public static GameType FromEngineType(this GameEngineType type) - { - return type == GameEngineType.Foc ? GameType.Foc : GameType.Eaw; - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/GameFinder/GameFinderResult.cs b/src/ModVerify.CliApp/GameFinder/GameFinderResult.cs index 3135955..beb3964 100644 --- a/src/ModVerify.CliApp/GameFinder/GameFinderResult.cs +++ b/src/ModVerify.CliApp/GameFinder/GameFinderResult.cs @@ -1,5 +1,5 @@ using PG.StarWarsGame.Infrastructure.Games; -namespace AET.ModVerifyTool.GameFinder; +namespace AET.ModVerify.App.GameFinder; internal record GameFinderResult(IGame Game, IGame? FallbackGame); \ No newline at end of file diff --git a/src/ModVerify.CliApp/GameFinder/GameFinderService.cs b/src/ModVerify.CliApp/GameFinder/GameFinderService.cs index 78fe408..a88ebd9 100644 --- a/src/ModVerify.CliApp/GameFinder/GameFinderService.cs +++ b/src/ModVerify.CliApp/GameFinder/GameFinderService.cs @@ -10,7 +10,7 @@ using PG.StarWarsGame.Infrastructure.Services; using PG.StarWarsGame.Infrastructure.Services.Detection; -namespace AET.ModVerifyTool.GameFinder; +namespace AET.ModVerify.App.GameFinder; internal class GameFinderService { @@ -79,7 +79,7 @@ private bool TryDetectGame(GameType gameType, IList detectors, ou catch (Exception e) { result = GameDetectionResult.NotInstalled(gameType); - _logger?.LogTrace($"Unable to find game installation: {e.Message}"); + _logger?.LogTrace("Unable to find game installation: {Message}", e.Message); return false; } } @@ -97,7 +97,8 @@ private GameFinderResult FindGames(IList detectors) if (result.GameLocation is null) throw new GameNotFoundException("Unable to find game installation: Wrong install path?"); - _logger?.LogInformation($"Found game installation: {result.GameIdentity} at {result.GameLocation.FullName}"); + _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, + "Found game installation: {ResultGameIdentity} at {GameLocationFullName}", result.GameIdentity, result.GameLocation.FullName); var game = _gameFactory.CreateGame(result, CultureInfo.InvariantCulture); @@ -118,7 +119,8 @@ private GameFinderResult FindGames(IList detectors) if (!TryDetectGame(GameType.Eaw, fallbackDetectors, out var fallbackResult) || fallbackResult.GameLocation is null) throw new GameNotFoundException("Unable to find fallback game installation: Wrong install path?"); - _logger?.LogInformation($"Found fallback game installation: {fallbackResult.GameIdentity} at {fallbackResult.GameLocation.FullName}"); + _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, + "Found fallback game installation: {FallbackResultGameIdentity} at {GameLocationFullName}", fallbackResult.GameIdentity, fallbackResult.GameLocation.FullName); fallbackGame = _gameFactory.CreateGame(fallbackResult, CultureInfo.InvariantCulture); diff --git a/src/ModVerify.CliApp/GameNotFoundException.cs b/src/ModVerify.CliApp/GameFinder/GameNotFoundException.cs similarity index 76% rename from src/ModVerify.CliApp/GameNotFoundException.cs rename to src/ModVerify.CliApp/GameFinder/GameNotFoundException.cs index 21cdc81..37b0386 100644 --- a/src/ModVerify.CliApp/GameNotFoundException.cs +++ b/src/ModVerify.CliApp/GameFinder/GameNotFoundException.cs @@ -1,5 +1,5 @@ using PG.StarWarsGame.Infrastructure.Games; -namespace AET.ModVerifyTool; +namespace AET.ModVerify.App.GameFinder; internal class GameNotFoundException(string message) : GameException(message); \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectors/AutomaticModSelector.cs b/src/ModVerify.CliApp/ModSelectors/AutomaticModSelector.cs index 00fc4ae..717db7b 100644 --- a/src/ModVerify.CliApp/ModSelectors/AutomaticModSelector.cs +++ b/src/ModVerify.CliApp/ModSelectors/AutomaticModSelector.cs @@ -2,8 +2,9 @@ using System.Globalization; using System.IO.Abstractions; using System.Linq; -using AET.ModVerifyTool.GameFinder; -using AET.ModVerifyTool.Options; +using AET.ModVerify.App.GameFinder; +using AET.ModVerify.App.Settings; +using AET.ModVerify.App.Utilities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using PG.StarWarsGame.Engine; @@ -13,7 +14,7 @@ using PG.StarWarsGame.Infrastructure.Services; using PG.StarWarsGame.Infrastructure.Services.Detection; -namespace AET.ModVerifyTool.ModSelectors; +namespace AET.ModVerify.App.ModSelectors; internal class AutomaticModSelector(IServiceProvider serviceProvider) : ModSelectorBase(serviceProvider) { @@ -37,7 +38,7 @@ internal class AutomaticModSelector(IServiceProvider serviceProvider) : ModSelec } catch (GameNotFoundException) { - Logger?.LogError($"Unable to find games based of the given location '{settings.GamePath}'. Consider specifying all paths manually."); + Logger?.LogError(ModVerifyConstants.ConsoleEventId, "Unable to find games based of the given location '{SettingsGamePath}'. Consider specifying all paths manually.", settings.GamePath); targetObject = null!; return null; } @@ -59,7 +60,7 @@ internal class AutomaticModSelector(IServiceProvider serviceProvider) : ModSelec if (!settings.EngineType.HasValue) throw new ArgumentException("Unable to determine game type. Use --type argument to set the game type."); - Logger?.LogDebug($"The requested mod at '{pathToVerify}' is detached from its games."); + Logger?.LogDebug("The requested mod at '{PathToVerify}' is detached from its games.", pathToVerify); // The path is a detached mod, that exists on a different location than the game. var result = GetDetachedModLocations(pathToVerify, finderResult, settings, out var mod); diff --git a/src/ModVerify.CliApp/ModSelectors/ConsoleModSelector.cs b/src/ModVerify.CliApp/ModSelectors/ConsoleModSelector.cs index 7a29368..c776d6d 100644 --- a/src/ModVerify.CliApp/ModSelectors/ConsoleModSelector.cs +++ b/src/ModVerify.CliApp/ModSelectors/ConsoleModSelector.cs @@ -1,14 +1,16 @@ using System; using System.Collections.Generic; using AET.Modinfo.Spec; -using AET.ModVerifyTool.GameFinder; -using AET.ModVerifyTool.Options; +using AET.ModVerify.App.GameFinder; +using AET.ModVerify.App.Settings; +using AET.ModVerify.App.Utilities; +using AnakinRaW.ApplicationBase; using PG.StarWarsGame.Engine; using PG.StarWarsGame.Infrastructure; using PG.StarWarsGame.Infrastructure.Games; using PG.StarWarsGame.Infrastructure.Mods; -namespace AET.ModVerifyTool.ModSelectors; +namespace AET.ModVerify.App.ModSelectors; internal class ConsoleModSelector(IServiceProvider serviceProvider) : ModSelectorBase(serviceProvider) { @@ -100,7 +102,7 @@ private static IPhysicalPlayableObject SelectPlayableObject(GameFinderResult fin if (!int.TryParse(input, out value)) return false; - return value <= list.Count; + return value <= list.Count && value >= 0; }); return list[selected]; } diff --git a/src/ModVerify.CliApp/ModSelectors/IModSelector.cs b/src/ModVerify.CliApp/ModSelectors/IModSelector.cs index f0b30a0..a04858c 100644 --- a/src/ModVerify.CliApp/ModSelectors/IModSelector.cs +++ b/src/ModVerify.CliApp/ModSelectors/IModSelector.cs @@ -1,8 +1,8 @@ -using AET.ModVerifyTool.Options; +using AET.ModVerify.App.Settings; using PG.StarWarsGame.Engine; using PG.StarWarsGame.Infrastructure; -namespace AET.ModVerifyTool.ModSelectors; +namespace AET.ModVerify.App.ModSelectors; internal interface IModSelector { diff --git a/src/ModVerify.CliApp/ModSelectors/ManualModSelector.cs b/src/ModVerify.CliApp/ModSelectors/ManualModSelector.cs index d5913c6..34cf39d 100644 --- a/src/ModVerify.CliApp/ModSelectors/ManualModSelector.cs +++ b/src/ModVerify.CliApp/ModSelectors/ManualModSelector.cs @@ -1,9 +1,9 @@ using System; -using AET.ModVerifyTool.Options; +using AET.ModVerify.App.Settings; using PG.StarWarsGame.Engine; using PG.StarWarsGame.Infrastructure; -namespace AET.ModVerifyTool.ModSelectors; +namespace AET.ModVerify.App.ModSelectors; internal class ManualModSelector(IServiceProvider serviceProvider) : ModSelectorBase(serviceProvider) { diff --git a/src/ModVerify.CliApp/ModSelectors/ModSelectorBase.cs b/src/ModVerify.CliApp/ModSelectors/ModSelectorBase.cs index 0eec285..8dd1d90 100644 --- a/src/ModVerify.CliApp/ModSelectors/ModSelectorBase.cs +++ b/src/ModVerify.CliApp/ModSelectors/ModSelectorBase.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; -using AET.ModVerifyTool.GameFinder; -using AET.ModVerifyTool.Options; +using AET.ModVerify.App.GameFinder; +using AET.ModVerify.App.Settings; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using PG.StarWarsGame.Engine; @@ -10,7 +10,7 @@ using PG.StarWarsGame.Infrastructure.Mods; using PG.StarWarsGame.Infrastructure.Services.Dependencies; -namespace AET.ModVerifyTool.ModSelectors; +namespace AET.ModVerify.App.ModSelectors; internal abstract class ModSelectorBase : IModSelector { diff --git a/src/ModVerify.CliApp/ModSelectors/ModSelectorFactory.cs b/src/ModVerify.CliApp/ModSelectors/ModSelectorFactory.cs index 8335a86..07ef263 100644 --- a/src/ModVerify.CliApp/ModSelectors/ModSelectorFactory.cs +++ b/src/ModVerify.CliApp/ModSelectors/ModSelectorFactory.cs @@ -1,7 +1,7 @@ using System; -using AET.ModVerifyTool.Options; +using AET.ModVerify.App.Settings; -namespace AET.ModVerifyTool.ModSelectors; +namespace AET.ModVerify.App.ModSelectors; internal class ModSelectorFactory(IServiceProvider serviceProvider) { diff --git a/src/ModVerify.CliApp/ModSelectors/SettingsBasedModSelector.cs b/src/ModVerify.CliApp/ModSelectors/SettingsBasedModSelector.cs index 12f7861..221bb9e 100644 --- a/src/ModVerify.CliApp/ModSelectors/SettingsBasedModSelector.cs +++ b/src/ModVerify.CliApp/ModSelectors/SettingsBasedModSelector.cs @@ -1,14 +1,15 @@ using System; using System.Linq; -using AET.ModVerifyTool.Options; +using AET.ModVerify.App.GameFinder; +using AET.ModVerify.App.Settings; using PG.StarWarsGame.Engine; using PG.StarWarsGame.Infrastructure; -namespace AET.ModVerifyTool.ModSelectors; +namespace AET.ModVerify.App.ModSelectors; internal class SettingsBasedModSelector(IServiceProvider serviceProvider) { - public VerifyInstallationInformation CreateInstallationDataFromSettings(GameInstallationsSettings settings) + public VerifyInstallationData CreateInstallationDataFromSettings(GameInstallationsSettings settings) { var gameLocations = new ModSelectorFactory(serviceProvider) .CreateSelector(settings) @@ -20,7 +21,7 @@ public VerifyInstallationInformation CreateInstallationDataFromSettings(GameInst if (engineType is null) throw new InvalidOperationException("Engine type not specified."); - return new VerifyInstallationInformation + return new VerifyInstallationData { EngineType = engineType.Value, GameLocations = gameLocations, diff --git a/src/ModVerify.CliApp/VerifyInstallationInformation.cs b/src/ModVerify.CliApp/ModSelectors/VerifyInstallationData.cs similarity index 90% rename from src/ModVerify.CliApp/VerifyInstallationInformation.cs rename to src/ModVerify.CliApp/ModSelectors/VerifyInstallationData.cs index 66816ab..1a1fcd2 100644 --- a/src/ModVerify.CliApp/VerifyInstallationInformation.cs +++ b/src/ModVerify.CliApp/ModSelectors/VerifyInstallationData.cs @@ -1,9 +1,9 @@ using System.Text; using PG.StarWarsGame.Engine; -namespace AET.ModVerifyTool; +namespace AET.ModVerify.App.ModSelectors; -internal sealed class VerifyInstallationInformation +internal sealed class VerifyInstallationData { public required string Name { get; init; } diff --git a/src/ModVerify.CliApp/ModVerify.CliApp.csproj b/src/ModVerify.CliApp/ModVerify.CliApp.csproj index 3ca41b6..0073b2b 100644 --- a/src/ModVerify.CliApp/ModVerify.CliApp.csproj +++ b/src/ModVerify.CliApp/ModVerify.CliApp.csproj @@ -1,9 +1,9 @@  - net9.0;net48 + net10.0;net481 Exe - AET.ModVerifyTool + AET.ModVerify.App ModVerify $(RepoRootPath)aet.ico AlamoEngineTools.ModVerify.CliApp @@ -22,42 +22,46 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - @@ -65,11 +69,16 @@ + + + + + - - + + diff --git a/src/ModVerify.CliApp/ModVerifyApp.cs b/src/ModVerify.CliApp/ModVerifyApp.cs deleted file mode 100644 index 5cefc40..0000000 --- a/src/ModVerify.CliApp/ModVerifyApp.cs +++ /dev/null @@ -1,155 +0,0 @@ -using AET.ModVerify; -using AET.ModVerify.Reporting; -using AET.ModVerifyTool.ModSelectors; -using AET.ModVerifyTool.Options; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using AET.ModVerify.Pipeline; -using AET.ModVerifyTool.Reporting; -using PG.StarWarsGame.Engine; - -namespace AET.ModVerifyTool; - -internal class ModVerifyApp(ModVerifyAppSettings settings, IServiceProvider services) -{ - private readonly ILogger? _logger = services.GetService()?.CreateLogger(typeof(ModVerifyApp)); - private readonly IFileSystem _fileSystem = services.GetRequiredService(); - - public async Task RunApplication() - { - var installData = new SettingsBasedModSelector(services) - .CreateInstallationDataFromSettings(settings.GameInstallationsSettings); - - _logger?.LogDebug($"Verify install data: {installData}"); - _logger?.LogTrace($"Verify settings: {settings}"); - - var allErrors = await Verify(installData).ConfigureAwait(false); - - try - { - await ReportErrors(allErrors).ConfigureAwait(false); - } - catch (GameVerificationException e) - { - return e.HResult; - } - - if (!settings.CreateNewBaseline) - return 0; - - await WriteBaseline(allErrors, settings.NewBaselinePath).ConfigureAwait(false); - _logger?.LogInformation("Baseline successfully created."); - - return 0; - } - - private async Task> Verify(VerifyInstallationInformation installInformation) - { - var gameEngineService = services.GetRequiredService(); - var engineErrorReporter = new ConcurrentGameEngineErrorReporter(); - - IStarWarsGameEngine gameEngine; - - try - { - var initProgress = new Progress(); - var initProgressReporter = new EngineInitializeProgressReporter(initProgress); - - try - { - _logger?.LogInformation($"Creating Game Engine '{installInformation.EngineType}'"); - gameEngine = await gameEngineService.InitializeAsync( - installInformation.EngineType, - installInformation.GameLocations, - engineErrorReporter, - initProgress, - false, - CancellationToken.None).ConfigureAwait(false); - _logger?.LogInformation("Game Engine created"); - } - finally - { - initProgressReporter.Dispose(); - } - } - catch (Exception e) - { - _logger?.LogError(e, $"Creating game engine failed: {e.Message}"); - throw; - } - - var progressReporter = new VerifyConsoleProgressReporter(installInformation.Name); - - using var verifyPipeline = new GameVerifyPipeline( - gameEngine, - engineErrorReporter, - settings.VerifyPipelineSettings, - settings.GlobalReportSettings, - progressReporter, - services); - - try - { - try - { - _logger?.LogInformation($"Verifying '{installInformation.Name}'..."); - await verifyPipeline.RunAsync().ConfigureAwait(false); - progressReporter.Report(string.Empty, 1.0); - } - catch - { - progressReporter.ReportError("Verification failed", null); - throw; - } - finally - { - progressReporter.Dispose(); - } - } - catch (OperationCanceledException) - { - _logger?.LogWarning("Verification stopped due to enabled failFast setting."); - } - catch (Exception e) - { - _logger?.LogError(e, $"Verification failed: {e.Message}"); - throw; - } - - _logger?.LogInformation("Finished verification"); - return verifyPipeline.FilteredErrors; - } - - private async Task ReportErrors(IReadOnlyCollection errors) - { - _logger?.LogInformation("Reporting Errors..."); - - var reportBroker = new VerificationReportBroker(services); - - await reportBroker.ReportAsync(errors); - - if (errors.Any(x => x.Severity >= settings.AppThrowsOnMinimumSeverity)) - throw new GameVerificationException(errors); - } - - private async Task WriteBaseline(IEnumerable errors, string baselineFile) - { - var baseline = new VerificationBaseline(settings.GlobalReportSettings.MinimumReportSeverity, errors); - - var fullPath = _fileSystem.Path.GetFullPath(baselineFile); - _logger?.LogInformation($"Writing Baseline to '{fullPath}'"); - -#if NET - await -#endif - using var fs = _fileSystem.FileStream.New(fullPath, FileMode.Create, FileAccess.Write, FileShare.None); - await baseline.ToJsonAsync(fs); - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModVerifyAppEnvironment.cs b/src/ModVerify.CliApp/ModVerifyAppEnvironment.cs new file mode 100644 index 0000000..86bfc40 --- /dev/null +++ b/src/ModVerify.CliApp/ModVerifyAppEnvironment.cs @@ -0,0 +1,71 @@ +using System.IO.Abstractions; +using System.Reflection; +using AnakinRaW.ApplicationBase.Environment; +#if !NET +using System; +using System.Collections.Generic; +using AnakinRaW.AppUpdaterFramework.Configuration; +using AnakinRaW.CommonUtilities.DownloadManager.Configuration; +#endif + +namespace AET.ModVerify.App; + +internal sealed class ModVerifyAppEnvironment(Assembly assembly, IFileSystem fileSystem) +#if NET + : ApplicationEnvironment(assembly, fileSystem) +#else + : UpdatableApplicationEnvironment(assembly, fileSystem) +#endif +{ + public override string ApplicationName => ModVerifyConstants.AppNameString; + + protected override string ApplicationLocalDirectoryName => ModVerifyConstants.ModVerifyToolPath; + +#if NETFRAMEWORK + + public override ICollection UpdateMirrors { get; } = new List + { +#if DEBUG + new("C:\\Test\\ModVerify"), +#endif + new($"https://republicatwar.com/downloads/{ModVerifyConstants.ModVerifyToolPath}") + }; + + public override string UpdateRegistryPath => $@"SOFTWARE\{ModVerifyConstants.ModVerifyToolPath}\Update"; + + protected override UpdateConfiguration CreateUpdateConfiguration() + { + return new UpdateConfiguration + { + DownloadLocation = FileSystem.Path.Combine(ApplicationLocalPath, "downloads"), + BackupLocation = FileSystem.Path.Combine(ApplicationLocalPath, "backups"), + BackupPolicy = BackupPolicy.Required, + ComponentDownloadConfiguration = new DownloadManagerConfiguration + { + AllowEmptyFileDownload = false, + DownloadRetryDelay = 500, + ValidationPolicy = ValidationPolicy.Required + }, + ManifestDownloadConfiguration = new DownloadManagerConfiguration + { + AllowEmptyFileDownload = false, + DownloadRetryDelay = 500, + ValidationPolicy = ValidationPolicy.Optional + }, + BranchDownloadConfiguration = new DownloadManagerConfiguration + { + AllowEmptyFileDownload = false, + DownloadRetryDelay = 500, + ValidationPolicy = ValidationPolicy.NoValidation + }, + DownloadRetryCount = 3, + RestartConfiguration = new UpdateRestartConfiguration + { + SupportsRestart = true, + PassCurrentArgumentsForRestart = true + }, + ValidateInstallation = true + }; + } +#endif +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModVerifyApplication.cs b/src/ModVerify.CliApp/ModVerifyApplication.cs new file mode 100644 index 0000000..7ea461c --- /dev/null +++ b/src/ModVerify.CliApp/ModVerifyApplication.cs @@ -0,0 +1,251 @@ +using AET.ModVerify.App.ModSelectors; +using AET.ModVerify.App.Reporting; +using AET.ModVerify.App.Settings; +using AET.ModVerify.Pipeline; +using AET.ModVerify.Reporting; +using AET.ModVerify.Reporting.Settings; +using AnakinRaW.ApplicationBase; +using AnakinRaW.ApplicationBase.Utilities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine; +using Serilog; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AET.ModVerify.App.GameFinder; +using ILogger = Microsoft.Extensions.Logging.ILogger; + +namespace AET.ModVerify.App; + +internal sealed class ModVerifyApplication(ModVerifyAppSettings settings, IServiceProvider services) +{ + private readonly ILogger? _logger = services.GetService()?.CreateLogger(typeof(ModVerifyApplication)); + private readonly IFileSystem _fileSystem = services.GetRequiredService(); + private readonly ModVerifyAppEnvironment _appEnvironment = services.GetRequiredService(); + + public async Task Run() + { + using (new UnhandledExceptionHandler(services)) + using (new UnobservedTaskExceptionHandler(services)) + return await RunCore().ConfigureAwait(false); + } + + private async Task RunCore() + { + _logger?.LogDebug("Raw command line: {CommandLine}", Environment.CommandLine); + + var interactive = settings.Interactive; + try + { + return await RunVerify().ConfigureAwait(false); + } + catch (Exception e) + { + _logger?.LogCritical(e, e.Message); + ConsoleUtilities.WriteApplicationFatalError(ModVerifyConstants.AppNameString, e); + return e.HResult; + } + finally + { +#if NET + await Log.CloseAndFlushAsync(); +#else + Log.CloseAndFlush(); +#endif + if (interactive) + { + Console.WriteLine(); + ConsoleUtilities.WriteHorizontalLine('-'); + Console.WriteLine("Press any key to exit"); + Console.ReadLine(); + } + } + } + + + private async Task RunVerify() + { + VerifyInstallationData installData; + try + { + installData = new SettingsBasedModSelector(services) + .CreateInstallationDataFromSettings(settings.GameInstallationsSettings); + } + catch (GameNotFoundException ex) + { + ConsoleUtilities.WriteApplicationFatalError(_appEnvironment.ApplicationName, + "Unable to find an installation of Empire at War or Forces of Corruption."); + _logger?.LogError(ex, "Game not found: {Message}", ex.Message); + return ex.HResult; + } + + var reportSettings = CreateGlobalReportSettings(installData); + + _logger?.LogDebug("Verify install data: {InstallData}", installData); + _logger?.LogTrace("Verify settings: {Settings}", settings); + + var allErrors = await Verify(installData, reportSettings) + .ConfigureAwait(false); + + try + { + await ReportErrors(allErrors).ConfigureAwait(false); + } + catch (GameVerificationException e) + { + return e.HResult; + } + + if (!settings.CreateNewBaseline) + return 0; + + await WriteBaseline(reportSettings, allErrors, settings.NewBaselinePath).ConfigureAwait(false); + _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Baseline successfully created."); + + return 0; + } + + private async Task> Verify( + VerifyInstallationData installData, + GlobalVerifyReportSettings reportSettings) + { + var gameEngineService = services.GetRequiredService(); + var engineErrorReporter = new ConcurrentGameEngineErrorReporter(); + + IStarWarsGameEngine gameEngine; + + try + { + var initProgress = new Progress(); + var initProgressReporter = new EngineInitializeProgressReporter(initProgress); + + try + { + _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Creating Game Engine '{Engine}'", installData.EngineType); + gameEngine = await gameEngineService.InitializeAsync( + installData.EngineType, + installData.GameLocations, + engineErrorReporter, + initProgress, + false, + CancellationToken.None).ConfigureAwait(false); + _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Game Engine created"); + } + finally + { + initProgressReporter.Dispose(); + } + } + catch (Exception e) + { + _logger?.LogError(e, "Creating game engine failed: {Message}", e.Message); + throw; + } + + var progressReporter = new VerifyConsoleProgressReporter(installData.Name); + + using var verifyPipeline = new GameVerifyPipeline( + gameEngine, + engineErrorReporter, + settings.VerifyPipelineSettings, + reportSettings, + progressReporter, + services); + + try + { + try + { + _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Verifying '{Target}'...", installData.Name); + await verifyPipeline.RunAsync().ConfigureAwait(false); + progressReporter.Report(string.Empty, 1.0); + } + catch + { + progressReporter.ReportError("Verification failed", null); + throw; + } + finally + { + progressReporter.Dispose(); + } + } + catch (OperationCanceledException) + { + _logger?.LogWarning(ModVerifyConstants.ConsoleEventId, "Verification stopped due to enabled failFast setting."); + } + catch (Exception e) + { + _logger?.LogError(e, "Verification failed: {Message}", e.Message); + throw; + } + + _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Finished verification"); + return verifyPipeline.FilteredErrors; + } + + private async Task ReportErrors(IReadOnlyCollection errors) + { + _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Reporting Errors..."); + + var reportBroker = new VerificationReportBroker(services); + + await reportBroker.ReportAsync(errors); + + if (errors.Any(x => x.Severity >= settings.AppThrowsOnMinimumSeverity)) + throw new GameVerificationException(errors); + } + + private async Task WriteBaseline( + GlobalVerifyReportSettings reportSettings, + IEnumerable errors, + string baselineFile) + { + var baseline = new VerificationBaseline(reportSettings.MinimumReportSeverity, errors); + + var fullPath = _fileSystem.Path.GetFullPath(baselineFile); + _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Writing Baseline to '{FullPath}'", fullPath); + +#if NET + await +#endif + using var fs = _fileSystem.FileStream.New(fullPath, FileMode.Create, FileAccess.Write, FileShare.None); + await baseline.ToJsonAsync(fs); + } + + private GlobalVerifyReportSettings CreateGlobalReportSettings(VerifyInstallationData installData) + { + var baselineSelector = new BaselineSelector(settings, services); + var baseline = baselineSelector.SelectBaseline(installData, out var baselinePath); + + if (baseline.Count > 0) + _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Using baseline '{Baseline}'", baselinePath); + + var suppressionsFile = settings.ReportSettings.SuppressionsPath; + SuppressionList suppressions; + + if (string.IsNullOrEmpty(suppressionsFile)) + suppressions = SuppressionList.Empty; + else + { + using var fs = _fileSystem.File.OpenRead(suppressionsFile); + suppressions = SuppressionList.FromJson(fs); + + if (suppressions.Count > 0) + _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Using suppressions from '{Suppressions}'", suppressionsFile); + } + + + return new GlobalVerifyReportSettings + { + Baseline = baseline, + Suppressions = suppressions, + MinimumReportSeverity = settings.ReportSettings.MinimumReportSeverity, + }; + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModVerifyConstants.cs b/src/ModVerify.CliApp/ModVerifyConstants.cs new file mode 100644 index 0000000..6b60f06 --- /dev/null +++ b/src/ModVerify.CliApp/ModVerifyConstants.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.Logging; + +namespace AET.ModVerify.App; + +internal static class ModVerifyConstants +{ + public const string AppNameString = "AET Mod Verify"; + public const string ModVerifyToolId = "AET.ModVerify"; + public const string ModVerifyToolPath = "ModVerify"; + public const int ConsoleEventIdValue = 1138; + + public static readonly EventId ConsoleEventId = new(ConsoleEventIdValue, "LogToConsole"); +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Options/CommandLine/VerifyVerbOption.cs b/src/ModVerify.CliApp/Options/CommandLine/VerifyVerbOption.cs deleted file mode 100644 index 517ceda..0000000 --- a/src/ModVerify.CliApp/Options/CommandLine/VerifyVerbOption.cs +++ /dev/null @@ -1,26 +0,0 @@ -using AET.ModVerify.Reporting; -using CommandLine; - -namespace AET.ModVerifyTool.Options.CommandLine; - -[Verb("verify", true, HelpText = "Verifies the specified game and reports the findings.")] -internal class VerifyVerbOption : BaseModVerifyOptions -{ - [Option('o', "outDir", Required = false, HelpText = "Directory where result files shall be stored to.")] - public string? OutputDirectory { get; set; } - - [Option("failFast", Required = false, Default = false, - HelpText = "When set, the application will abort on the first failure. The option also recognized the 'MinimumFailureSeverity' setting.")] - public bool FailFast { get; set; } - - [Option("minFailSeverity", Required = false, Default = null, - HelpText = "When set, the application return with an error, if any finding has at least the specified severity value.")] - public VerificationSeverity? MinimumFailureSeverity { get; set; } - - [Option("ignoreAsserts", Required = false, - HelpText = "When this flag is present, the application will not report engine assertions.")] - public bool IgnoreAsserts { get; set; } - - [Option("baseline", Required = false, HelpText = "Path to a JSON baseline file.")] - public string? Baseline { get; set; } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Program.cs b/src/ModVerify.CliApp/Program.cs index 3ac92ba..cd4747b 100644 --- a/src/ModVerify.CliApp/Program.cs +++ b/src/ModVerify.CliApp/Program.cs @@ -1,18 +1,21 @@ -using AET.ModVerify; +using AET.ModVerify.App.Settings; +using AET.ModVerify.App.Settings.CommandLine; +using AET.ModVerify.App.Updates; +using AET.ModVerify.App.Utilities; using AET.ModVerify.Reporting; using AET.ModVerify.Reporting.Reporters; using AET.ModVerify.Reporting.Reporters.JSON; using AET.ModVerify.Reporting.Reporters.Text; using AET.ModVerify.Reporting.Settings; -using AET.ModVerifyTool.Options; -using AET.ModVerifyTool.Options.CommandLine; -using AET.ModVerifyTool.Updates; using AET.SteamAbstraction; +using AnakinRaW.ApplicationBase; +using AnakinRaW.ApplicationBase.Environment; +using AnakinRaW.ApplicationBase.Update; +using AnakinRaW.ApplicationBase.Update.Options; +using AnakinRaW.AppUpdaterFramework.Json; using AnakinRaW.CommonUtilities.Hashing; using AnakinRaW.CommonUtilities.Registry; using AnakinRaW.CommonUtilities.Registry.Windows; -using CommandLine; -using CommandLine.Text; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using PG.Commons; @@ -28,194 +31,189 @@ using PG.StarWarsGame.Infrastructure.Services.Name; using Serilog; using Serilog.Events; +using Serilog.Expressions; using Serilog.Filters; using Serilog.Sinks.SystemConsole.Themes; using System; +using System.Collections.Generic; +using System.Diagnostics; using System.IO.Abstractions; +using System.Runtime.InteropServices; using System.Threading.Tasks; using Testably.Abstractions; using ILogger = Serilog.ILogger; -namespace AET.ModVerifyTool; +namespace AET.ModVerify.App; -internal class Program +internal class MainClass +{ + // Fody/Costura application with .NET Core apparently don't work well when the class containing the Main method are derived by a type in an embedded assembly. + private static Task Main(string[] args) + { + return new Program().StartAsync(args); + } +} + +internal class Program : SelfUpdateableAppLifecycle { private static readonly string EngineParserNamespace = typeof(XmlObjectParser<>).Namespace!; private static readonly string ParserNamespace = typeof(PetroglyphXmlFileParser<>).Namespace!; private static readonly string ModVerifyRootNameSpace = typeof(Program).Namespace!; + private static readonly CompiledExpression PrintToConsoleExpression = SerilogExpression.Compile($"EventId.Id = {ModVerifyConstants.ConsoleEventIdValue}"); - private static async Task Main(string[] args) - { - ConsoleUtilities.WriteHeader(); + private static ModVerifyOptionsContainer _optionsContainer = null!; - var result = 0; - - Type[] programVerbs = - [ - typeof(VerifyVerbOption), - typeof(CreateBaselineVerbOption), - ]; - - var parseResult = Parser.Default.ParseArguments(args, programVerbs); - - await parseResult.WithParsedAsync(async o => - { - result = await Run((BaseModVerifyOptions)o); - }); - await parseResult.WithNotParsedAsync(e => - { - Console.WriteLine(HelpText.AutoBuild(parseResult).ToString()); - result = 0xA0; - return Task.CompletedTask; - }); - - return result; - } - - private static async Task Run(BaseModVerifyOptions options) + protected override async Task InitializeAppAsync(IReadOnlyList args) { - var coreServiceCollection = CreateCoreServices(options.Verbose); - var coreServices = coreServiceCollection.BuildServiceProvider(); - var logger = coreServices.GetService()?.CreateLogger(typeof(Program)); + ModVerifyConsoleUtilities.WriteHeader(ApplicationEnvironment.AssemblyInfo.InformationalVersion); - logger?.LogDebug($"Raw command line: {Environment.CommandLine}"); + await base.InitializeAppAsync(args); - var interactive = false; try { - var settings = new SettingsBuilder(coreServices).BuildSettings(options); - interactive = settings.Interactive; - var services = CreateAppServices(coreServiceCollection, settings); - - if (!settings.Offline) - await CheckForUpdate(services, logger); - - var verifier = new ModVerifyApp(settings, services); - return await verifier.RunApplication().ConfigureAwait(false); + var settings = new ModVerifyOptionsParser(ApplicationEnvironment, BootstrapLoggerFactory).Parse(args); + if (!settings.HasOptions) + return 0xA0; + _optionsContainer = settings; + return 0; } catch (Exception e) { - ConsoleUtilities.WriteApplicationFailure(); - logger?.LogCritical(e, e.Message); + Logger?.LogCritical(e, "Failed to parse commandline arguments: {Message}", e.Message); + ConsoleUtilities.WriteApplicationFatalError(ModVerifyConstants.AppNameString, e); return e.HResult; } - finally + } + + protected override void CreateAppServices(IServiceCollection services, IReadOnlyList args) + { + base.CreateAppServices(services, args); + + services.AddSingleton((ApplicationEnvironment as ModVerifyAppEnvironment)!); + + services.AddLogging(ConfigureLogging); + + services.AddSingleton(sp => new HashingService(sp)); + + + if (IsUpdateableApplication) { #if NET - await Log.CloseAndFlushAsync(); -#else - Log.CloseAndFlush(); + throw new NotSupportedException(); #endif - if (interactive) - { - Console.WriteLine(); - ConsoleUtilities.WriteHorizontalLine('-'); - Console.WriteLine("Press any key to exit"); - Console.ReadLine(); - } + services.MakeAppUpdateable( + UpdatableApplicationEnvironment, + sp => new CosturaApplicationProductService(ApplicationEnvironment, sp), + sp => new JsonManifestLoader(sp)); } - } - private static async Task CheckForUpdate(IServiceProvider services, Microsoft.Extensions.Logging.ILogger? logger) - { - var updateChecker = new ModVerifyUpdaterChecker(services); + if (_optionsContainer.ModVerifyOptions is null) + return; - logger?.LogDebug("Checking for available update"); + SteamAbstractionLayer.InitializeServices(services); + PetroglyphGameInfrastructure.InitializeServices(services); - try - { - var updateInfo = await updateChecker.CheckForUpdateAsync().ConfigureAwait(false); - if (updateInfo.IsUpdateAvailable) - { - ConsoleUtilities.WriteHorizontalLine(); - - Console.ForegroundColor = ConsoleColor.DarkGreen; - Console.WriteLine("New Update Available!"); - Console.ResetColor(); - - Console.WriteLine($"Version: {updateInfo.NewVersion}, Download here: {updateInfo.DownloadLink}"); - ConsoleUtilities.WriteHorizontalLine(); - Console.WriteLine(); + services.SupportMTD(); + services.SupportMEG(); + services.SupportALO(); + services.SupportXML(); + PetroglyphCommons.ContributeServices(services); - } + PetroglyphEngineServiceContribution.ContributeServices(services); + services.RegisterVerifierCache(); + + + SetupVerifyReporting(services); + + if (_optionsContainer.ModVerifyOptions.OfflineMode) + { + services.AddSingleton(sp => new OfflineModNameResolver(sp)); + services.AddSingleton(sp => new OfflineModGameTypeResolver(sp)); } - catch(Exception e) + else { - logger?.LogWarning($"Unable to check for updates due to an internal error: {e.Message}"); - logger?.LogTrace(e, "Checking for update failed: " + e.Message); + services.AddSingleton(sp => new OnlineModNameResolver(sp)); + services.AddSingleton(sp => new OnlineModGameTypeResolver(sp)); } } - private static IServiceCollection CreateCoreServices(bool verboseLogging) + protected override ApplicationEnvironment CreateAppEnvironment() { - var fileSystem = new RealFileSystem(); - var serviceCollection = new ServiceCollection(); - - serviceCollection.AddSingleton(new WindowsRegistry()); - serviceCollection.AddSingleton(fileSystem); - - serviceCollection.AddLogging(builder => ConfigureLogging(builder, fileSystem, verboseLogging)); - - return serviceCollection; + return new ModVerifyAppEnvironment(typeof(Program).Assembly, FileSystem); } - private static IServiceProvider CreateAppServices(IServiceCollection serviceCollection, ModVerifyAppSettings settings) + protected override IFileSystem CreateFileSystem() { - serviceCollection.AddSingleton(sp => new HashingService(sp)); - - SteamAbstractionLayer.InitializeServices(serviceCollection); - PetroglyphGameInfrastructure.InitializeServices(serviceCollection); + return new RealFileSystem(); + } - serviceCollection.SupportMTD(); - serviceCollection.SupportMEG(); - serviceCollection.SupportALO(); - serviceCollection.SupportXML(); - PetroglyphCommons.ContributeServices(serviceCollection); + protected override IRegistry CreateRegistry() + { + return !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? new InMemoryRegistry(InMemoryRegistryCreationFlags.WindowsLike) + : new WindowsRegistry(); + } - PetroglyphEngineServiceContribution.ContributeServices(serviceCollection); - serviceCollection.RegisterVerifierCache(); + protected override async Task RunAppAsync(string[] args, IServiceProvider appServiceProvider) + { + var result = await HandleUpdate(appServiceProvider); + if (result != 0 || _optionsContainer.ModVerifyOptions is null) + return result; - SetupVerifyReporting(serviceCollection, settings); + ModVerifyAppSettings modVerifySettings; - if (settings.Offline) + try { - serviceCollection.AddSingleton(sp => new OfflineModNameResolver(sp)); - serviceCollection.AddSingleton(sp => new OfflineModGameTypeResolver(sp)); + modVerifySettings = new SettingsBuilder(appServiceProvider).BuildSettings(_optionsContainer.ModVerifyOptions); } - else + catch (Exception e) { - serviceCollection.AddSingleton(sp => new OnlineModNameResolver(sp)); - serviceCollection.AddSingleton(sp => new OnlineModGameTypeResolver(sp)); + Logger?.LogCritical(e, "Failed to create settings form commandline arguments: {EMessage}", e.Message); + ConsoleUtilities.WriteApplicationFatalError(ModVerifyConstants.AppNameString, e); + return e.HResult; } - - return serviceCollection.BuildServiceProvider(); + + return await new ModVerifyApplication(modVerifySettings, appServiceProvider).Run().ConfigureAwait(false); } - private static void SetupVerifyReporting(IServiceCollection serviceCollection, ModVerifyAppSettings settings) + private void SetupVerifyReporting(IServiceCollection serviceCollection) { - var printOnlySummary = settings.CreateNewBaseline; + var options = _optionsContainer.ModVerifyOptions; + Debug.Assert(options is not null); + + + var verifyVerb = options as VerifyVerbOption; + + // Console should be in minimal summary mode if we are not in verify mode. + var printOnlySummary = verifyVerb is null; + serviceCollection.RegisterConsoleReporter(new VerifyReportSettings { MinimumReportSeverity = VerificationSeverity.Error }, printOnlySummary); - if (string.IsNullOrEmpty(settings.ReportOutput)) + if (verifyVerb == null) return; + var outputDirectory = Environment.CurrentDirectory; + + if (!string.IsNullOrEmpty(verifyVerb.OutputDirectory)) + outputDirectory = FileSystem.Path.GetFullPath(FileSystem.Path.Combine(Environment.CurrentDirectory, verifyVerb.OutputDirectory!)); + serviceCollection.RegisterJsonReporter(new JsonReporterSettings { - OutputDirectory = settings.ReportOutput!, - MinimumReportSeverity = settings.GlobalReportSettings.MinimumReportSeverity + OutputDirectory = outputDirectory!, + MinimumReportSeverity = options.MinimumSeverity }); serviceCollection.RegisterTextFileReporter(new TextFileReporterSettings { - OutputDirectory = settings.ReportOutput!, - MinimumReportSeverity = settings.GlobalReportSettings.MinimumReportSeverity + OutputDirectory = outputDirectory!, + MinimumReportSeverity = options.MinimumSeverity }); } - private static void ConfigureLogging(ILoggingBuilder loggingBuilder, IFileSystem fileSystem, bool verbose) + private void ConfigureLogging(ILoggingBuilder loggingBuilder) { loggingBuilder.ClearProviders(); @@ -226,53 +224,116 @@ private static void ConfigureLogging(ILoggingBuilder loggingBuilder, IFileSystem loggingBuilder.AddDebug(); #endif - if (verbose) + if (_optionsContainer.ModVerifyOptions?.Verbose == true || _optionsContainer.UpdateOptions?.Verbose == true) { logLevel = LogEventLevel.Verbose; loggingBuilder.AddDebug(); } - var fileLogger = SetupFileLogging(fileSystem, logLevel); + var fileLogger = SetupFileLogging(); loggingBuilder.AddSerilog(fileLogger); - var cLogger = new LoggerConfiguration() - .WriteTo.Console( - logLevel, - theme: AnsiConsoleTheme.Code, - outputTemplate: "[{Level:u3}] {Message:lj}{NewLine}{Exception}") - .Filter.ByIncludingOnly(x => - { - if (!x.Properties.TryGetValue("SourceContext", out var value)) - return true; - - var source = value.ToString().AsSpan().Trim('\"'); - - return source.StartsWith(ModVerifyRootNameSpace.AsSpan()); - }) - .CreateLogger(); - loggingBuilder.AddSerilog(cLogger); + var consoleLogger = SetupConsoleLogging(); + loggingBuilder.AddSerilog(consoleLogger); + + return; + + ILogger SetupConsoleLogging() + { + return new LoggerConfiguration() + .WriteTo.Console( + logLevel, + theme: AnsiConsoleTheme.Code, + outputTemplate: "[{Level:u3}] {Message:lj}{NewLine}{Exception}") + .MinimumLevel.Is(logLevel) + .Filter.ByIncludingOnly(x => + { + // Fatal errors are handled by a global exception handler + if (x.Level == LogEventLevel.Fatal) + return false; + + // Verbose should print everything we get + if (logLevel == LogEventLevel.Verbose) + return true; + + // Debug should print everything that has something to do with ModVerify + if (logLevel == LogEventLevel.Debug) + { + if (!x.Properties.TryGetValue("SourceContext", out var value)) + return false; + var source = value.ToString().AsSpan().Trim('\"'); + return source.StartsWith(ModVerifyRootNameSpace.AsSpan()); + } + + // In normal operation, we only print logs, which have the print-to-console EventId set. + return ExpressionResult.IsTrue(PrintToConsoleExpression(x)); + }) + .CreateLogger(); + } + + ILogger SetupFileLogging() + { + var logPath = FileSystem.Path.Combine(ApplicationEnvironment.ApplicationLocalPath, "ModVerify_log.txt"); + + return new LoggerConfiguration() + .Enrich.FromLogContext() + .MinimumLevel.Is(logLevel) + .Filter.ByExcluding(IsXmlParserLogging) + .WriteTo.Async(c => + { + c.RollingFile( + logPath, + outputTemplate: + "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{SourceContext}] {Message}{NewLine}{Exception}"); + }) + .CreateLogger(); + } + + static bool IsXmlParserLogging(LogEvent logEvent) + { + return Matching.FromSource(ParserNamespace)(logEvent) || Matching.FromSource(EngineParserNamespace)(logEvent); + } } - private static ILogger SetupFileLogging(IFileSystem fileSystem, LogEventLevel minLevel) + private async Task HandleUpdate(IServiceProvider serviceProvider) { - var logPath = fileSystem.Path.Combine(fileSystem.Path.GetTempPath(), "ModVerify_log.txt"); + var updateOptions = _optionsContainer.UpdateOptions ?? new ApplicationUpdateOptions(); + ModVerifyUpdateMode updateMode; + + if (_optionsContainer.ModVerifyOptions is not null) + { + if (_optionsContainer.ModVerifyOptions.OfflineMode) + { + Logger?.LogTrace("Running in offline mode. There will be nothing to update."); + return 0; + } - return new LoggerConfiguration() - .Enrich.FromLogContext() - .MinimumLevel.Is(minLevel) - .Filter.ByExcluding(IsXmlParserLogging) - .WriteTo.Async(c => + updateMode = _optionsContainer.ModVerifyOptions.LaunchedWithoutArguments() + ? ModVerifyUpdateMode.InteractiveUpdate + : ModVerifyUpdateMode.CheckOnly; + } + else + updateMode = ModVerifyUpdateMode.AutoUpdate; + + try + { + Logger?.LogDebug("Running update with mode '{ModVerifyUpdateMode}'", updateMode); + var modVerifyUpdater = new ModVerifyUpdater(serviceProvider); + await modVerifyUpdater.RunUpdateProcedure(updateOptions, updateMode).ConfigureAwait(false); + Logger?.LogDebug("Update procedure completed successfully."); + return 0; + } + catch (Exception e) + { + Logger?.LogCritical(e, e.Message); + var action = updateMode switch { - c.RollingFile( - logPath, - outputTemplate: - "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{SourceContext}] {Message}{NewLine}{Exception}"); - }) - .CreateLogger(); - } + ModVerifyUpdateMode.CheckOnly => "checking for updates", + _ => "updating" + }; + ConsoleUtilities.WriteApplicationFatalError(ModVerifyConstants.AppNameString, $"Error while {action}: {e.Message}", e.StackTrace); + return e.HResult; + } - private static bool IsXmlParserLogging(LogEvent logEvent) - { - return Matching.FromSource(ParserNamespace)(logEvent) || Matching.FromSource(EngineParserNamespace)(logEvent); } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Properties/AssemblyAttributes.cs b/src/ModVerify.CliApp/Properties/AssemblyAttributes.cs new file mode 100644 index 0000000..af943d8 --- /dev/null +++ b/src/ModVerify.CliApp/Properties/AssemblyAttributes.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly:InternalsVisibleTo("ModVerify.CliApp.Test")] \ No newline at end of file diff --git a/src/ModVerify.CliApp/Properties/launchSettings.json b/src/ModVerify.CliApp/Properties/launchSettings.json index 1e01ece..e47583a 100644 --- a/src/ModVerify.CliApp/Properties/launchSettings.json +++ b/src/ModVerify.CliApp/Properties/launchSettings.json @@ -1,8 +1,12 @@ { "profiles": { + "Run": { + "commandName": "Project", + "commandLineArgs": "" + }, "Interactive Verify": { "commandName": "Project", - "commandLineArgs": "verify -o verifyResults --minFailSeverity Information -v --baseline focBaseline.json --offline" + "commandLineArgs": "verify -o verifyResults --minFailSeverity Information --offline" }, "Interactive Baseline": { "commandName": "Project", diff --git a/src/ModVerify.CliApp/Reporting/BaselineFactory.cs b/src/ModVerify.CliApp/Reporting/BaselineFactory.cs new file mode 100644 index 0000000..9fcc911 --- /dev/null +++ b/src/ModVerify.CliApp/Reporting/BaselineFactory.cs @@ -0,0 +1,71 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Abstractions; +using AET.ModVerify.Reporting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace AET.ModVerify.App.Reporting; + +internal sealed class BaselineFactory(IServiceProvider serviceProvider) +{ + private readonly ILogger? _logger = serviceProvider.GetService()?.CreateLogger(typeof(BaselineFactory)); + private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); + + public bool TryCreateBaseline( + string directory, + out VerificationBaseline baseline, + [NotNullWhen(true)] out string? path) + { + baseline = VerificationBaseline.Empty; + path = null; + + if (!_fileSystem.Directory.Exists(directory)) + return false; + + _logger?.LogDebug(ModVerifyConstants.ConsoleEventId, "Searching for baseline file at '{Directory}'", directory); + + var jsonFiles = _fileSystem.Directory.EnumerateFiles( + directory, + "*.json" +#if NET || NETSTANDARD2_1_OR_GREATER + , new EnumerationOptions + { + MatchCasing = MatchCasing.CaseInsensitive, + RecurseSubdirectories = false + } +#endif + ); + + foreach (var jsonFile in jsonFiles) + { + try + { + baseline = CreateBaselineFromFilePath(jsonFile); + path = jsonFile; + _logger?.LogDebug("Create baseline from file: {JsonFile}", jsonFile); + return true; + } + catch (InvalidBaselineException e) + { + _logger?.LogDebug("'{JsonFile}' is not a valid baseline file: {Message}", jsonFile, e.Message); + // Ignore this exception + } + } + + path = null; + return false; + } + + public VerificationBaseline CreateBaseline(string filePath) + { + return CreateBaselineFromFilePath(filePath); + } + + private VerificationBaseline CreateBaselineFromFilePath(string baselineFile) + { + using var fs = _fileSystem.FileStream.New(baselineFile, FileMode.Open, FileAccess.Read); + return VerificationBaseline.FromJson(fs); + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Reporting/BaselineSelector.cs b/src/ModVerify.CliApp/Reporting/BaselineSelector.cs new file mode 100644 index 0000000..95953f1 --- /dev/null +++ b/src/ModVerify.CliApp/Reporting/BaselineSelector.cs @@ -0,0 +1,139 @@ +using AET.ModVerify.App.ModSelectors; +using AET.ModVerify.App.Resources.Baselines; +using AET.ModVerify.App.Settings; +using AET.ModVerify.Reporting; +using AnakinRaW.ApplicationBase; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine; +using System; +using System.Diagnostics; + +namespace AET.ModVerify.App.Reporting; + +internal sealed class BaselineSelector(ModVerifyAppSettings settings, IServiceProvider services) +{ + private readonly ILogger? _logger = services.GetService()?.CreateLogger(typeof(ModVerifyApplication)); + private readonly BaselineFactory _baselineFactory = new(services); + + public VerificationBaseline SelectBaseline(VerifyInstallationData installationData, out string? usedBaselinePath) + { + var baselinePath = settings.ReportSettings.BaselinePath; + if (!string.IsNullOrEmpty(baselinePath)) + { + try + { + usedBaselinePath = baselinePath; + return _baselineFactory.CreateBaseline(baselinePath!); + } + catch (InvalidBaselineException e) + { + using (ConsoleUtilities.HorizontalLineSeparatedBlock('*')) + { + Console.WriteLine($"The baseline '{baselinePath}' is not a valid baseline file: {e.Message}" + + $"{Environment.NewLine}Please generate a new baseline file or download the latest version." + + $"{Environment.NewLine}"); + } + + // For now, we bubble up this exception because we except users + // to correctly specify their baselines through command line arguments. + throw; + } + } + + if (!settings.ReportSettings.SearchBaselineLocally) + { + _logger?.LogDebug(ModVerifyConstants.ConsoleEventId, "No baseline path specified and local search is not enabled. Using empty baseline."); + usedBaselinePath = null; + return VerificationBaseline.Empty; + } + + if (settings.Interactive) + return FindBaselineInteractive(installationData, out usedBaselinePath); + + // If the application is not interactive, we only use a baseline file present in the directory of the verification target. + return FindBaselineNonInteractive(installationData.GameLocations.TargetPath, out usedBaselinePath); + + } + + private VerificationBaseline FindBaselineInteractive(VerifyInstallationData installationData, out string? baselinePath) + { + // The application is in interactive mode. We apply the following lookup: + // 1. Use a baseline found in the directory of the verification target. + // 2. Use a baseline found in the directory ModVerify executable. + // 3. If the verification target is a mod, ask the user to apply the default game's baseline. + // In any case ask the use if they want to use the located baseline file, or they wish to continue using none/empty. + + _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Searching for local baseline files..."); + + if (!_baselineFactory.TryCreateBaseline(installationData.GameLocations.TargetPath, out var baseline, + out baselinePath)) + { + if (!_baselineFactory.TryCreateBaseline("./", out baseline, out baselinePath)) + { + // It does not make sense to load the game's default baselines if the user wants to verify the game, + // as the verification result would always be empty (at least in a non-development scenario) + if (installationData.GameLocations.ModPaths.Count == 0) + { + _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "No local baseline file found."); + return VerificationBaseline.Empty; + } + + Console.WriteLine("No baseline found locally."); + return TryGetDefaultBaseline(installationData.EngineType, out baselinePath); + } + } + + Debug.Assert(baselinePath is not null); + + return ConsoleUtilities.UserYesNoQuestion($"ModVerify found the baseline file '{baselinePath}'. Do you want to use it?") + ? baseline + : VerificationBaseline.Empty; + } + + private VerificationBaseline TryGetDefaultBaseline(GameEngineType engineType, out string? baselinePath) + { + baselinePath = null; + if (engineType == GameEngineType.Eaw) + { + // TODO: EAW currently not implemented + return VerificationBaseline.Empty; + } + + if (!ConsoleUtilities.UserYesNoQuestion($"Do you want to load the default baseline for game engine '{engineType}'?")) + return VerificationBaseline.Empty; + + baselinePath = $"{engineType} (Default)"; + + try + { + return LoadEmbeddedBaseline(engineType); + } + catch (InvalidBaselineException) + { + throw new InvalidOperationException( + "Invalid baseline packed along ModVerify App. Please reach out to the creators. Thanks!"); + } + } + + internal VerificationBaseline LoadEmbeddedBaseline(GameEngineType engineType) + { + var baselineFileName = $"baseline-{engineType.ToString().ToLower()}.json"; + var resourcePath = $"{typeof(BaselineResources).Namespace}.{baselineFileName}"; + + using var baselineStream = typeof(BaselineSelector).Assembly.GetManifestResourceStream(resourcePath)!; + return VerificationBaseline.FromJson(baselineStream); + } + + private VerificationBaseline FindBaselineNonInteractive(string targetPath, out string? usedPath) + { + if (_baselineFactory.TryCreateBaseline(targetPath, out var baseline, out usedPath)) + { + _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Automatically applying local baseline file '{Path}'.", usedPath); + return baseline; + } + _logger?.LogTrace("No baseline file found in taget path '{TargetPath}'.", targetPath); + usedPath = null; + return VerificationBaseline.Empty; + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Reporting/EngineInitializeProgressReporter.cs b/src/ModVerify.CliApp/Reporting/EngineInitializeProgressReporter.cs index 69413c0..b994e97 100644 --- a/src/ModVerify.CliApp/Reporting/EngineInitializeProgressReporter.cs +++ b/src/ModVerify.CliApp/Reporting/EngineInitializeProgressReporter.cs @@ -1,6 +1,6 @@ using System; -namespace AET.ModVerifyTool.Reporting; +namespace AET.ModVerify.App.Reporting; internal sealed class EngineInitializeProgressReporter : IDisposable { diff --git a/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs b/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs index 37d3ca2..4700457 100644 --- a/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs +++ b/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs @@ -1,11 +1,11 @@ -using AnakinRaW.CommonUtilities; -using AnakinRaW.CommonUtilities.SimplePipeline.Progress; -using ShellProgressBar; -using System; +using System; using System.Threading; using AET.ModVerify.Pipeline.Progress; +using AnakinRaW.CommonUtilities; +using AnakinRaW.CommonUtilities.SimplePipeline.Progress; +using ShellProgressBar; -namespace AET.ModVerifyTool.Reporting; +namespace AET.ModVerify.App.Reporting; public sealed class VerifyConsoleProgressReporter(string toVerifyName) : DisposableObject, IVerifyProgressReporter { diff --git a/src/ModVerify.CliApp/Resources/Baselines/BaselineResources.cs b/src/ModVerify.CliApp/Resources/Baselines/BaselineResources.cs new file mode 100644 index 0000000..bf49f9e --- /dev/null +++ b/src/ModVerify.CliApp/Resources/Baselines/BaselineResources.cs @@ -0,0 +1,4 @@ +namespace AET.ModVerify.App.Resources.Baselines; + +// Marker class to provide static namespace information for resource lookup. +internal static class BaselineResources; \ No newline at end of file diff --git a/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json b/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json new file mode 100644 index 0000000..c94d121 --- /dev/null +++ b/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json @@ -0,0 +1,2081 @@ +{ + "version": "2.0", + "minSeverity": "Information", + "errors": [ + { + "id": "XML04", + "verifiers": [ + "XMLError" + ], + "message": "Expected double but got value \u002737\u0060\u0027. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #11571\u0027", + "severity": "Warning", + "context": [ + "DATA\\XML\\COMMANDBARCOMPONENTS.XML", + "Size", + "parentName=\u0027bm_text_steal\u0027" + ], + "asset": "Size" + }, + { + "id": "XML04", + "verifiers": [ + "XMLError" + ], + "message": "Expected integer but got \u002780, 20\u0027. File=\u0027DATA\\XML\\SFXEVENTSWEAPONS.XML #90\u0027", + "severity": "Warning", + "context": [ + "DATA\\XML\\SFXEVENTSWEAPONS.XML", + "Probability", + "parentName=\u0027Unit_TIE_Fighter_Fire\u0027" + ], + "asset": "Probability" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_SKIPRAY.ALO\u0027.", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\UV_SKIPRAY.ALO" + ], + "asset": "Default.fx" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_PRISON.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\NB_PRISON.ALO" + ], + "asset": "p_smoke_small_thin2" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027W_Kamino_Reflect.ALO\u0027", + "severity": "Error", + "context": [], + "asset": "W_Kamino_Reflect.ALO" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027p_ssd_debris\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_ECLIPSE_UC_DC.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\UV_ECLIPSE_UC_DC.ALO" + ], + "asset": "p_ssd_debris" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_p_proton_torpedo.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_p_proton_torpedo.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_DStar_LeverPanel.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_DStar_LeverPanel.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027lookat\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_ECLIPSE.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\UV_ECLIPSE.ALO" + ], + "asset": "lookat" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" + ], + "message": "Could not find texture \u0027Cin_DeathStar.tga\u0027 for context: [ALTTEST.ALO].", + "severity": "Error", + "context": [ + "ALTTEST.ALO" + ], + "asset": "Cin_DeathStar.tga" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027p_prison_light\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_PRISON.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\NB_PRISON.ALO" + ], + "asset": "p_prison_light" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\EV_MDU_SENSORNODE.ALO\u0027.", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\EV_MDU_SENSORNODE.ALO" + ], + "asset": "Default.fx" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" + ], + "message": "Could not find texture \u0027w_grenade.tga\u0027 for context: [W_GRENADE.ALO].", + "severity": "Error", + "context": [ + "W_GRENADE.ALO" + ], + "asset": "w_grenade.tga" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_Rbel_NavyRow.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Rbel_NavyRow.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_Planet_Alderaan_High.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_Planet_Alderaan_High.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027lookat\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_ECLIPSE_UC.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\UV_ECLIPSE_UC.ALO" + ], + "asset": "lookat" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\EI_MARAJADE.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\EI_MARAJADE.ALO" + ], + "asset": "p_desert_ground_dust" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027p_splash_wake_lava.alo\u0027", + "severity": "Error", + "context": [], + "asset": "p_splash_wake_lava.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_rv_XWingProp.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_rv_XWingProp.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_Fire_Huge.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Fire_Huge.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_Probe_Droid.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Probe_Droid.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_05_STATION_D.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\UB_05_STATION_D.ALO" + ], + "asset": "p_uwstation_death" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\RI_KYLEKATARN.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\RI_KYLEKATARN.ALO" + ], + "asset": "p_desert_ground_dust" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027p_steam_small\u0027 not found for model \u0027DATA\\ART\\MODELS\\RB_HEAVYVEHICLEFACTORY.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\RB_HEAVYVEHICLEFACTORY.ALO" + ], + "asset": "p_steam_small" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" + ], + "message": "Could not find texture \u0027p_particle_master\u0027 for context: [P_DIRT_EMITTER_TEST1.ALO].", + "severity": "Error", + "context": [ + "P_DIRT_EMITTER_TEST1.ALO" + ], + "asset": "p_particle_master" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027p_smoke_small_thin4\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_PRISON.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\NB_PRISON.ALO" + ], + "asset": "p_smoke_small_thin4" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_EI_Vader.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_EI_Vader.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027DATA\\ART\\MODELS\\RB_HYPERVELOCITYGUN.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\RB_HYPERVELOCITYGUN.ALO" + ], + "asset": "p_smoke_small_thin2" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\EV_TIE_LANCET.ALO\u0027.", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\EV_TIE_LANCET.ALO" + ], + "asset": "Default.fx" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_DeathStar_Wall.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_DeathStar_Wall.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027W_droid_steam.alo\u0027", + "severity": "Error", + "context": [], + "asset": "W_droid_steam.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_DeathStar_High.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_DeathStar_High.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027MODELS\u0027", + "severity": "Error", + "context": [], + "asset": "MODELS" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027W_AllShaders.ALO\u0027", + "severity": "Error", + "context": [], + "asset": "W_AllShaders.ALO" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027p_bomb_spin\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_THERMAL_DETONATOR_EMPIRE.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\W_THERMAL_DETONATOR_EMPIRE.ALO" + ], + "asset": "p_bomb_spin" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_03_STATION_D.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\UB_03_STATION_D.ALO" + ], + "asset": "p_uwstation_death" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_Rbel_GreyGroup.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Rbel_GreyGroup.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027p_cold_tiny01\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_SCH.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\NB_SCH.ALO" + ], + "asset": "p_cold_tiny01" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CINE_EV_StarDestroyer.ALO\u0027", + "severity": "Error", + "context": [], + "asset": "CINE_EV_StarDestroyer.ALO" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027p_hp_archammer-damage\u0027 not found for model \u0027DATA\\ART\\MODELS\\EV_ARCHAMMER.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\EV_ARCHAMMER.ALO" + ], + "asset": "p_hp_archammer-damage" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_Rbel_grey.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Rbel_grey.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_Reb_CelebHall.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Reb_CelebHall.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027P_mptl-2a_Die\u0027 not found for model \u0027DATA\\ART\\MODELS\\RV_MPTL-2A.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\RV_MPTL-2A.ALO" + ], + "asset": "P_mptl-2a_Die" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_ImperialCraft.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_ImperialCraft.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_DStar_Dish_close.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_DStar_Dish_close.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027pe_bwing_yellow\u0027 not found for model \u0027DATA\\ART\\MODELS\\RV_BWING.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\RV_BWING.ALO" + ], + "asset": "pe_bwing_yellow" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_bridge.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_bridge.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\UI_SABOTEUR.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\UI_SABOTEUR.ALO" + ], + "asset": "p_desert_ground_dust" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_Trooper_Row.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Trooper_Row.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_EV_TieAdvanced.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_EV_TieAdvanced.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027w_sith_arch.alo\u0027", + "severity": "Error", + "context": [], + "asset": "w_sith_arch.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" + ], + "message": "Could not find texture \u0027NB_YsalamiriTree_B.tga\u0027 for context: [UV_MDU_CAGE.ALO].", + "severity": "Error", + "context": [ + "UV_MDU_CAGE.ALO" + ], + "asset": "NB_YsalamiriTree_B.tga" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" + ], + "message": "Could not find texture \u0027W_TE_Rock_f_02_b.tga\u0027 for context: [EV_TIE_PHANTOM.ALO].", + "severity": "Error", + "context": [ + "EV_TIE_PHANTOM.ALO" + ], + "asset": "W_TE_Rock_f_02_b.tga" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_EI_Palpatine.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_EI_Palpatine.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_Rbel_Soldier.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Rbel_Soldier.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_MONCAL_BUILDING.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\NB_MONCAL_BUILDING.ALO" + ], + "asset": "p_smoke_small_thin2" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027W_Bush_Swmp00.ALO\u0027", + "severity": "Error", + "context": [], + "asset": "W_Bush_Swmp00.ALO" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_Officer_Row.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Officer_Row.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_HIGH.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\W_STARS_HIGH.ALO" + ], + "asset": "Lensflare0" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" + ], + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall_B.tga\u0027 for context: [W_SITH_LEFTHALL.ALO].", + "severity": "Error", + "context": [ + "W_SITH_LEFTHALL.ALO" + ], + "asset": "Cin_Reb_CelebHall_Wall_B.tga" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_Coruscant.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_Coruscant.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027p_ewok_drag_dirt\u0027 not found for model \u0027DATA\\ART\\MODELS\\UI_EWOK_HANDLER.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\UI_EWOK_HANDLER.ALO" + ], + "asset": "p_ewok_drag_dirt" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027P_heat_small01\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_VCH.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\NB_VCH.ALO" + ], + "asset": "P_heat_small01" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027W_Vol_Steam01.ALO\u0027", + "severity": "Error", + "context": [], + "asset": "W_Vol_Steam01.ALO" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027p_explosion_smoke_small_thin5\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_NOGHRI_HUT.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\NB_NOGHRI_HUT.ALO" + ], + "asset": "p_explosion_smoke_small_thin5" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_MEDIUM.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\W_STARS_MEDIUM.ALO" + ], + "asset": "Lensflare0" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_02_STATION_D.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\UB_02_STATION_D.ALO" + ], + "asset": "p_uwstation_death" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_Officer.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_Officer.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_04_STATION_D.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\UB_04_STATION_D.ALO" + ], + "asset": "p_uwstation_death" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_Lambda_Head.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Lambda_Head.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_Biker_Row.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Biker_Row.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_DStar_protons.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_DStar_protons.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\UI_IG88.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\UI_IG88.ALO" + ], + "asset": "p_desert_ground_dust" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_Lambda_Mouth.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Lambda_Mouth.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_Planet_Hoth_High.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_Planet_Hoth_High.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_LOW.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\W_STARS_LOW.ALO" + ], + "asset": "Lensflare0" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_NavyTrooper_Row.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_NavyTrooper_Row.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_DStar_TurretLasers.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_DStar_TurretLasers.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027W_SwampGasEmit.ALO\u0027", + "severity": "Error", + "context": [], + "asset": "W_SwampGasEmit.ALO" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_Shuttle_Tyderium.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_Shuttle_Tyderium.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_EV_Stardestroyer_Warp.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_EV_Stardestroyer_Warp.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_CINE_LUA.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\W_STARS_CINE_LUA.ALO" + ], + "asset": "Lensflare0" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_DeathStar_Hangar.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_DeathStar_Hangar.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_Fire_Medium.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Fire_Medium.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_CINE.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\W_STARS_CINE.ALO" + ], + "asset": "Lensflare0" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_Rbel_Soldier_Group.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Rbel_Soldier_Group.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027RV_nebulonb_D_death_00.ALO\u0027", + "severity": "Error", + "context": [], + "asset": "RV_nebulonb_D_death_00.ALO" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027W_Volcano_Rock02.ALO\u0027", + "severity": "Error", + "context": [], + "asset": "W_Volcano_Rock02.ALO" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027w_planet_volcanic.alo\u0027", + "severity": "Error", + "context": [], + "asset": "w_planet_volcanic.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027CIN_REb_CelebCharacters.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_REb_CelebCharacters.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_CRUSADERCLASSCORVETTE.ALO\u0027.", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\UV_CRUSADERCLASSCORVETTE.ALO" + ], + "asset": "Default.fx" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" + ], + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall.tga\u0027 for context: [W_SITH_LEFTHALL.ALO].", + "severity": "Error", + "context": [ + "W_SITH_LEFTHALL.ALO" + ], + "asset": "Cin_Reb_CelebHall_Wall.tga" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027Cin_EV_lambdaShuttle_150.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_EV_lambdaShuttle_150.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027p_explosion_small_delay00\u0027 not found for model \u0027DATA\\ART\\MODELS\\EB_COMMANDCENTER.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\EB_COMMANDCENTER.ALO" + ], + "asset": "p_explosion_small_delay00" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" + ], + "message": "Could not find texture \u0027UB_girder_B.tga\u0027 for context: [UV_MDU_CAGE.ALO].", + "severity": "Error", + "context": [ + "UV_MDU_CAGE.ALO" + ], + "asset": "UB_girder_B.tga" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_01_STATION_D.ALO\u0027", + "severity": "Error", + "context": [ + "DATA\\ART\\MODELS\\UB_01_STATION_D.ALO" + ], + "asset": "p_uwstation_death" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0206_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0206_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0204_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0204_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0102_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0102_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0215_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0215_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0107_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0107_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0504_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Remove_Corruption_Leia" + ], + "asset": "U000_LEI0504_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027AMB_DES_CLEAR_LOOP_1.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Weather_Ambient_Clear_Sandstorm_Loop" + ], + "asset": "AMB_DES_CLEAR_LOOP_1.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0105_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0105_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0213_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0213_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0201_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0201_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0303_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0303_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0103_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0103_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0207_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0207_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_DEF3006_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Corrupt_Sabateur" + ], + "asset": "U000_DEF3006_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0309_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0309_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0209_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0209_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_DEF3106_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Weaken_Sabateur" + ], + "asset": "U000_DEF3106_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0503_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Remove_Corruption_Leia" + ], + "asset": "U000_LEI0503_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0502_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Remove_Corruption_Leia" + ], + "asset": "U000_LEI0502_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0212_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0212_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027AMB_URB_CLEAR_LOOP_1.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Weather_Ambient_Clear_Urban_Loop" + ], + "asset": "AMB_URB_CLEAR_LOOP_1.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0311_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0311_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0115_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0115_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0101_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0101_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0401_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Guard_Leia" + ], + "asset": "U000_LEI0401_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0315_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0315_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0106_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0106_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0603_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Increase_Production_Leia" + ], + "asset": "U000_LEI0603_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0104_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0104_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0501_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Remove_Corruption_Leia" + ], + "asset": "U000_LEI0501_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027TESTUNITMOVE_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Gneneric_Test" + ], + "asset": "TESTUNITMOVE_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0108_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0108_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_MCF1601_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_StarDest_MC30_Frigate" + ], + "asset": "U000_MCF1601_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0111_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0111_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0211_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0211_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0110_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0110_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0403_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Guard_Leia" + ], + "asset": "U000_LEI0403_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0306_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0306_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0308_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0308_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0112_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0112_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0301_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0301_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0404_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Guard_Leia" + ], + "asset": "U000_LEI0404_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_TMC0212_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Tie_Mauler" + ], + "asset": "U000_TMC0212_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027EGL_STAR_VIPER_SPINNING_1.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Star_Viper_Spinning_By" + ], + "asset": "EGL_STAR_VIPER_SPINNING_1.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0208_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0208_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0604_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Increase_Production_Leia" + ], + "asset": "U000_LEI0604_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027FS_BEETLE_3.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "SFX_Anim_Beetle_Footsteps" + ], + "asset": "FS_BEETLE_3.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0109_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0109_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0202_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0202_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0602_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Increase_Production_Leia" + ], + "asset": "U000_LEI0602_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0305_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0305_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_MAL0503_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Assist_Move_Missile_Launcher" + ], + "asset": "U000_MAL0503_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0601_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Increase_Production_Leia" + ], + "asset": "U000_LEI0601_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_ARC3106_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Complete_Troops_Arc_Hammer" + ], + "asset": "U000_ARC3106_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027FS_BEETLE_4.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "SFX_Anim_Beetle_Footsteps" + ], + "asset": "FS_BEETLE_4.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027FS_BEETLE_1.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "SFX_Anim_Beetle_Footsteps" + ], + "asset": "FS_BEETLE_1.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0205_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0205_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0113_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0113_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0314_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0314_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0304_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0304_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0203_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0203_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027C000_DST0102_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "EHD_Death_Star_Activate" + ], + "asset": "C000_DST0102_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0114_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Select_Leia" + ], + "asset": "U000_LEI0114_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_ARC3104_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Produce_Troops_Arc_Hammer" + ], + "asset": "U000_ARC3104_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027FS_BEETLE_2.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "SFX_Anim_Beetle_Footsteps" + ], + "asset": "FS_BEETLE_2.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_ARC3105_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Complete_Troops_Arc_Hammer" + ], + "asset": "U000_ARC3105_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0307_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0307_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0402_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Guard_Leia" + ], + "asset": "U000_LEI0402_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0312_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0312_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0210_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0210_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.AudioFilesVerifier" + ], + "message": "Audio file \u0027U000_LEI0313_ENG.WAV\u0027 could not be found.", + "severity": "Error", + "context": [ + "Unit_Attack_Leia" + ], + "asset": "U000_LEI0313_ENG.WAV" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" + ], + "message": "Could not find GUI texture \u0027underworld_logo_selected.tga\u0027 at location \u0027MegaTexture\u0027.", + "severity": "Error", + "context": [ + "IDC_PLAY_FACTION_A_BUTTON_BIG", + "MegaTexture" + ], + "asset": "underworld_logo_selected.tga" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" + ], + "message": "Could not find GUI texture \u0027i_button_petro_sliver.tga\u0027 at location \u0027MegaTexture\u0027.", + "severity": "Error", + "context": [ + "IDC_MENU_PETRO_LOGO", + "MegaTexture" + ], + "asset": "i_button_petro_sliver.tga" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" + ], + "message": "Could not find GUI texture \u0027i_dialogue_button_large_middle_off.tga\u0027 at location \u0027Repository\u0027.", + "severity": "Error", + "context": [ + "IDC_PLAY_FACTION_B_BUTTON_BIG", + "Repository" + ], + "asset": "i_dialogue_button_large_middle_off.tga" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" + ], + "message": "Could not find GUI texture \u0027underworld_logo_rollover.tga\u0027 at location \u0027MegaTexture\u0027.", + "severity": "Error", + "context": [ + "IDC_PLAY_FACTION_A_BUTTON_BIG", + "MegaTexture" + ], + "asset": "underworld_logo_rollover.tga" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" + ], + "message": "Could not find GUI texture \u0027underworld_logo_off.tga\u0027 at location \u0027MegaTexture\u0027.", + "severity": "Error", + "context": [ + "IDC_PLAY_FACTION_A_BUTTON_BIG", + "MegaTexture" + ], + "asset": "underworld_logo_off.tga" + } + ] +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Options/CommandLine/BaseModVerifyOptions.cs b/src/ModVerify.CliApp/Settings/CommandLine/BaseModVerifyOptions.cs similarity index 83% rename from src/ModVerify.CliApp/Options/CommandLine/BaseModVerifyOptions.cs rename to src/ModVerify.CliApp/Settings/CommandLine/BaseModVerifyOptions.cs index d239112..0e5e203 100644 --- a/src/ModVerify.CliApp/Options/CommandLine/BaseModVerifyOptions.cs +++ b/src/ModVerify.CliApp/Settings/CommandLine/BaseModVerifyOptions.cs @@ -3,57 +3,57 @@ using CommandLine; using PG.StarWarsGame.Engine; -namespace AET.ModVerifyTool.Options.CommandLine; +namespace AET.ModVerify.App.Settings.CommandLine; internal abstract class BaseModVerifyOptions { [Option('v', "verbose", Required = false, HelpText = "Sets output to verbose messages.")] - public bool Verbose { get; set; } + public bool Verbose { get; init; } [Option("offline", Default = false, HelpText = "When set, the application will work in offline mode and does not need an Internet connection.")] - public bool OfflineMode { get; set; } + public bool OfflineMode { get; init; } [Option("minSeverity", Required = false, Default = VerificationSeverity.Information, HelpText = "When set, only findings with at least the specified severity value are processed.")] - public VerificationSeverity MinimumSeverity { get; set; } + public VerificationSeverity MinimumSeverity { get; init; } [Option("suppressions", Required = false, HelpText = "Path to a JSON suppression file.")] - public string? Suppressions { get; set; } + public string? Suppressions { get; init; } [Option("path", SetName = "autoDetection", Required = false, Default = null, - HelpText = "Specifies the path to verify. The path may be a game or mod. The application will try to find all necessary submods or base games itself. " + + HelpText = "Specifies the path to verify. The path may be a game or mod. The application will try to find all necessary sub-mods or base games itself. " + "The argument cannot be combined with any of --mods, --game or --fallbackGame")] - public string? AutoPath { get; set; } + public string? AutoPath { get; init; } [Option("mods", SetName = "manualPaths", Required = false, Default = null, Separator = ';', HelpText = "The path of the mod to verify. To support submods, multiple paths can be separated using the ';' (semicolon) character. " + "Leave empty, if you want to verify a game. If you want to use the interactive mode, leave this, --game and --fallbackGame empty.")] - public IList? ModPaths { get; set; } + public IList? ModPaths { get; init; } [Option("game", SetName = "manualPaths", Required = false, Default = null, HelpText = "The path of the base game. For FoC mods this points to the FoC installation, for EaW mods this points to the EaW installation. " + "Leave empty, if you want to auto-detect games. If you want to use the interactive mode, leave this, --mods and --fallbackGame empty. " + "If this argument is set, you also need to set --mods (including sub mods) and --fallbackGame manually.")] - public string? GamePath { get; set; } + public string? GamePath { get; init; } [Option("fallbackGame", SetName = "manualPaths", Required = false, Default = null, HelpText = "The path of the fallback game. Usually this points to the EaW installation. This argument only recognized if --game is set.")] - public string? FallbackGamePath { get; set; } + public string? FallbackGamePath { get; init; } [Option("type", Required = false, Default = null, HelpText = "The game type of the mod that shall be verified. Skip this value to auto-determine the type. Valid values are 'Eaw' and 'Foc'. " + "This argument is required, if the first mod of '--mods' points to a directory outside of the common folder hierarchy (e.g, /MODS/MOD_NAME or /32470/WORKSHOP_ID")] - public GameEngineType? GameType { get; set; } + public GameEngineType? GameType { get; init; } [Option("additionalFallbackPaths", Required = false, Separator = ';', HelpText = "Additional fallback paths, which may contain assets that shall be included when doing the verification. Do not add EaW here. " + "Multiple paths can be separated using the ';' (semicolon) character.")] - public IList? AdditionalFallbackPath { get; set; } + public IList? AdditionalFallbackPath { get; init; } [Option("parallel", Default = false, HelpText = "When set, game verifiers will run in parallel. " + "While this may reduce analysis time, console output might be harder to read.")] - public bool Parallel { get; set; } + public bool Parallel { get; init; } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Options/CommandLine/CreateBaselineVerbOption.cs b/src/ModVerify.CliApp/Settings/CommandLine/CreateBaselineVerbOption.cs similarity index 59% rename from src/ModVerify.CliApp/Options/CommandLine/CreateBaselineVerbOption.cs rename to src/ModVerify.CliApp/Settings/CommandLine/CreateBaselineVerbOption.cs index 78132cc..dc60a73 100644 --- a/src/ModVerify.CliApp/Options/CommandLine/CreateBaselineVerbOption.cs +++ b/src/ModVerify.CliApp/Settings/CommandLine/CreateBaselineVerbOption.cs @@ -1,10 +1,10 @@ using CommandLine; -namespace AET.ModVerifyTool.Options.CommandLine; +namespace AET.ModVerify.App.Settings.CommandLine; [Verb("createBaseline", HelpText = "Verifies the specified game and creates a new baseline file at the specified location.")] -internal class CreateBaselineVerbOption : BaseModVerifyOptions +internal sealed class CreateBaselineVerbOption : BaseModVerifyOptions { [Option('o', "outFile", Required = true, HelpText = "The file path of the new baseline file.")] - public string OutputFile { get; set; } + public required string OutputFile { get; init; } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/CommandLine/ModVerifyOptionsContainer.cs b/src/ModVerify.CliApp/Settings/CommandLine/ModVerifyOptionsContainer.cs new file mode 100644 index 0000000..c9aab20 --- /dev/null +++ b/src/ModVerify.CliApp/Settings/CommandLine/ModVerifyOptionsContainer.cs @@ -0,0 +1,12 @@ +using AnakinRaW.ApplicationBase.Update.Options; + +namespace AET.ModVerify.App.Settings.CommandLine; + +internal sealed class ModVerifyOptionsContainer +{ + public BaseModVerifyOptions? ModVerifyOptions { get; init; } + + public ApplicationUpdateOptions? UpdateOptions { get; init; } + + public bool HasOptions => ModVerifyOptions is not null || UpdateOptions is not null; +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/CommandLine/ModVerifyOptionsParser.cs b/src/ModVerify.CliApp/Settings/CommandLine/ModVerifyOptionsParser.cs new file mode 100644 index 0000000..63a29d8 --- /dev/null +++ b/src/ModVerify.CliApp/Settings/CommandLine/ModVerifyOptionsParser.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using AET.ModVerify.App.Utilities; +using AnakinRaW.ApplicationBase.Environment; +using AnakinRaW.ApplicationBase.Update.Options; +using AnakinRaW.ExternalUpdater; +using CommandLine; +using CommandLine.Text; +using Microsoft.Extensions.Logging; + +namespace AET.ModVerify.App.Settings.CommandLine; + +internal sealed class ModVerifyOptionsParser +{ + private readonly ApplicationEnvironment _applicationEnvironment; + private readonly ILogger? _logger; + + [field: AllowNull, MaybeNull] + private Type[] AvailableVerbTypes => LazyInitializer.EnsureInitialized(ref field, GetAvailableVerbTypes)!; + + public ModVerifyOptionsParser(ApplicationEnvironment applicationEnvironment, ILoggerFactory? loggerFactory) + { + _applicationEnvironment = applicationEnvironment; + _logger = loggerFactory?.CreateLogger(GetType()); + } + + public ModVerifyOptionsContainer Parse(IReadOnlyList args) + { + // If the application is updatable (.NET Framework) we need to remove potential arguments from the external updater + // in order to keep strict parsing rules enabled for better user error messages. + if (_applicationEnvironment.IsUpdatable()) + args = StripExternalUpdateResults(args); + + return ParseArguments(args); + } + + private ModVerifyOptionsContainer ParseArguments(IReadOnlyList args) + { + // Empty arguments means that we are "interactive" mode (user simply double-clicked the executable) + if (args.Count == 0) + { + return new ModVerifyOptionsContainer + { + ModVerifyOptions = VerifyVerbOption.WithoutArguments, + UpdateOptions = null + }; + } + + var parseResult = Parser.Default.ParseArguments(args, AvailableVerbTypes); + + BaseModVerifyOptions? modVerifyOptions = null; + ApplicationUpdateOptions? updateOptions = null; + + parseResult.WithParsed(o => modVerifyOptions = o); + parseResult.WithParsed(o => updateOptions = o); + + parseResult.WithNotParsed(_ => + { + _logger?.LogError("Unable to parse command line"); + Console.WriteLine(HelpText.AutoBuild(parseResult).ToString()); + }); + + return new ModVerifyOptionsContainer + { + ModVerifyOptions = modVerifyOptions, + UpdateOptions = updateOptions, + }; + } + + public static IReadOnlyList StripExternalUpdateResults(IReadOnlyList args) + { + // Parser.Default.FormatCommandLine(ResultOption) as used in ProcessTool.cs either returns + // two argument segments or none (if Result == UpdaterNotRun) + if (args.Count < 2) + return args; + + // The external updater promises to append the result to the arguments. + // Thus, it's sufficient to check the second last segment whether it matches. + var secondLast = args[^2]; + + return secondLast == ExternalUpdaterResultOptions.RawOptionString + ? [..args.Take(args.Count - 2)] + : args; + } + + private Type[] GetAvailableVerbTypes() + { + return _applicationEnvironment.IsUpdatable() + ? [typeof(VerifyVerbOption), typeof(CreateBaselineVerbOption), typeof(ApplicationUpdateOptions)] + : [typeof(VerifyVerbOption), typeof(CreateBaselineVerbOption)]; + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/CommandLine/VerifyVerbOption.cs b/src/ModVerify.CliApp/Settings/CommandLine/VerifyVerbOption.cs new file mode 100644 index 0000000..97f1536 --- /dev/null +++ b/src/ModVerify.CliApp/Settings/CommandLine/VerifyVerbOption.cs @@ -0,0 +1,39 @@ +using AET.ModVerify.Reporting; +using CommandLine; + +namespace AET.ModVerify.App.Settings.CommandLine; + +[Verb("verify", HelpText = "Verifies the specified game and reports the findings.")] +internal sealed class VerifyVerbOption : BaseModVerifyOptions +{ + internal static readonly VerifyVerbOption WithoutArguments = new() + { + IsRunningWithoutArguments = true, + SearchBaselineLocally = true, + }; + + [Option('o', "outDir", Required = false, HelpText = "Directory where result files shall be stored to.")] + public string? OutputDirectory { get; init; } + + [Option("failFast", Required = false, Default = false, + HelpText = "When set, the application will abort on the first failure. The option also recognized the 'MinimumFailureSeverity' setting.")] + public bool FailFast { get; init; } + + [Option("minFailSeverity", Required = false, Default = null, + HelpText = "When set, the application return with an error, if any finding has at least the specified severity value.")] + public VerificationSeverity? MinimumFailureSeverity { get; set; } + + [Option("ignoreAsserts", Required = false, + HelpText = "When this flag is present, the application will not report engine assertions.")] + public bool IgnoreAsserts { get; init; } + + [Option("baseline", SetName = "baselineSelection", Required = false, + HelpText = "Path to a JSON baseline file. Cannot be used together with --searchBaseline.")] + public string? Baseline { get; init; } + + [Option("searchBaseline", SetName = "baselineSelection", Required = false, + HelpText = "When set, the application will search for baseline files and use them for verification. Cannot be used together with --baseline")] + public bool SearchBaselineLocally { get; init; } + + public bool IsRunningWithoutArguments { get; init; } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Options/GameInstallationsSettings.cs b/src/ModVerify.CliApp/Settings/GameInstallationsSettings.cs similarity index 74% rename from src/ModVerify.CliApp/Options/GameInstallationsSettings.cs rename to src/ModVerify.CliApp/Settings/GameInstallationsSettings.cs index 667e2cf..00bc4f0 100644 --- a/src/ModVerify.CliApp/Options/GameInstallationsSettings.cs +++ b/src/ModVerify.CliApp/Settings/GameInstallationsSettings.cs @@ -1,11 +1,10 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using PG.StarWarsGame.Engine; -namespace AET.ModVerifyTool.Options; +namespace AET.ModVerify.App.Settings; -internal record GameInstallationsSettings +internal sealed record GameInstallationsSettings { public bool Interactive => string.IsNullOrEmpty(AutoPath) && ModPaths.Count == 0 && string.IsNullOrEmpty(GamePath); @@ -17,13 +16,13 @@ internal record GameInstallationsSettings public string? AutoPath { get; init; } - public IList ModPaths { get; init; } = Array.Empty(); + public IList ModPaths { get; init; } = []; public string? GamePath { get; init; } public string? FallbackGamePath { get; init; } - public IList AdditionalFallbackPaths { get; init; } = Array.Empty(); + public IList AdditionalFallbackPaths { get; init; } = []; public GameEngineType? EngineType { get; init; } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Options/ModVerifyAppSettings.cs b/src/ModVerify.CliApp/Settings/ModVerifyAppSettings.cs similarity index 72% rename from src/ModVerify.CliApp/Options/ModVerifyAppSettings.cs rename to src/ModVerify.CliApp/Settings/ModVerifyAppSettings.cs index 6a3b0bd..3376354 100644 --- a/src/ModVerify.CliApp/Options/ModVerifyAppSettings.cs +++ b/src/ModVerify.CliApp/Settings/ModVerifyAppSettings.cs @@ -1,9 +1,8 @@ using System.Diagnostics.CodeAnalysis; using AET.ModVerify.Reporting; -using AET.ModVerify.Reporting.Settings; using AET.ModVerify.Settings; -namespace AET.ModVerifyTool.Options; +namespace AET.ModVerify.App.Settings; internal sealed class ModVerifyAppSettings { @@ -11,18 +10,14 @@ internal sealed class ModVerifyAppSettings public required VerifyPipelineSettings VerifyPipelineSettings { get; init; } - public required GlobalVerifyReportSettings GlobalReportSettings { get; init; } + public required ModVerifyReportSettings ReportSettings { get; init; } public required GameInstallationsSettings GameInstallationsSettings { get; init; } public VerificationSeverity? AppThrowsOnMinimumSeverity { get; init; } - public string? ReportOutput { get; init; } - [MemberNotNullWhen(true, nameof(NewBaselinePath))] public bool CreateNewBaseline => !string.IsNullOrEmpty(NewBaselinePath); public string? NewBaselinePath { get; init; } - - public bool Offline { get; init; } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/ModVerifyReportSettings.cs b/src/ModVerify.CliApp/Settings/ModVerifyReportSettings.cs new file mode 100644 index 0000000..482b844 --- /dev/null +++ b/src/ModVerify.CliApp/Settings/ModVerifyReportSettings.cs @@ -0,0 +1,14 @@ +using AET.ModVerify.Reporting; + +namespace AET.ModVerify.App.Settings; + +internal sealed class ModVerifyReportSettings +{ + public VerificationSeverity MinimumReportSeverity { get; init; } + + public string? SuppressionsPath { get; init; } + + public string? BaselinePath { get; init; } + + public bool SearchBaselineLocally { get; init; } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/SettingsBuilder.cs b/src/ModVerify.CliApp/Settings/SettingsBuilder.cs similarity index 60% rename from src/ModVerify.CliApp/SettingsBuilder.cs rename to src/ModVerify.CliApp/Settings/SettingsBuilder.cs index d2ba861..3c5535f 100644 --- a/src/ModVerify.CliApp/SettingsBuilder.cs +++ b/src/ModVerify.CliApp/Settings/SettingsBuilder.cs @@ -1,23 +1,20 @@ -using AET.ModVerify.Reporting; -using AET.ModVerify.Reporting.Settings; -using AET.ModVerify.Settings; -using AET.ModVerifyTool.Options; -using Microsoft.Extensions.DependencyInjection; -using System; +using System; using System.Collections.Generic; -using System.IO; using System.IO.Abstractions; +using AET.ModVerify.App.Settings.CommandLine; +using AET.ModVerify.App.Utilities; using AET.ModVerify.Pipeline; -using AET.ModVerifyTool.Options.CommandLine; +using AET.ModVerify.Reporting; +using AET.ModVerify.Settings; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace AET.ModVerifyTool; +namespace AET.ModVerify.App.Settings; -internal sealed class SettingsBuilder(IServiceProvider services) +internal sealed class SettingsBuilder(IServiceProvider serviceProvider) { - private readonly IFileSystem _fileSystem = services.GetRequiredService(); - private readonly ILogger? _logger = - services.GetRequiredService()?.CreateLogger(typeof(SettingsBuilder)); + private readonly ILogger? _logger = serviceProvider.GetService()?.CreateLogger(typeof(SettingsBuilder)); + private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); public ModVerifyAppSettings BuildSettings(BaseModVerifyOptions options) { @@ -33,12 +30,6 @@ public ModVerifyAppSettings BuildSettings(BaseModVerifyOptions options) private ModVerifyAppSettings BuildFromVerifyVerb(VerifyVerbOption verifyOptions) { - var output = Environment.CurrentDirectory; - var outDir = verifyOptions.OutputDirectory; - - if (!string.IsNullOrEmpty(outDir)) - output = _fileSystem.Path.GetFullPath(_fileSystem.Path.Combine(Environment.CurrentDirectory, outDir!)); - return new ModVerifyAppSettings { VerifyPipelineSettings = new VerifyPipelineSettings @@ -54,9 +45,7 @@ private ModVerifyAppSettings BuildFromVerifyVerb(VerifyVerbOption verifyOptions) }, AppThrowsOnMinimumSeverity = verifyOptions.MinimumFailureSeverity, GameInstallationsSettings = BuildInstallationSettings(verifyOptions), - GlobalReportSettings = BuilderGlobalReportSettings(verifyOptions), - ReportOutput = output, - Offline = verifyOptions.OfflineMode + ReportSettings = BuildReportSettings(verifyOptions), }; VerificationSeverity? GetVerifierMinimumThrowSeverity() @@ -66,8 +55,8 @@ private ModVerifyAppSettings BuildFromVerifyVerb(VerifyVerbOption verifyOptions) { if (minFailSeverity == null) { - _logger?.LogWarning($"Verification is configured to fail fast but 'minFailSeverity' is not specified. " + - $"Using severity '{VerificationSeverity.Information}'."); + _logger?.LogWarning(ModVerifyConstants.ConsoleEventId, + "Verification is configured to fail fast but 'minFailSeverity' is not specified. Using severity '{Info}'.", VerificationSeverity.Information); minFailSeverity = VerificationSeverity.Information; } @@ -97,49 +86,28 @@ private ModVerifyAppSettings BuildFromCreateBaselineVerb(CreateBaselineVerbOptio }, AppThrowsOnMinimumSeverity = null, GameInstallationsSettings = BuildInstallationSettings(baselineVerb), - GlobalReportSettings = BuilderGlobalReportSettings(baselineVerb), + ReportSettings = BuildReportSettings(baselineVerb), NewBaselinePath = baselineVerb.OutputFile, - ReportOutput = null, - Offline = baselineVerb.OfflineMode }; } - private GlobalVerifyReportSettings BuilderGlobalReportSettings(BaseModVerifyOptions options) + private static ModVerifyReportSettings BuildReportSettings(BaseModVerifyOptions options) { - return new GlobalVerifyReportSettings + var baselinePath = (options as VerifyVerbOption)?.Baseline; + + return new ModVerifyReportSettings { - Baseline = CreateBaseline(), - Suppressions = CreateSuppressions(), + BaselinePath = baselinePath, MinimumReportSeverity = options.MinimumSeverity, + SearchBaselineLocally = SearchLocally(options), + SuppressionsPath = options.Suppressions }; - VerificationBaseline CreateBaseline() - { - // It does not make sense to create a baseline on another baseline. - if (options is not VerifyVerbOption verifyOptions || string.IsNullOrEmpty(verifyOptions.Baseline)) - return VerificationBaseline.Empty; - - using var fs = _fileSystem.FileStream.New(verifyOptions.Baseline!, FileMode.Open, FileAccess.Read); - - try - { - return VerificationBaseline.FromJson(fs); - } - catch (IncompatibleBaselineException) - { - Console.WriteLine($"The baseline '{verifyOptions.Baseline}' is not compatible with with version of ModVerify." + - $"{Environment.NewLine}Please generate a new baseline file or download the latest version." + - $"{Environment.NewLine}"); - throw; - } - } - - SuppressionList CreateSuppressions() + static bool SearchLocally(BaseModVerifyOptions o) { - if (options.Suppressions is null) - return SuppressionList.Empty; - using var fs = _fileSystem.FileStream.New(options.Suppressions, FileMode.Open, FileAccess.Read); - return SuppressionList.FromJson(fs); + if (o is not VerifyVerbOption v) + return false; + return v.SearchBaselineLocally || v.LaunchedWithoutArguments(); } } @@ -167,16 +135,16 @@ private GameInstallationsSettings BuildInstallationSettings(BaseModVerifyOptions var gamePath = options.GamePath; if (!string.IsNullOrEmpty(gamePath)) - gamePath = _fileSystem.Path.GetFullPath(gamePath); + gamePath = _fileSystem.Path.GetFullPath(gamePath!); string? fallbackGamePath = null; if (!string.IsNullOrEmpty(gamePath) && !string.IsNullOrEmpty(options.FallbackGamePath)) - fallbackGamePath = _fileSystem.Path.GetFullPath(options.FallbackGamePath); + fallbackGamePath = _fileSystem.Path.GetFullPath(options.FallbackGamePath!); var autoPath = options.AutoPath; if (!string.IsNullOrEmpty(autoPath)) - autoPath = _fileSystem.Path.GetFullPath(autoPath); + autoPath = _fileSystem.Path.GetFullPath(autoPath!); return new GameInstallationsSettings { diff --git a/src/ModVerify.CliApp/Updates/GithubReleaseEntry.cs b/src/ModVerify.CliApp/Updates/Github/GithubReleaseEntry.cs similarity index 91% rename from src/ModVerify.CliApp/Updates/GithubReleaseEntry.cs rename to src/ModVerify.CliApp/Updates/Github/GithubReleaseEntry.cs index 968e6a9..4cd4585 100644 --- a/src/ModVerify.CliApp/Updates/GithubReleaseEntry.cs +++ b/src/ModVerify.CliApp/Updates/Github/GithubReleaseEntry.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace AET.ModVerifyTool.Updates; +namespace AET.ModVerify.App.Updates.Github; [JsonUnmappedMemberHandling(JsonUnmappedMemberHandling.Skip)] [method: JsonConstructor] diff --git a/src/ModVerify.CliApp/Updates/GithubReleaseList.cs b/src/ModVerify.CliApp/Updates/Github/GithubReleaseList.cs similarity index 70% rename from src/ModVerify.CliApp/Updates/GithubReleaseList.cs rename to src/ModVerify.CliApp/Updates/Github/GithubReleaseList.cs index a54edfb..92ef368 100644 --- a/src/ModVerify.CliApp/Updates/GithubReleaseList.cs +++ b/src/ModVerify.CliApp/Updates/Github/GithubReleaseList.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -namespace AET.ModVerifyTool.Updates; +namespace AET.ModVerify.App.Updates.Github; internal sealed class GithubReleaseList : List; \ No newline at end of file diff --git a/src/ModVerify.CliApp/Updates/ModVerifyUpdaterChecker.cs b/src/ModVerify.CliApp/Updates/Github/GithubUpdateChecker.cs similarity index 60% rename from src/ModVerify.CliApp/Updates/ModVerifyUpdaterChecker.cs rename to src/ModVerify.CliApp/Updates/Github/GithubUpdateChecker.cs index d0ab59b..df4d769 100644 --- a/src/ModVerify.CliApp/Updates/ModVerifyUpdaterChecker.cs +++ b/src/ModVerify.CliApp/Updates/Github/GithubUpdateChecker.cs @@ -1,29 +1,31 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System; +using System; using System.IO; using System.Linq; using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Semver; -namespace AET.ModVerifyTool.Updates; +namespace AET.ModVerify.App.Updates.Github; -internal sealed class ModVerifyUpdaterChecker +internal class GithubUpdateChecker { private readonly ILogger? _logger; + private readonly ModVerifyAppEnvironment _appEnvironment; - public ModVerifyUpdaterChecker(IServiceProvider serviceProvider) + public GithubUpdateChecker(IServiceProvider serviceProvider) { _logger = serviceProvider.GetService()?.CreateLogger(GetType()); + _appEnvironment = serviceProvider.GetRequiredService(); } - public async Task CheckForUpdateAsync() + public async Task CheckForUpdateAsync() { var githubReleases = await DownloadReleaseList().ConfigureAwait(false); - var branch = ModVerifyUpdaterInformation.BranchName; + var branch = GithubUpdateConstants.BranchName; var latestRelease = githubReleases.FirstOrDefault(r => r.Branch == branch); if (latestRelease == null) @@ -32,20 +34,20 @@ public async Task CheckForUpdateAsync() if (!SemVersion.TryParse(latestRelease.Tag, SemVersionStyles.Any, out var latestVersion)) throw new InvalidOperationException($"Cannot create a version from tag '{latestRelease.Tag}'."); - var currentVersion = ModVerifyUpdaterInformation.CurrentVersion; + var currentVersion = _appEnvironment.AssemblyInfo.InformationalAsSemVer(); if (currentVersion is null) throw new InvalidOperationException("Unable to get current version."); if (SemVersion.ComparePrecedence(currentVersion, latestVersion) >= 0) { - _logger?.LogDebug($"No update available - [Current Version = {currentVersion}], [Available Version = {latestVersion}]"); + _logger?.LogDebug("No update available - [Current Version = {CurrentVersion}], [Available Version = {LatestVersion}]", currentVersion, latestVersion); return default; } - _logger?.LogDebug($"Update available - [Current Version = {currentVersion}], [Available Version = {latestVersion}]"); - return new UpdateInfo + _logger?.LogDebug("Update available - [Current Version = {CurrentVersion}], [Available Version = {LatestVersion}]", currentVersion, latestVersion); + return new GithubUpdateInfo { - DownloadLink = ModVerifyUpdaterInformation.ModVerifyReleasesDownloadLink, + DownloadLink = GithubUpdateConstants.ModVerifyReleasesDownloadLink, IsUpdateAvailable = true, NewVersion = latestVersion.ToString() }; @@ -54,8 +56,8 @@ public async Task CheckForUpdateAsync() private static async Task DownloadReleaseList() { using var httpClient = new HttpClient(); - httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(ModVerifyUpdaterInformation.UserAgent); - using var downloadStream = await httpClient.GetStreamAsync(ModVerifyUpdaterInformation.GithubReleasesApiLink).ConfigureAwait(false); + httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(GithubUpdateConstants.UserAgent); + using var downloadStream = await httpClient.GetStreamAsync(GithubUpdateConstants.GithubReleasesApiLink).ConfigureAwait(false); using var jsonStream = new MemoryStream(); await downloadStream.CopyToAsync(jsonStream).ConfigureAwait(false); diff --git a/src/ModVerify.CliApp/Updates/Github/GithubUpdateConstants.cs b/src/ModVerify.CliApp/Updates/Github/GithubUpdateConstants.cs new file mode 100644 index 0000000..7695246 --- /dev/null +++ b/src/ModVerify.CliApp/Updates/Github/GithubUpdateConstants.cs @@ -0,0 +1,9 @@ +namespace AET.ModVerify.App.Updates.Github; + +internal static class GithubUpdateConstants +{ + public const string BranchName = "main"; + public const string GithubReleasesApiLink = "https://api.github.com/repos/AlamoEngine-Tools/ModVerify/releases"; + public const string ModVerifyReleasesDownloadLink = "https://github.com/AlamoEngine-Tools/ModVerify/releases/latest"; + public const string UserAgent = "AET.Modifo"; +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Updates/UpdateInfo.cs b/src/ModVerify.CliApp/Updates/Github/GithubUpdateInfo.cs similarity index 75% rename from src/ModVerify.CliApp/Updates/UpdateInfo.cs rename to src/ModVerify.CliApp/Updates/Github/GithubUpdateInfo.cs index b6a6505..8d8f532 100644 --- a/src/ModVerify.CliApp/Updates/UpdateInfo.cs +++ b/src/ModVerify.CliApp/Updates/Github/GithubUpdateInfo.cs @@ -1,8 +1,8 @@ using System.Diagnostics.CodeAnalysis; -namespace AET.ModVerifyTool.Updates; +namespace AET.ModVerify.App.Updates.Github; -internal readonly struct UpdateInfo +internal readonly struct GithubUpdateInfo { public string DownloadLink { get; init; } diff --git a/src/ModVerify.CliApp/Updates/ModVerifyUpdateMode.cs b/src/ModVerify.CliApp/Updates/ModVerifyUpdateMode.cs new file mode 100644 index 0000000..882a6a2 --- /dev/null +++ b/src/ModVerify.CliApp/Updates/ModVerifyUpdateMode.cs @@ -0,0 +1,8 @@ +namespace AET.ModVerify.App.Updates; + +public enum ModVerifyUpdateMode +{ + CheckOnly, + InteractiveUpdate, + AutoUpdate, +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Updates/ModVerifyUpdater.cs b/src/ModVerify.CliApp/Updates/ModVerifyUpdater.cs new file mode 100644 index 0000000..af7d369 --- /dev/null +++ b/src/ModVerify.CliApp/Updates/ModVerifyUpdater.cs @@ -0,0 +1,149 @@ +using AET.ModVerify.App.Updates.Github; +using AET.ModVerify.App.Updates.SelfUpdate; +using AET.ModVerify.App.Utilities; +using AnakinRaW.ApplicationBase; +using AnakinRaW.ApplicationBase.Update.Options; +using AnakinRaW.AppUpdaterFramework.Metadata.Update; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace AET.ModVerify.App.Updates; + +internal sealed class ModVerifyUpdater +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger? _logger; + private readonly ModVerifyAppEnvironment _appEnvironment; + + public ModVerifyUpdater(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + _logger = serviceProvider.GetService()?.CreateLogger(GetType()); + _appEnvironment = serviceProvider.GetRequiredService(); + } + + public async Task RunUpdateProcedure(ApplicationUpdateOptions updateOptions, ModVerifyUpdateMode mode) + { + _logger?.LogTrace("Running update procedure - '{mode}'", mode); + + // If we are in the check-only mode, GitHub check is sufficient. + if (mode == ModVerifyUpdateMode.CheckOnly) + { + await CheckForUpdateAndReport().ConfigureAwait(false); + return; + } + + await UpdateApplication(updateOptions, mode).ConfigureAwait(false); + } + + private async Task UpdateApplication(ApplicationUpdateOptions updateOptions, ModVerifyUpdateMode mode) + { + if (!_appEnvironment.IsUpdatable(out var updatableEnvironment)) + { + _logger?.LogWarning("Application is not updatable, yet we entered the update path. Checking only."); + await CheckForUpdateAndReport().ConfigureAwait(false); + return; + } + + var updater = new ModVerifyApplicationUpdater(updatableEnvironment, _serviceProvider); + + var actualBranchName = updater.GetBranchNameFromRegistry(updateOptions.BranchName, false); + var branch = updater.CreateBranch(actualBranchName, updateOptions.ManifestUrl); + + using (ConsoleUtilities.CreateHorizontalFrame(length: 40, startWithNewLine: true, newLineAtEnd: true)) + { + var currentAction = "checking for update"; + try + { + var updateCheckSpinner = new ConsoleSpinnerOptions + { + CompletedMessage = "Update check completed.", + RunningMessage = "Checking for update...", + FailedMessage = "Update check failed", + HideCursor = true + }; + + var updateCatalog = await ConsoleSpinner.Run(async () => + await updater.CheckForUpdateAsync(branch, CancellationToken.None), + updateCheckSpinner); + + + if (updateCatalog.Action != UpdateCatalogAction.Update) + { + Console.WriteLine("No update available."); + return; + } + + Console.ForegroundColor = ConsoleColor.DarkGreen; + Console.WriteLine($"New update available: Version {updateCatalog.UpdateReference.Version}"); + Console.ResetColor(); + + if (mode == ModVerifyUpdateMode.InteractiveUpdate) + { + var shallUpdate = ConsoleUtilities.UserYesNoQuestion("Do you want to update now?"); + if (!shallUpdate) + return; + } + + currentAction = "updating"; + + + var updatingSpinner = new ConsoleSpinnerOptions + { + RunningMessage = $"Updating {ModVerifyConstants.AppNameString}...", + HideCursor = true + }; + await ConsoleSpinner.Run(async () => + await updater.UpdateAsync(updateCatalog, CancellationToken.None), + updatingSpinner); + } + catch (Exception e) + { + WriteError(e, $"Error while {currentAction}: {e.Message}"); + } + } + } + + private async Task CheckForUpdateAndReport() + { + _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Checking for available update..."); + try + { + var updateInfo = await new GithubUpdateChecker(_serviceProvider) + .CheckForUpdateAsync().ConfigureAwait(false); + + if (updateInfo.IsUpdateAvailable) + { + using (ConsoleUtilities.HorizontalLineSeparatedBlock(startWithNewLine: true, newLineAtEnd: true)) + { + Console.ForegroundColor = ConsoleColor.DarkGreen; + Console.WriteLine("New Update Available!"); + Console.ResetColor(); + Console.WriteLine($"Version: {updateInfo.NewVersion}, Download here: {updateInfo.DownloadLink}"); + } + } + else + { + _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "No update available."); + } + } + catch (Exception e) + { + _logger?.LogWarning(ModVerifyConstants.ConsoleEventId, "Unable to check for updates due to an internal error: {message}", e.Message); + _logger?.LogTrace(e, "Checking for update failed: {message}", e.Message); + } + } + + private void WriteError(Exception e, string? customMessage) + { + Console.WriteLine(); + Console.ForegroundColor = ConsoleColor.DarkRed; + if (!string.IsNullOrEmpty(customMessage)) + Console.WriteLine(customMessage); + Console.ResetColor(); + _logger?.LogError(e, e.Message); + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Updates/ModVerifyUpdaterInformation.cs b/src/ModVerify.CliApp/Updates/ModVerifyUpdaterInformation.cs deleted file mode 100644 index 2fa1022..0000000 --- a/src/ModVerify.CliApp/Updates/ModVerifyUpdaterInformation.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Diagnostics; -using Semver; - -namespace AET.ModVerifyTool.Updates; - -internal static class ModVerifyUpdaterInformation -{ - public const string BranchName = "main"; - public const string GithubReleasesApiLink = "https://api.github.com/repos/AlamoEngine-Tools/ModVerify/releases"; - public const string ModVerifyReleasesDownloadLink = "https://github.com/AlamoEngine-Tools/ModVerify/releases/latest"; - public const string UserAgent = "AET.Modifo"; - - public static readonly SemVersion? CurrentVersion; - - static ModVerifyUpdaterInformation() - { - var currentAssembly = typeof(ModVerifyUpdaterInformation).Assembly; - var fi = FileVersionInfo.GetVersionInfo(currentAssembly.Location); - SemVersion.TryParse(fi.ProductVersion, SemVersionStyles.Any, out var currentVersion); - CurrentVersion = currentVersion; - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Updates/SelfUpdate/AssemblyInfo.cs b/src/ModVerify.CliApp/Updates/SelfUpdate/AssemblyInfo.cs new file mode 100644 index 0000000..4b94b5e --- /dev/null +++ b/src/ModVerify.CliApp/Updates/SelfUpdate/AssemblyInfo.cs @@ -0,0 +1,6 @@ +#if NETFRAMEWORK +using AnakinRaW.AppUpdaterFramework.Attributes; + +[assembly: UpdateProduct("AET ModVerify")] +[assembly: UpdateComponent("AET.ModVerify.Exe", Name = "AET ModVerify")] +#endif \ No newline at end of file diff --git a/src/ModVerify.CliApp/Updates/SelfUpdate/ModVerifyApplicationUpdater.cs b/src/ModVerify.CliApp/Updates/SelfUpdate/ModVerifyApplicationUpdater.cs new file mode 100644 index 0000000..9b97043 --- /dev/null +++ b/src/ModVerify.CliApp/Updates/SelfUpdate/ModVerifyApplicationUpdater.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using AnakinRaW.ApplicationBase.Environment; +using AnakinRaW.ApplicationBase.Update; +using AnakinRaW.AppUpdaterFramework.Metadata.Product; +using AnakinRaW.AppUpdaterFramework.Metadata.Update; + +namespace AET.ModVerify.App.Updates.SelfUpdate; + + +internal class ModVerifyApplicationUpdater( + UpdatableApplicationEnvironment environment, + IServiceProvider serviceProvider) + : ApplicationUpdater(environment, serviceProvider) +{ + public override async Task CheckForUpdateAsync(ProductBranch branch, CancellationToken token = default) + { + var updateReference = ProductService.CreateProductReference(null, branch); + + var updateCatalog = await UpdateService.CheckForUpdatesAsync(updateReference, token); + + if (updateCatalog is null) + throw new InvalidOperationException("Update service was already doing something."); + + return updateCatalog.Action is UpdateCatalogAction.Install or UpdateCatalogAction.Uninstall + ? throw new NotSupportedException("Install and Uninstall operations are not supported") + : updateCatalog; + } + + public override async Task UpdateAsync(UpdateCatalog updateCatalog, CancellationToken token = default) + { + var updateResult = await UpdateService.UpdateAsync(updateCatalog, token).ConfigureAwait(false); + if (updateResult is null) + throw new InvalidOperationException("There is already an update running."); + + var resultHandler = new ModVerifyUpdateResultHandler(Environment, ServiceProvider); + await resultHandler.Handle(updateResult).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Updates/SelfUpdate/ModVerifyUpdateResultHandler.cs b/src/ModVerify.CliApp/Updates/SelfUpdate/ModVerifyUpdateResultHandler.cs new file mode 100644 index 0000000..42ab413 --- /dev/null +++ b/src/ModVerify.CliApp/Updates/SelfUpdate/ModVerifyUpdateResultHandler.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; +using AnakinRaW.ApplicationBase.Environment; +using AnakinRaW.ApplicationBase.Update; +using AnakinRaW.AppUpdaterFramework.Handlers; +using AnakinRaW.AppUpdaterFramework.Updater; + +namespace AET.ModVerify.App.Updates.SelfUpdate; + +internal sealed class ModVerifyUpdateResultHandler( + UpdatableApplicationEnvironment applicationEnvironment, + IServiceProvider serviceProvider) + : ApplicationUpdateResultHandler(applicationEnvironment, serviceProvider) +{ + protected override Task ShowError(UpdateResult updateResult) + { + Console.WriteLine(); + Console.WriteLine($"Update failed with error: {updateResult.ErrorMessage}"); + return base.ShowError(updateResult); + } + + protected override void RestartApplication(RestartReason reason) + { + Console.WriteLine(); + Console.WriteLine("Restarting application to complete update..."); + base.RestartApplication(reason); + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Utilities/ExtensionMethods.cs b/src/ModVerify.CliApp/Utilities/ExtensionMethods.cs new file mode 100644 index 0000000..b2e7ab0 --- /dev/null +++ b/src/ModVerify.CliApp/Utilities/ExtensionMethods.cs @@ -0,0 +1,41 @@ +using System.Diagnostics.CodeAnalysis; +using AET.ModVerify.App.Settings.CommandLine; +using AnakinRaW.ApplicationBase.Environment; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Infrastructure.Games; + +namespace AET.ModVerify.App.Utilities; + +internal static class ExtensionMethods +{ + public static GameEngineType ToEngineType(this GameType type) + { + return type == GameType.Foc ? GameEngineType.Foc : GameEngineType.Eaw; + } + + public static GameType FromEngineType(this GameEngineType type) + { + return type == GameEngineType.Foc ? GameType.Foc : GameType.Eaw; + } + + extension(ApplicationEnvironment modVerifyEnvironment) + { + public bool IsUpdatable() + { + return modVerifyEnvironment.IsUpdatable(out _); + } + + public bool IsUpdatable([NotNullWhen(true)] out UpdatableApplicationEnvironment? updatableEnvironment) + { + updatableEnvironment = modVerifyEnvironment as UpdatableApplicationEnvironment; + return updatableEnvironment is not null; + } + } + + public static bool LaunchedWithoutArguments(this BaseModVerifyOptions options) + { + if (options is VerifyVerbOption verifyOptions) + return verifyOptions.IsRunningWithoutArguments; + return false; + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Utilities/ModVerifyConsoleUtilities.cs b/src/ModVerify.CliApp/Utilities/ModVerifyConsoleUtilities.cs new file mode 100644 index 0000000..6e6664a --- /dev/null +++ b/src/ModVerify.CliApp/Utilities/ModVerifyConsoleUtilities.cs @@ -0,0 +1,30 @@ +using AnakinRaW.ApplicationBase; +using Figgle; +using System; + +namespace AET.ModVerify.App.Utilities; + +[GenerateFiggleText("HeaderText", "standard", ModVerifyConstants.AppNameString)] +internal static partial class ModVerifyConsoleUtilities +{ + public static void WriteHeader(string? version = null) + { + const int lineLength = 73; + const string author = "by AnakinRaW"; + + ConsoleUtilities.WriteHorizontalLine('*', lineLength); + Console.WriteLine(HeaderText); + if (!string.IsNullOrEmpty(version)) + { + Console.ForegroundColor = ConsoleColor.DarkGray; + ConsoleUtilities.WriteLineRight($"Version: {version}", lineLength); + Console.ResetColor(); + Console.WriteLine(); + } + ConsoleUtilities.WriteHorizontalLine('*', lineLength); + + ConsoleUtilities.WriteLineRight(author, lineLength); + Console.WriteLine(); + Console.WriteLine(); + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Utilities/Spinner.cs b/src/ModVerify.CliApp/Utilities/Spinner.cs new file mode 100644 index 0000000..f25edda --- /dev/null +++ b/src/ModVerify.CliApp/Utilities/Spinner.cs @@ -0,0 +1,172 @@ +using AnakinRaW.CommonUtilities; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace AET.ModVerify.App.Utilities; + +/// +/// Options for configuring a . +/// +public sealed class ConsoleSpinnerOptions +{ + public string? RunningMessage { get; init; } + public string? CompletedMessage { get; init; } + public string? FailedMessage { get; init; } + public bool HideCursor { get; init; } + public TextWriter Writer { get; init; } = Console.Out; + public int Interval { get; init; } = 200; + public string[] Animation { get; init; } = ["|", "/", "-", "\\"]; + + public static ConsoleSpinnerOptions Default { get; } = new(); +} + + + +internal sealed class ConsoleSpinner : IAsyncDisposable +{ + private readonly ConsoleSpinnerOptions _options; + private readonly CancellationTokenSource _cts = new(); + private readonly Task _observedTask; + private readonly bool _origCursorVisibility; + private readonly string[] _animation; + private int _frame; + private int _lastTextLength; + + private ConsoleSpinner(Task observedTask, ConsoleSpinnerOptions options) + { + _observedTask = observedTask; + _options = options; + _animation = options.Animation; + _origCursorVisibility = Console.CursorVisible; + + if (_options.HideCursor) + Console.CursorVisible = false; + + SpinnerLoop().Forget(); + } + + public static async Task Run(Task task, ConsoleSpinnerOptions? options = null) + { + options ??= ConsoleSpinnerOptions.Default; + await using var spinner = new ConsoleSpinner(task, options); + var result = await task.ConfigureAwait(false); + return result; + } + + public static async Task Run(Task task, ConsoleSpinnerOptions? options = null) + { + options ??= ConsoleSpinnerOptions.Default; + await using var spinner = new ConsoleSpinner(task, options); + await task.ConfigureAwait(false); + } + + public static Task Run(Func asyncAction, ConsoleSpinnerOptions? options = null) + { + if (asyncAction is null) + throw new ArgumentNullException(nameof(asyncAction)); + return Run(asyncAction(), options); + } + + public static Task Run(Func> asyncAction, ConsoleSpinnerOptions? options = null) + { + if (asyncAction is null) + throw new ArgumentNullException(nameof(asyncAction)); + return Run(asyncAction(), options); + } + + public static ConsoleSpinner Endless(ConsoleSpinnerOptions? options = null) + { + options ??= ConsoleSpinnerOptions.Default; + var tcs = new TaskCompletionSource(); + return new ConsoleSpinner(tcs.Task, options); + } + + private async Task SpinnerLoop() + { + try + { + while (!_cts.IsCancellationRequested && !_observedTask.IsCompleted) + { + await ShowFrameAsync(); + await Task.Delay(_options.Interval, _cts.Token); + } + } + catch (OperationCanceledException) + { + // Ignore + } + } + + private async Task ShowFrameAsync() + { + // Clear previous content if any + if (_lastTextLength > 0) + { + await ClearTextAsync(_lastTextLength); + } + + // Write new frame + var frameChar = _animation[_frame++ % _animation.Length]; + var text = string.IsNullOrEmpty(_options.RunningMessage) + ? frameChar + : $"{frameChar} {_options.RunningMessage}"; + + await _options.Writer.WriteAsync(text); + await _options.Writer.FlushAsync(); + _lastTextLength = text.Length; + } + + public async Task CleanupAndFinishAsync() + { + // Clear spinner content + if (_lastTextLength > 0) + { + await ClearTextAsync(_lastTextLength); + } + + // Show final message if needed + var finalMessage = GetFinalMessage(); + if (!string.IsNullOrEmpty(finalMessage)) + { + await _options.Writer.WriteLineAsync(finalMessage); + } + + await _options.Writer.FlushAsync(); + Console.CursorVisible = _origCursorVisibility; + } + + private async Task ClearTextAsync(int length) + { + // Use backspaces to go back, spaces to clear, backspaces to return to start + await _options.Writer.WriteAsync(new string('\b', length)); + await _options.Writer.WriteAsync(new string(' ', length)); + await _options.Writer.WriteAsync(new string('\b', length)); + } + + private string? GetFinalMessage() + { + return _observedTask.IsCompleted + ? _observedTask.IsFaulted || _observedTask.IsCanceled ? _options.FailedMessage : _options.CompletedMessage + : null; + } + + public async ValueTask DisposeAsync() + { + await CleanupAndFinishAsync(); + + await CastAndDispose(_cts); + await CastAndDispose(_observedTask); + + return; + + static async ValueTask CastAndDispose(IDisposable resource) + { + if (resource is IAsyncDisposable resourceAsyncDisposable) + await resourceAsyncDisposable.DisposeAsync(); + else + resource.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/ModVerify/ModVerify.csproj b/src/ModVerify/ModVerify.csproj index fecceb2..427458d 100644 --- a/src/ModVerify/ModVerify.csproj +++ b/src/ModVerify/ModVerify.csproj @@ -25,18 +25,25 @@ - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + + + + + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - diff --git a/src/ModVerify/Pipeline/GameVerifierPipelineStep.cs b/src/ModVerify/Pipeline/GameVerifierPipelineStep.cs index a27ee9b..6405dbd 100644 --- a/src/ModVerify/Pipeline/GameVerifierPipelineStep.cs +++ b/src/ModVerify/Pipeline/GameVerifierPipelineStep.cs @@ -26,13 +26,13 @@ protected override void RunCore(CancellationToken token) { try { - Logger?.LogDebug($"Running verifier '{GameVerifier.FriendlyName}'..."); + Logger?.LogDebug("Running verifier '{Name}'...", GameVerifier.FriendlyName); ReportProgress(new ProgressEventArgs(0.0, "Started")); GameVerifier.Progress += OnVerifyProgress; GameVerifier.Verify(token); - Logger?.LogDebug($"Finished verifier '{GameVerifier.FriendlyName}'"); + Logger?.LogDebug("Finished verifier '{Name}'", GameVerifier.FriendlyName); ReportProgress(new ProgressEventArgs(1.0, "Finished")); } finally diff --git a/src/ModVerify/Pipeline/GameVerifyPipeline.cs b/src/ModVerify/Pipeline/GameVerifyPipeline.cs index b7ac00d..810651a 100644 --- a/src/ModVerify/Pipeline/GameVerifyPipeline.cs +++ b/src/ModVerify/Pipeline/GameVerifyPipeline.cs @@ -103,6 +103,7 @@ protected override void OnError(object sender, StepRunnerErrorEventArgs e) { if (FailFast && e.Exception is GameVerificationException v) { + // TODO: Apply globalMinSeverity if (v.Errors.All(error => _reportSettings.Baseline.Contains(error) || _reportSettings.Suppressions.Suppresses(error))) return; } diff --git a/src/ModVerify/Reporting/IncompatibleBaselineException.cs b/src/ModVerify/Reporting/IncompatibleBaselineException.cs deleted file mode 100644 index c9a9eb1..0000000 --- a/src/ModVerify/Reporting/IncompatibleBaselineException.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace AET.ModVerify.Reporting; - -public sealed class IncompatibleBaselineException : Exception -{ - public override string Message => "The specified baseline is not compatible to this version of the application."; -} \ No newline at end of file diff --git a/src/ModVerify/Reporting/InvalidBaselineException.cs b/src/ModVerify/Reporting/InvalidBaselineException.cs new file mode 100644 index 0000000..37ab9c8 --- /dev/null +++ b/src/ModVerify/Reporting/InvalidBaselineException.cs @@ -0,0 +1,14 @@ +using System; + +namespace AET.ModVerify.Reporting; + +public sealed class InvalidBaselineException : Exception +{ + public InvalidBaselineException(string message) : base(message) + { + } + + public InvalidBaselineException(string? message, Exception? inner) : base(message, inner) + { + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonBaselineParser.cs b/src/ModVerify/Reporting/Json/JsonBaselineParser.cs new file mode 100644 index 0000000..ef2f5d1 --- /dev/null +++ b/src/ModVerify/Reporting/Json/JsonBaselineParser.cs @@ -0,0 +1,38 @@ +using System; +using System.IO; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace AET.ModVerify.Reporting.Json; + +public static class JsonBaselineParser +{ + public static VerificationBaseline Parse(Stream dataStream) + { + if (dataStream == null) + throw new ArgumentNullException(nameof(dataStream)); + try + { + var jsonNode = JsonNode.Parse(dataStream); + var jsonBaseline = ParseCore(jsonNode); + + if (jsonBaseline is null) + throw new InvalidBaselineException($"Unable to parse input from stream to {nameof(VerificationBaseline)}. Unknown Error!"); + + return new VerificationBaseline(jsonBaseline); + } + catch (JsonException cause) + { + throw new InvalidBaselineException(cause.Message, cause); + } + } + + private static JsonVerificationBaseline? ParseCore(JsonNode? jsonData) + { + if (jsonData is null) + return null; + + JsonBaselineSchema.Evaluate(jsonData); + return jsonData.Deserialize(); + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonBaselineSchema.cs b/src/ModVerify/Reporting/Json/JsonBaselineSchema.cs new file mode 100644 index 0000000..7c8b02a --- /dev/null +++ b/src/ModVerify/Reporting/Json/JsonBaselineSchema.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Text.Json.Nodes; +using Json.Schema; + +namespace AET.ModVerify.Reporting.Json; + +public static class JsonBaselineSchema +{ + private static readonly JsonSchema Schema; + private static readonly EvaluationOptions EvaluationOptions; + + static JsonBaselineSchema() + { + var evalvOptions = new EvaluationOptions + { + EvaluateAs = SpecVersion.Draft202012, + OutputFormat = OutputFormat.Hierarchical, + AllowReferencesIntoUnknownKeywords = false + }; + + Schema = GetCurrentSchema(); + EvaluationOptions = evalvOptions; + } + + /// + /// Evaluates a JSON node against the ModVerify Baseline JSON schema. + /// + /// The JSON node to evaluate. + /// is not valid against the baseline JSON schema. + /// is . + public static void Evaluate(JsonNode json) + { + if (json == null) + throw new ArgumentNullException(nameof(json)); + var result = Schema.Evaluate(json, EvaluationOptions); + ThrowOnValidationError(result); + } + + private static void ThrowOnValidationError(EvaluationResults result) + { + if (!result.IsValid) + { + var error = GetFirstError(result); + var errorMessage = "Baseline JSON not valid"; + + if (error is null) + errorMessage += ": Unknown Error"; + else + errorMessage += $": {error}"; + + throw new InvalidBaselineException(errorMessage); + } + } + + private static KeyValuePair? GetFirstError(EvaluationResults result) + { + if (result.HasErrors) + return result.Errors!.First(); + foreach (var child in result.Details) + { + var error = GetFirstError(child); + if (error is not null) + return error; + } + return null; + } + + private static JsonSchema GetCurrentSchema() + { + using var resourceStream = typeof(JsonBaselineSchema) + .Assembly.GetManifestResourceStream($"AET.ModVerify.Resources.Schemas.{GetVersionedPath()}.baseline.json"); + + Debug.Assert(resourceStream is not null); + var schema = JsonSchema.FromStream(resourceStream!).GetAwaiter().GetResult(); + + var id = schema.GetId(); + if (id is null || !UriContainsVersion(id, VerificationBaseline.LatestVersionString)) + throw new InvalidOperationException("Internal error: The embedded schema version does not match the expected baseline version!"); + + return schema; + } + + private static bool UriContainsVersion(Uri id, string latestVersionString) + { + foreach (var segment in id.Segments) + { + var trimmed = segment.AsSpan().TrimEnd('/'); + if (trimmed.Equals(latestVersionString, StringComparison.OrdinalIgnoreCase)) + return true; + } + return false; + } + + private static string GetVersionedPath() + { + var version = VerificationBaseline.LatestVersion; + var sb = new StringBuilder(); + + AddVersionSegment(version.Major, ref sb); + AddVersionSegment(version.Minor, ref sb); + AddVersionSegment(version.Build, ref sb); + AddVersionSegment(version.Revision, ref sb); + + // Remove the trailing dot + sb.Length -= 1; + + return sb.ToString(); + + static void AddVersionSegment(int segment, ref StringBuilder sb) + { + if (segment >= 0) + { + sb.Append('_'); + sb.Append(segment); + sb.Append("."); + } + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonVerificationError.cs b/src/ModVerify/Reporting/Json/JsonVerificationError.cs index ac80810..55f7b92 100644 --- a/src/ModVerify/Reporting/Json/JsonVerificationError.cs +++ b/src/ModVerify/Reporting/Json/JsonVerificationError.cs @@ -31,7 +31,7 @@ private JsonVerificationError( string message, VerificationSeverity severity, IEnumerable? contextEntries, - string asset) + string? asset) { Id = id; VerifierChain = verifierChain ?? []; diff --git a/src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs b/src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs index a2a00c5..cb6b258 100644 --- a/src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs +++ b/src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs @@ -14,6 +14,7 @@ internal sealed class GameAssertErrorReporter(IGameRepository gameRepository, IS protected override ErrorData CreateError(EngineAssert assert) { + // TODO: Why is context not used atm? var context = new List(); if (assert.Value is not null) diff --git a/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs b/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs index 601e424..7de81eb 100644 --- a/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs +++ b/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs @@ -7,49 +7,46 @@ namespace AET.ModVerify.Reporting.Reporters; public static class VerificationReportersExtensions { - public static IServiceCollection RegisterJsonReporter(this IServiceCollection serviceCollection) + extension(IServiceCollection serviceCollection) { - return RegisterJsonReporter(serviceCollection, new JsonReporterSettings + public IServiceCollection RegisterJsonReporter() { - OutputDirectory = "." - }); - } + return RegisterJsonReporter(serviceCollection, new JsonReporterSettings + { + OutputDirectory = "." + }); + } - public static IServiceCollection RegisterTextFileReporter(this IServiceCollection serviceCollection) - { - return RegisterTextFileReporter(serviceCollection, new TextFileReporterSettings + public IServiceCollection RegisterTextFileReporter() { - OutputDirectory = "." - }); - } + return RegisterTextFileReporter(serviceCollection, new TextFileReporterSettings + { + OutputDirectory = "." + }); + } - public static IServiceCollection RegisterConsoleReporter(this IServiceCollection serviceCollection, bool summaryOnly = false) - { - return RegisterConsoleReporter(serviceCollection, new VerifyReportSettings + public IServiceCollection RegisterConsoleReporter(bool summaryOnly = false) { - MinimumReportSeverity = VerificationSeverity.Error - }, summaryOnly); - } + return RegisterConsoleReporter(serviceCollection, new VerifyReportSettings + { + MinimumReportSeverity = VerificationSeverity.Error + }, summaryOnly); + } - public static IServiceCollection RegisterJsonReporter( - this IServiceCollection serviceCollection, - JsonReporterSettings settings) - { - return serviceCollection.AddSingleton(sp => new JsonReporter(settings, sp)); - } + public IServiceCollection RegisterJsonReporter(JsonReporterSettings settings) + { + return serviceCollection.AddSingleton(sp => new JsonReporter(settings, sp)); + } - public static IServiceCollection RegisterTextFileReporter( - this IServiceCollection serviceCollection, - TextFileReporterSettings settings) - { - return serviceCollection.AddSingleton(sp => new TextFileReporter(settings, sp)); - } + public IServiceCollection RegisterTextFileReporter(TextFileReporterSettings settings) + { + return serviceCollection.AddSingleton(sp => new TextFileReporter(settings, sp)); + } - public static IServiceCollection RegisterConsoleReporter( - this IServiceCollection serviceCollection, - VerifyReportSettings settings, - bool summaryOnly = false) - { - return serviceCollection.AddSingleton(sp => new ConsoleReporter(settings, summaryOnly, sp)); + public IServiceCollection RegisterConsoleReporter(VerifyReportSettings settings, + bool summaryOnly = false) + { + return serviceCollection.AddSingleton(sp => new ConsoleReporter(settings, summaryOnly, sp)); + } } } \ No newline at end of file diff --git a/src/ModVerify/Reporting/VerificationBaseline.cs b/src/ModVerify/Reporting/VerificationBaseline.cs index 55a8973..c37539b 100644 --- a/src/ModVerify/Reporting/VerificationBaseline.cs +++ b/src/ModVerify/Reporting/VerificationBaseline.cs @@ -11,7 +11,8 @@ namespace AET.ModVerify.Reporting; public sealed class VerificationBaseline : IReadOnlyCollection { - private static readonly Version LatestVersion = new(2, 0); + public static readonly Version LatestVersion = new(2, 0); + public static readonly string LatestVersionString = LatestVersion.ToString(2); public static readonly VerificationBaseline Empty = new(VerificationSeverity.Information, []); @@ -60,14 +61,7 @@ public Task ToJsonAsync(Stream stream) public static VerificationBaseline FromJson(Stream stream) { - var baselineJson = JsonSerializer.Deserialize(stream, JsonSerializerOptions.Default); - if (baselineJson is null) - throw new InvalidOperationException("Unable to deserialize baseline."); - - if (baselineJson.Version is null || baselineJson.Version != LatestVersion) - throw new IncompatibleBaselineException(); - - return new VerificationBaseline(baselineJson); + return JsonBaselineParser.Parse(stream); } /// diff --git a/src/ModVerify/Resources/Schemas/2.0/baseline.json b/src/ModVerify/Resources/Schemas/2.0/baseline.json new file mode 100644 index 0000000..2520a58 --- /dev/null +++ b/src/ModVerify/Resources/Schemas/2.0/baseline.json @@ -0,0 +1,70 @@ +{ + "$id": "https://AlamoEngine-Tools.github.io/schemas/mod-verify/2.0/baseline", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Represents a baseline for AET ModVerify", + "type": "object", + "$defs": { + "severity": { + "enum": [ "Information", "Warning", "Error", "Critical" ] + }, + "error": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + }, + "asset": { + "type": "string" + }, + "severity": { + "$ref": "#/$defs/severity" + }, + "verifiers": { + "type": "array", + "items": { + "type": "string" + } + }, + "context": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "message", + "asset", + "severity", + "verifiers", + "context" + ], + "additionalProperties": false + } + }, + "properties": { + "version": { + "const": "2.0" + }, + "minSeverity": { + "$ref": "#/$defs/severity" + }, + "errors": { + "type": "array", + "items": { + "$ref": "#/$defs/error" + }, + "additionalItems": false + } + }, + "required": [ + "version", + "minSeverity", + "errors" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/src/ModVerify/Utilities/VerificationErrorExtensions.cs b/src/ModVerify/Utilities/VerificationErrorExtensions.cs index 1244b45..e5aa9fe 100644 --- a/src/ModVerify/Utilities/VerificationErrorExtensions.cs +++ b/src/ModVerify/Utilities/VerificationErrorExtensions.cs @@ -6,23 +6,24 @@ namespace AET.ModVerify.Utilities; public static class VerificationErrorExtensions { - public static IEnumerable ApplyBaseline(this IEnumerable errors, - VerificationBaseline baseline) + extension(IEnumerable errors) { - if (errors == null) - throw new ArgumentNullException(nameof(errors)); - if (baseline == null) - throw new ArgumentNullException(nameof(baseline)); - return baseline.Apply(errors); - } + public IEnumerable ApplyBaseline(VerificationBaseline baseline) + { + if (errors == null) + throw new ArgumentNullException(nameof(errors)); + if (baseline == null) + throw new ArgumentNullException(nameof(baseline)); + return baseline.Apply(errors); + } - public static IEnumerable ApplySuppressions(this IEnumerable errors, - SuppressionList suppressions) - { - if (errors == null) - throw new ArgumentNullException(nameof(errors)); - if (suppressions == null) - throw new ArgumentNullException(nameof(suppressions)); - return suppressions.Apply(errors); + public IEnumerable ApplySuppressions(SuppressionList suppressions) + { + if (errors == null) + throw new ArgumentNullException(nameof(errors)); + if (suppressions == null) + throw new ArgumentNullException(nameof(suppressions)); + return suppressions.Apply(errors); + } } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameLocations.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameLocations.cs index 18539a7..3b77732 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameLocations.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameLocations.cs @@ -7,6 +7,11 @@ namespace PG.StarWarsGame.Engine; public sealed class GameLocations { + /// + /// Gets the path that represents the topmost playable target. This is typically the actual mod selected by the user. + /// + public string TargetPath { get; } + public IReadOnlyList ModPaths { get; } public string GamePath { get; } @@ -38,5 +43,9 @@ public GameLocations(IList modPaths, string gamePath, IList fall ModPaths = modPaths.ToList(); GamePath = gamePath; FallbackPaths = fallbackPaths.ToList(); + + TargetPath = ModPaths.Count > 0 + ? ModPaths[0] + : GamePath; } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameManagerBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameManagerBase.cs index 7fa4d02..130fb50 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameManagerBase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameManagerBase.cs @@ -62,7 +62,7 @@ public async Task InitializeAsync(CancellationToken token) } catch (Exception e) { - Logger?.LogError(e, $"Initialization of {this} failed: {e.Message}"); + Logger?.LogError(e, "Initialization of {Class} failed: {Message}", this, e.Message); throw; } OnInitialized(); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager.cs index 82ce825..879c16f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager.cs @@ -79,7 +79,7 @@ public IReadOnlyDictionary GetTextureEn { if (!_perComponentTextures.TryGetValue(component, out var textures)) { - Logger?.LogDebug($"The component '{component}' has no overrides. Using default textures."); + Logger?.LogDebug("The component '{Component}' has no overrides. Using default textures.", component); componentExist = false; return DefaultTextureEntries; } @@ -92,7 +92,7 @@ public bool TryGetTextureEntry(string component, GuiComponentType key, out Compo { if (!_perComponentTextures.TryGetValue(component, out var textures)) { - Logger?.LogDebug($"The component '{component}' has no overrides. Using default textures."); + Logger?.LogDebug("The component '{Component}' has no overrides. Using default textures.", component); textures = _defaultTextures; } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs index 1304a89..e952db8 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs @@ -84,7 +84,7 @@ protected FileFoundInfo GetFileInfoFromMasterMeg(ReadOnlySpan filePath) if (filePath.Length > PGConstants.MaxMegEntryPathLength) { - Logger.LogWarning($"Trying to open a MEG entry which is longer than 259 characters: '{filePath.ToString()}'"); + Logger.LogWarning("Trying to open a MEG entry which is longer than 259 characters: '{FilePath}'", filePath.ToString()); return default; } @@ -97,7 +97,7 @@ protected FileFoundInfo GetFileInfoFromMasterMeg(ReadOnlySpan filePath) if (fileName.Length > PGConstants.MaxMegEntryPathLength) { - Logger.LogWarning($"Trying to open a MEG entry which is longer than 259 characters after normalization: '{fileName.ToString()}'"); + Logger.LogWarning("Trying to open a MEG entry which is longer than 259 characters after normalization: '{FileName}'", fileName.ToString()); return default; } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.cs index 42bfa54..d1451b0 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.cs @@ -116,9 +116,9 @@ public void AddMegFile(string megFile) if (megArchive is null) { if (IsSpeechMeg(megFile)) - Logger.LogDebug($"Unable to find Speech MEG file at '{megFile}'"); + Logger.LogDebug("Unable to find Speech MEG file at '{MegFile}'", megFile); else - Logger.LogWarning($"Unable to find MEG file at '{megFile}'"); + Logger.LogWarning("Unable to find MEG file at '{MegFile}'", megFile); return; } @@ -217,7 +217,7 @@ protected IList LoadMegArchivesFromXml(string lookupPath) if (xmlStream is null) { - Logger.LogWarning($"Unable to find MegaFiles.xml at '{lookupPath}'"); + Logger.LogWarning("Unable to find MegaFiles.xml at '{LookupPath}'", lookupPath); return Array.Empty(); } @@ -251,12 +251,12 @@ internal void Seal() if (megFileStream is not FileSystemStream fileSystemStream) { if (IsSpeechMeg(megPath)) - Logger.LogDebug($"Unable to find Speech MEG file '{megPath}'"); + Logger.LogDebug("Unable to find Speech MEG file '{MegPath}'", megPath); else { var message = $"Unable to find MEG file '{megPath}'"; _errorReporter.Assert(EngineAssert.Create(EngineAssertKind.FileNotFound, megPath, [], message)); - Logger.LogWarning($"Unable to find MEG file '{megPath}'"); + Logger.LogWarning("Unable to find MEG file '{MegPath}'", megPath); } return null; } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj index 6055649..e54e6f0 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj @@ -1,6 +1,6 @@  - netstandard2.0;netstandard2.1;net9.0 + netstandard2.0;netstandard2.1;net10.0 PG.StarWarsGame.Engine PG.StarWarsGame.Engine AlamoEngineTools.PG.StarWarsGame.Engine @@ -23,22 +23,16 @@ - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs index d2acfba..8ce1a5a 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs @@ -65,7 +65,7 @@ private async Task InitializeEngine( { try { - _logger?.LogInformation($"Initializing game engine for type '{engineType}'."); + _logger?.LogInformation("Initializing game engine for type '{GameEngineType}'.", engineType); var repoFactory = _serviceProvider.GetRequiredService(); var repository = repoFactory.Create(engineType, gameLocations, errorReporter); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs index 5d5f98a..0c911e2 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs @@ -37,13 +37,13 @@ public void ParseEntriesFromFileListXml( ValueListDictionary entries, Action? onFileParseAction = null) where T : notnull { - Logger.LogDebug($"Parsing container data '{xmlFile}'"); + Logger.LogDebug("Parsing container data '{XmlFile}'", xmlFile); using var containerStream = gameRepository.TryOpenFile(xmlFile); if (containerStream == null) { _reporter?.Report(this, XmlParseErrorEventArgs.FromMissingFile(xmlFile)); - Logger.LogWarning($"Could not find XML file '{xmlFile}'"); + Logger.LogWarning("Could not find XML file '{XmlFile}'", xmlFile); var args = new XmlContainerParserErrorEventArgs(xmlFile, null, true) { @@ -89,7 +89,7 @@ public void ParseEntriesFromFileListXml( if (fileStream is null) { _reporter?.Report(parser, XmlParseErrorEventArgs.FromMissingFile(file)); - Logger.LogWarning($"Could not find XML file '{file}'"); + Logger.LogWarning("Could not find XML file '{File}'", file); var args = new XmlContainerParserErrorEventArgs(file); XmlParseError?.Invoke(this, args); @@ -99,7 +99,7 @@ public void ParseEntriesFromFileListXml( return; } - Logger.LogDebug($"Parsing File '{file}'"); + Logger.LogDebug("Parsing File '{File}'", file); try { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/PG.StarWarsGame.Files.ALO.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/PG.StarWarsGame.Files.ALO.csproj index 4fd3dd4..c653074 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/PG.StarWarsGame.Files.ALO.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/PG.StarWarsGame.Files.ALO.csproj @@ -16,11 +16,7 @@ snupkg - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj index d6a7939..36221be 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj @@ -14,9 +14,10 @@ true snupkg + preview - + diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/PG.StarWarsGame.Files.XML.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/PG.StarWarsGame.Files.XML.csproj index 066e32d..fbb055c 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/PG.StarWarsGame.Files.XML.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/PG.StarWarsGame.Files.XML.csproj @@ -15,15 +15,16 @@ true snupkg true + preview - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/CommonTestBase.cs b/test/ModVerify.CliApp.Test/CommonTestBase.cs new file mode 100644 index 0000000..7d6dc18 --- /dev/null +++ b/test/ModVerify.CliApp.Test/CommonTestBase.cs @@ -0,0 +1,29 @@ +using System; +using System.IO.Abstractions; +using AnakinRaW.CommonUtilities.Hashing; +using Microsoft.Extensions.DependencyInjection; +using PG.Commons; +using Testably.Abstractions.Testing; + +namespace ModVerify.CliApp.Test; + +public abstract class CommonTestBase +{ + protected readonly MockFileSystem FileSystem = new(); + protected readonly IServiceProvider ServiceProvider; + + protected CommonTestBase() + { + var sc = new ServiceCollection(); + sc.AddSingleton(sp => new HashingService(sp)); + sc.AddSingleton(FileSystem); + PetroglyphCommons.ContributeServices(sc); + // ReSharper disable once VirtualMemberCallInConstructor + SetupServices(sc); + ServiceProvider = sc.BuildServiceProvider(); + } + + protected virtual void SetupServices(ServiceCollection serviceCollection) + { + } +} \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/EmbeddedBaselineTest.cs b/test/ModVerify.CliApp.Test/EmbeddedBaselineTest.cs new file mode 100644 index 0000000..cde1dad --- /dev/null +++ b/test/ModVerify.CliApp.Test/EmbeddedBaselineTest.cs @@ -0,0 +1,47 @@ +using AET.ModVerify.App; +using AET.ModVerify.App.Reporting; +using AET.ModVerify.App.Settings; +using AET.ModVerify.Settings; +using AnakinRaW.ApplicationBase.Environment; +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Engine; +using System; +using System.IO.Abstractions; +using ModVerify.CliApp.Test.TestData; +using Testably.Abstractions; + +namespace ModVerify.CliApp.Test; + +public class BaselineSelectorTest +{ + private static readonly IFileSystem FileSystem = new RealFileSystem(); + private static readonly ModVerifyAppSettings TestSettings = new() + { + ReportSettings = new(), + GameInstallationsSettings = new (), + VerifyPipelineSettings = new() + { + GameVerifySettings = new GameVerifySettings(), + VerifiersProvider = new NoVerifierProvider() + } + }; + + private readonly IServiceProvider _serviceProvider; + + public BaselineSelectorTest() + { + var sc = new ServiceCollection(); + sc.AddSingleton(FileSystem); + sc.AddSingleton(new ModVerifyAppEnvironment(typeof(ModVerifyAppEnvironment).Assembly, FileSystem)); + _serviceProvider = sc.BuildServiceProvider(); + } + + [Theory] + // [InlineData(GameEngineType.Eaw)] TODO EaW is currently not supported + [InlineData(GameEngineType.Foc)] + public void LoadEmbeddedBaseline(GameEngineType engineType) + { + // Ensure this operation does not crash, meaning the embedded baseline is at least compatible. + new BaselineSelector(TestSettings, _serviceProvider).LoadEmbeddedBaseline(engineType); + } +} \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj b/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj new file mode 100644 index 0000000..84d1bba --- /dev/null +++ b/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj @@ -0,0 +1,33 @@ + + + + net10.0 + $(TargetFrameworks);net481 + false + preview + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/test/ModVerify.CliApp.Test/ModVerifyOptionsParserTest.cs b/test/ModVerify.CliApp.Test/ModVerifyOptionsParserTest.cs new file mode 100644 index 0000000..294baf7 --- /dev/null +++ b/test/ModVerify.CliApp.Test/ModVerifyOptionsParserTest.cs @@ -0,0 +1,210 @@ +using AET.ModVerify.App.Settings.CommandLine; +using AnakinRaW.ApplicationBase.Environment; +using System; +using System.IO.Abstractions; +using ModVerify.CliApp.Test.TestData; +using Testably.Abstractions; +using ModVerify.CliApp.Test.Utilities; + +namespace ModVerify.CliApp.Test; + +public class ModVerifyOptionsParserTest_Updateable : ModVerifyOptionsParserTestBase +{ + protected override bool IsUpdatable => true; + + protected override ApplicationEnvironment CreateEnvironment() + { + return new UpdatableEnv(GetType().Assembly, FileSystem); + } + + [Fact] + public void Parse_UpdateAppArg() + { + const string argString = "updateApplication --updateBranch test --updateManifestUrl https://examlple.com"; + + var settings = Parser.Parse(argString.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + + Assert.True(settings.HasOptions); + Assert.Null(settings.ModVerifyOptions); + Assert.NotNull(settings.UpdateOptions); + Assert.Equal("test", settings.UpdateOptions.BranchName); + Assert.Equal("https://examlple.com", settings.UpdateOptions.ManifestUrl); + } + + [Fact] + public void Parse_CombinedIsNotAllowed() + { + const string argString = "verify --updateBranch test --updateManifestUrl https://examlple.com"; + + var settings = Parser.Parse(argString.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + + Assert.False(settings.HasOptions); + Assert.Null(settings.ModVerifyOptions); + Assert.Null(settings.UpdateOptions); + } +} + +public class ModVerifyOptionsParserTest_NotUpdateable : ModVerifyOptionsParserTestBase +{ + protected override bool IsUpdatable => false; + + protected override ApplicationEnvironment CreateEnvironment() + { + return new TestEnv(GetType().Assembly, FileSystem); + } + + [Theory] + [InlineData("verify --externalUpdaterResult UpdateSuccess")] + [InlineData("createBaseline --externalUpdaterResult UpdateSuccess")] + [InlineData("verify --junkOption")] + [InlineData("createBaseline --junkOption")] + [InlineData("updateApplication")] + [InlineData("updateApplication --updateBranch test --updateManifestUrl https://examlple.com")] + public void Parse_InvalidArgs_NotUpdateable(string argString) + { + var settings = Parser.Parse(argString.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + + Assert.False(settings.HasOptions); + Assert.Null(settings.ModVerifyOptions); + Assert.Null(settings.UpdateOptions); + } +} + + +public abstract class ModVerifyOptionsParserTestBase +{ + private protected readonly ModVerifyOptionsParser Parser; + protected readonly IFileSystem FileSystem = new RealFileSystem(); + + protected abstract ApplicationEnvironment CreateEnvironment(); + + protected abstract bool IsUpdatable { get; } + + protected ModVerifyOptionsParserTestBase() + { + Parser = new ModVerifyOptionsParser(CreateEnvironment(), null); + } + + [Fact] + public void Parse_NoArgs_IsVerify_IsInteractive() + { + var settings = Parser.Parse([]); + + Assert.True(settings.HasOptions); + var verify = Assert.IsType(settings.ModVerifyOptions); + Assert.True(verify.IsRunningWithoutArguments); + Assert.Null(settings.UpdateOptions); + } + + [Theory] + [InlineData("verify", false)] + [InlineData("verify -v", false)] + [InlineData("createBaseline -o out.json", true)] + [InlineData("createBaseline -v -o out.json", true)] + public void Parse_Interactive(string argString, bool createBaseLine) + { + var settings = Parser.Parse(argString.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + + Assert.True(settings.HasOptions); + if (createBaseLine) + { + Assert.IsType(settings.ModVerifyOptions); + } + else + { + var verify = Assert.IsType(settings.ModVerifyOptions); + Assert.False(verify.IsRunningWithoutArguments); + } + Assert.Null(settings.UpdateOptions); + } + + [Theory] + [InlineData("verify --path myMod", false)] + [InlineData("verify -v --game myGame", false)] + [InlineData("createBaseline -o out.json --path myMod", true)] + [InlineData("createBaseline -v -o out.json --game myGame", true)] + public void Parse_NotInteractive(string argString, bool createBaseLine) + { + var settings = Parser.Parse(argString.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + + Assert.True(settings.HasOptions); + Assert.NotNull(settings.ModVerifyOptions); + + if (createBaseLine) + Assert.IsType(settings.ModVerifyOptions); + else + { + var verify = Assert.IsType(settings.ModVerifyOptions); + Assert.False(verify.IsRunningWithoutArguments); + } + + Assert.Null(settings.UpdateOptions); + } + + [Theory] + [InlineData("verify --path myMod --game myGame")] + [InlineData("verify --game myMod --path myMod")] + [InlineData("verify --mod myMod --path myMod")] + [InlineData("verify --fallbackGame myGame --path myMod")] + public void Parse_InvalidPathConfig(string argString) + { + var settings = Parser.Parse(argString.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + + Assert.False(settings.HasOptions); + Assert.Null(settings.ModVerifyOptions); + Assert.Null(settings.UpdateOptions); + } + + [Theory] + [InlineData("")] + [InlineData("junkVerb")] + [InlineData("junkVerb verify")] + [InlineData("junkVerb verify --v")] + [InlineData("junkVerb --v")] + [InlineData("verify --junkOption")] + [InlineData("verify -v --junkOption")] + [InlineData("updateApplication --junkOption")] + [InlineData("--junkOption")] + [InlineData("junkVerb --junkOption")] + [InlineData("junkVerb --externalUpdaterResult UpdateSuccess")] + [InlineData("-v")] + public void Parse_InvalidArgs(string argString) + { + var settings = Parser.Parse(argString.Split(' ')); + + Assert.False(settings.HasOptions); + Assert.Null(settings.ModVerifyOptions); + Assert.Null(settings.UpdateOptions); + } + + [Fact] + public void Parse_UpdatePerformed_RestartedFromNoArgs() + { + // This only happens when we run without args, performed an auto-update and restarted the application automatically. + const string argString = "--externalUpdaterResult UpdateSuccess"; + + var settings = Parser.Parse(argString.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + + if (!IsUpdatable) + Assert.False(settings.HasOptions); + else + { + Assert.True(settings.HasOptions); + var verify = Assert.IsType(settings.ModVerifyOptions); + Assert.True(verify.IsRunningWithoutArguments); + Assert.Null(settings.UpdateOptions); + } + } + + [Theory] + [InlineData("createBaseline")] + [InlineData("createBaseline -v")] + public void Parse_CreateBaseline_MissingRequired_Fails(string argString) + { + var settings = Parser.Parse(argString.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + + Assert.False(settings.HasOptions); + Assert.Null(settings.ModVerifyOptions); + Assert.Null(settings.UpdateOptions); + } +} \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/TestData/NoVerifierProvider.cs b/test/ModVerify.CliApp.Test/TestData/NoVerifierProvider.cs new file mode 100644 index 0000000..ef7ad74 --- /dev/null +++ b/test/ModVerify.CliApp.Test/TestData/NoVerifierProvider.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using AET.ModVerify.Pipeline; +using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers; +using PG.StarWarsGame.Engine; + +namespace ModVerify.CliApp.Test.TestData; + +internal sealed class NoVerifierProvider : IGameVerifiersProvider +{ + public IEnumerable GetVerifiers(IStarWarsGameEngine database, GameVerifySettings settings, IServiceProvider serviceProvider) + { + yield break; + } +} \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/TestData/TestEnv.cs b/test/ModVerify.CliApp.Test/TestData/TestEnv.cs new file mode 100644 index 0000000..f72f001 --- /dev/null +++ b/test/ModVerify.CliApp.Test/TestData/TestEnv.cs @@ -0,0 +1,11 @@ +using System.IO.Abstractions; +using System.Reflection; +using AnakinRaW.ApplicationBase.Environment; + +namespace ModVerify.CliApp.Test.TestData; + +internal class TestEnv(Assembly assembly, IFileSystem fileSystem) : ApplicationEnvironment(assembly, fileSystem) +{ + public override string ApplicationName => "TestEnv"; + protected override string ApplicationLocalDirectoryName => ApplicationName; +} \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/TestData/UpdatableEnv.cs b/test/ModVerify.CliApp.Test/TestData/UpdatableEnv.cs new file mode 100644 index 0000000..557f522 --- /dev/null +++ b/test/ModVerify.CliApp.Test/TestData/UpdatableEnv.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Reflection; +using AnakinRaW.ApplicationBase.Environment; +using AnakinRaW.AppUpdaterFramework.Configuration; + +namespace ModVerify.CliApp.Test.TestData; + +internal class UpdatableEnv(Assembly assembly, IFileSystem fileSystem) : UpdatableApplicationEnvironment(assembly, fileSystem) +{ + public override string ApplicationName => "TestUpdateEnv"; + protected override string ApplicationLocalDirectoryName => ApplicationName; + public override ICollection UpdateMirrors => []; + public override string UpdateRegistryPath => ApplicationName; + protected override UpdateConfiguration CreateUpdateConfiguration() + { + return UpdateConfiguration.Default; + } +} \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/Utilities/StringExtensions.cs b/test/ModVerify.CliApp.Test/Utilities/StringExtensions.cs new file mode 100644 index 0000000..052c322 --- /dev/null +++ b/test/ModVerify.CliApp.Test/Utilities/StringExtensions.cs @@ -0,0 +1,13 @@ +using System; + +namespace ModVerify.CliApp.Test.Utilities; + +internal static class StringExtensions +{ +#if NETFRAMEWORK + public static string[] Split(this string str, char separator, StringSplitOptions options) + { + return str.Split([separator], options); + } +#endif +} \ No newline at end of file diff --git a/version.json b/version.json index 251e6e2..3e87545 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.0-alpha", + "version": "0.1-beta", "publicReleaseRefSpec": [ "^refs/heads/main$" ], From cb78c793c50e62ad1e3dcf3d7579449ebef1fab4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:33:43 +0100 Subject: [PATCH 3/7] Bump actions/setup-dotnet in the actions-deps group across 1 directory (#28) Bumps the actions-deps group with 1 update in the / directory: [actions/setup-dotnet](https://github.com/actions/setup-dotnet). Updates `actions/setup-dotnet` from 4 to 5 - [Release notes](https://github.com/actions/setup-dotnet/releases) - [Commits](https://github.com/actions/setup-dotnet/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-dotnet dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6e60fde..cc452f9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,7 +23,7 @@ jobs: with: fetch-depth: 0 submodules: recursive - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@v5 with: dotnet-version: 10.0.x - name: Build & Test in Release Mode From f0da6c816837aa05a9a39906a7317f646c9be2c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Dec 2025 19:22:58 +0100 Subject: [PATCH 4/7] Bump the actions-deps group with 2 updates (#30) Bumps the actions-deps group with 2 updates: [actions/upload-artifact](https://github.com/actions/upload-artifact) and [actions/download-artifact](https://github.com/actions/download-artifact). Updates `actions/upload-artifact` from 5 to 6 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v5...v6) Updates `actions/download-artifact` from 6 to 7 - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps - dependency-name: actions/download-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7a9b844..be274ae 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,7 +48,7 @@ jobs: # 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@v5 + uses: actions/upload-artifact@v6 with: name: Binary Releases path: ./releases @@ -68,7 +68,7 @@ jobs: with: fetch-depth: 0 submodules: recursive - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: name: Binary Releases path: ./releases From 53071639915ac53c5da2e8dc37e47989c227524d Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Wed, 11 Feb 2026 10:02:56 +0100 Subject: [PATCH 5/7] Refactor ModVerify to expose and use a VerificationTarget class (#31) * start verify target impl * update deps * update sub * make app running again * reorganize solution * basic support for VerificationTarget type * start refactoring verification target selection * rename stuff * start testing automatic selector * fix and test automatic selector * update to new deps * update to new deps (just to make it compile) * make everything compile at least * to weekly deps check * update license year * rename class * make compile and run again * local deploy script * update gitignore * polishing * update module * various little changes * update deps * use filed keyword * do not thorw stepfailure exception if verification errors occured. * Fail fast should print any error to console * major refactorings * allow skip location in baselines * only search for baselines of the correct engine type * Support argument validation * Setting appFailure does not terminate the verifiers on first error. * udpate assert reporting * make tests compile again * pretty print verify info * do not print location if not exists * simply automatic selector * extract to file * nicer console output * update deps * update dpes * make compile * run tests * windows only tests * fix app is not updatebale sometimes * minor cleanup --- .github/dependabot.yml | 4 +- .github/workflows/test.yml | 2 +- .gitignore | 3 + Directory.Build.props | 4 +- LICENSE | 2 +- deploy-local.ps1 | 75 + global.json | 5 + modules/ModdingToolBase | 2 +- .../App/CreateBaselineAction.cs | 56 + .../App/IModVerifyAppAction.cs | 8 + .../App/ModVerifyApplication.cs | 68 + .../App/ModVerifyApplicationAction.cs | 142 ++ src/ModVerify.CliApp/App/VerifyAction.cs | 60 + src/ModVerify.CliApp/AppArgumentException.cs | 5 + .../GameFinder/GameFinderService.cs | 197 +- .../GameFinder/GameFinderSettings.cs | 14 + .../ModSelectors/AutomaticModSelector.cs | 143 -- .../ModSelectors/IModSelector.cs | 13 - .../ModSelectors/ManualModSelector.cs | 29 - .../ModSelectors/ModSelectorBase.cs | 75 - .../ModSelectors/ModSelectorFactory.cs | 18 - .../ModSelectors/SettingsBasedModSelector.cs | 40 - .../ModSelectors/VerifyInstallationData.cs | 28 - src/ModVerify.CliApp/ModVerify.CliApp.csproj | 39 +- .../ModVerify.CliApp.csproj.DotSettings | 2 + .../ModVerifyAppEnvironment.cs | 24 +- src/ModVerify.CliApp/ModVerifyApplication.cs | 251 -- src/ModVerify.CliApp/ModVerifyConstants.cs | 4 + src/ModVerify.CliApp/Program.cs | 147 +- .../Properties/launchSettings.json | 15 +- .../Reporting/BaselineFactory.cs | 74 +- .../Reporting/BaselineSelector.cs | 102 +- .../EngineInitializeProgressReporter.cs | 27 +- .../Reporting/IBaselineFactory.cs | 26 + .../VerifyConsoleProgressReporter.cs | 13 +- .../Resources/Baselines/baseline-foc.json | 2042 ++++++++++++----- .../CommandLine/BaseModVerifyOptions.cs | 10 +- .../CommandLine/CreateBaselineVerbOption.cs | 3 + .../Settings/CommandLine/VerifyVerbOption.cs | 6 +- .../Settings/CommandLineHelper.cs | 18 + .../Settings/GameInstallationsSettings.cs | 28 - .../Settings/ModVerifyAppSettings.cs | 74 +- .../Settings/ModVerifyReportSettings.cs | 14 - .../Settings/SettingsBuilder.cs | 137 +- .../Settings/VerificationTargetSettings.cs | 28 + .../TargetSelectors/AutomaticSelector.cs | 163 ++ .../ConsoleSelector.cs} | 20 +- .../IVerificationTargetSelector.cs | 8 + .../TargetSelectors/ManualSelector.cs | 73 + .../TargetNotFoundException.cs | 6 + .../VerificationTargetSelectorBase.cs | 136 ++ .../VerificationTargetSelectorFactory.cs | 18 + .../Utilities/ExtensionMethods.cs | 17 +- .../Utilities/ModVerifyConsoleUtilities.cs | 60 + .../Utilities/PathUtilities.cs | 39 + src/ModVerify/GameVerificationException.cs | 8 +- src/ModVerify/ModVerify.csproj | 16 +- .../Pipeline/GameVerifierPipelineStep.cs | 6 +- src/ModVerify/Pipeline/GameVerifyPipeline.cs | 156 +- .../AggregatedVerifyProgressReporter.cs | 1 + .../Pipeline/Progress/VerifyProgressInfo.cs | 2 +- .../Reporting/BaselineVerificationTarget.cs | 23 + .../Reporting/Json/JsonBaselineParser.cs | 16 +- .../Reporting/Json/JsonBaselineSchema.cs | 46 +- .../Reporting/Json/JsonGameLocation.cs | 40 + .../Json/JsonVerificationBaseline.cs | 12 +- .../Reporting/Json/JsonVerificationTarget.cs | 63 + .../Reporting/Reporters/ConsoleReporter.cs | 4 +- .../Engine/EngineErrorReporterBase.cs | 5 +- .../Engine/GameAssertErrorReporter.cs | 14 +- .../Reporting/Reporters/JSON/JsonReporter.cs | 1 - .../Reporting/Reporters/ReporterBase.cs | 2 +- .../VerificationReportersExtensions.cs | 8 +- .../Settings/FileBasedReporterSettings.cs | 10 +- .../Settings/GlobalVerifyReportSettings.cs | 12 +- ...yReportSettings.cs => ReporterSettings.cs} | 2 +- .../Reporting/VerificationBaseline.cs | 19 +- src/ModVerify/Reporting/VerificationError.cs | 3 +- .../Schemas/{2.0 => 2.1}/baseline.json | 58 +- src/ModVerify/Settings/FailFastSetting.cs | 18 + src/ModVerify/Settings/GameVerifySettings.cs | 2 +- .../Settings/VerifyPipelineSettings.cs | 2 +- src/ModVerify/VerificationTarget.cs | 41 + src/ModVerify/Verifiers/AudioFilesVerifier.cs | 2 - .../Verifiers/DuplicateNameFinder.cs | 10 +- src/ModVerify/Verifiers/GameVerifierBase.cs | 5 +- .../CommandBar/CommandBarGameManager.cs | 8 +- .../PG.StarWarsGame.Engine/GameLocations.cs | 26 +- .../PG.StarWarsGame.Engine/GameManagerBase.cs | 5 +- .../GuiDialog/Xml/XmlComponentTextureData.cs | 6 +- .../IGameEngineInitializationReporter.cs | 10 + .../PG.StarWarsGame.Engine/IGameManager.cs | 2 +- .../IO/Repositories/GameRepository.Files.cs | 2 +- .../IO/Repositories/GameRepository.cs | 4 +- .../IPetroglyphStarWarsGameEngineService.cs | 5 +- .../PG.StarWarsGame.Engine.csproj | 11 +- .../PetroglyphStarWarsGameEngineService.cs | 22 +- .../Animations/AnimationCollection.cs | 11 +- .../Rendering/Font/WindowsFontManager.cs | 4 +- .../Parsers/Data/CommandBarComponentParser.cs | 4 +- .../Xml/Parsers/Data/GameObjectParser.cs | 4 +- .../Xml/Parsers/Data/SfxEventParser.cs | 4 +- .../File/CommandBarComponentFileParser.cs | 4 +- .../Xml/Parsers/File/GameObjectFileParser.cs | 4 +- .../Xml/Parsers/File/GuiDialogParser.cs | 4 +- .../Xml/Parsers/File/SfxEventFileParser.cs | 4 +- .../Xml/Parsers/XmlContainerContentParser.cs | 4 +- .../Xml/Parsers/XmlObjectParser.cs | 8 +- .../PG.StarWarsGame.Files.ALO.csproj | 3 - .../PG.StarWarsGame.Files.ChunkFiles.csproj | 5 +- .../PG.StarWarsGame.Files.XML.csproj | 3 +- .../Base/IPetroglyphXmlFileContainerParser.cs | 4 +- .../PetroglyphXmlFileContainerParser.cs | 6 +- test/ModVerify.CliApp.Test/CommonTestBase.cs | 31 +- .../EmbeddedBaselineTest.cs | 17 +- .../ModVerify.CliApp.Test.csproj | 40 +- .../ModVerifyOptionsParserTest.cs | 4 + .../TargetSelectors/AutomaticSelectorTest.cs | 408 ++++ .../TargetSelectors/GameFinderServiceTest.cs | 74 + 119 files changed, 4199 insertions(+), 1776 deletions(-) create mode 100644 deploy-local.ps1 create mode 100644 global.json create mode 100644 src/ModVerify.CliApp/App/CreateBaselineAction.cs create mode 100644 src/ModVerify.CliApp/App/IModVerifyAppAction.cs create mode 100644 src/ModVerify.CliApp/App/ModVerifyApplication.cs create mode 100644 src/ModVerify.CliApp/App/ModVerifyApplicationAction.cs create mode 100644 src/ModVerify.CliApp/App/VerifyAction.cs create mode 100644 src/ModVerify.CliApp/AppArgumentException.cs create mode 100644 src/ModVerify.CliApp/GameFinder/GameFinderSettings.cs delete mode 100644 src/ModVerify.CliApp/ModSelectors/AutomaticModSelector.cs delete mode 100644 src/ModVerify.CliApp/ModSelectors/IModSelector.cs delete mode 100644 src/ModVerify.CliApp/ModSelectors/ManualModSelector.cs delete mode 100644 src/ModVerify.CliApp/ModSelectors/ModSelectorBase.cs delete mode 100644 src/ModVerify.CliApp/ModSelectors/ModSelectorFactory.cs delete mode 100644 src/ModVerify.CliApp/ModSelectors/SettingsBasedModSelector.cs delete mode 100644 src/ModVerify.CliApp/ModSelectors/VerifyInstallationData.cs create mode 100644 src/ModVerify.CliApp/ModVerify.CliApp.csproj.DotSettings delete mode 100644 src/ModVerify.CliApp/ModVerifyApplication.cs create mode 100644 src/ModVerify.CliApp/Reporting/IBaselineFactory.cs create mode 100644 src/ModVerify.CliApp/Settings/CommandLineHelper.cs delete mode 100644 src/ModVerify.CliApp/Settings/GameInstallationsSettings.cs delete mode 100644 src/ModVerify.CliApp/Settings/ModVerifyReportSettings.cs create mode 100644 src/ModVerify.CliApp/Settings/VerificationTargetSettings.cs create mode 100644 src/ModVerify.CliApp/TargetSelectors/AutomaticSelector.cs rename src/ModVerify.CliApp/{ModSelectors/ConsoleModSelector.cs => TargetSelectors/ConsoleSelector.cs} (79%) create mode 100644 src/ModVerify.CliApp/TargetSelectors/IVerificationTargetSelector.cs create mode 100644 src/ModVerify.CliApp/TargetSelectors/ManualSelector.cs create mode 100644 src/ModVerify.CliApp/TargetSelectors/TargetNotFoundException.cs create mode 100644 src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorBase.cs create mode 100644 src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorFactory.cs create mode 100644 src/ModVerify.CliApp/Utilities/PathUtilities.cs create mode 100644 src/ModVerify/Reporting/BaselineVerificationTarget.cs create mode 100644 src/ModVerify/Reporting/Json/JsonGameLocation.cs create mode 100644 src/ModVerify/Reporting/Json/JsonVerificationTarget.cs rename src/ModVerify/Reporting/Settings/{VerifyReportSettings.cs => ReporterSettings.cs} (81%) rename src/ModVerify/Resources/Schemas/{2.0 => 2.1}/baseline.json (55%) create mode 100644 src/ModVerify/Settings/FailFastSetting.cs create mode 100644 src/ModVerify/VerificationTarget.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/IGameEngineInitializationReporter.cs create mode 100644 test/ModVerify.CliApp.Test/TargetSelectors/AutomaticSelectorTest.cs create mode 100644 test/ModVerify.CliApp.Test/TargetSelectors/GameFinderServiceTest.cs 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/test.yml b/.github/workflows/test.yml index cc452f9..aca43b4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,4 +27,4 @@ jobs: with: 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/Directory.Build.props b/Directory.Build.props index d475e7f..5110cdb 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -12,7 +12,7 @@ ModVerify Alamo Engine Tools and Contributors - Copyright © 2025 Alamo Engine Tools and contributors. All rights reserved. + Copyright © 2026 Alamo Engine Tools and contributors. All rights reserved. https://github.com/AlamoEngine-Tools/ModVerify $(RepoRootPath)LICENSE MIT @@ -33,7 +33,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/LICENSE b/LICENSE index 7159126..23fe869 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Alamo Engine Tools +Copyright (c) 2026 Alamo Engine Tools Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/deploy-local.ps1 b/deploy-local.ps1 new file mode 100644 index 0000000..740c579 --- /dev/null +++ b/deploy-local.ps1 @@ -0,0 +1,75 @@ +# Local deployment script for ModVerify to test the update feature. +# This script builds the application, creates an update manifest, and "deploys" it to a local directory. + +$ErrorActionPreference = "Stop" + +$root = $PSScriptRoot +if ([string]::IsNullOrEmpty($root)) { $root = Get-Location } + +$deployRoot = Join-Path $root ".local_deploy" +$stagingDir = Join-Path $deployRoot "staging" +$serverDir = Join-Path $deployRoot "server" +$installDir = Join-Path $deployRoot "install" + +$toolProj = Join-Path $root "src\ModVerify.CliApp\ModVerify.CliApp.csproj" +$creatorProj = Join-Path $root "modules\ModdingToolBase\src\AnakinApps\ApplicationManifestCreator\ApplicationManifestCreator.csproj" +$uploaderProj = Join-Path $root "modules\ModdingToolBase\src\AnakinApps\FtpUploader\FtpUploader.csproj" + +$toolExe = "ModVerify.exe" +$updaterExe = "AnakinRaW.ExternalUpdater.exe" +$manifestCreatorDll = "AnakinRaW.ApplicationManifestCreator.dll" +$uploaderDll = "AnakinRaW.FtpUploader.dll" + +# 1. Clean and Create directories +if (Test-Path $deployRoot) { Remove-Item -Recurse -Force $deployRoot } +New-Item -ItemType Directory -Path $stagingDir | Out-Null +New-Item -ItemType Directory -Path $serverDir | Out-Null +New-Item -ItemType Directory -Path $installDir | Out-Null + +Write-Host "--- Building ModVerify (net481) ---" -ForegroundColor Cyan +dotnet build $toolProj --configuration Release -f net481 --output "$deployRoot\bin\tool" /p:DebugType=None /p:DebugSymbols=false + +Write-Host "--- Building Manifest Creator ---" -ForegroundColor Cyan +dotnet build $creatorProj --configuration Release --output "$deployRoot\bin\creator" + +Write-Host "--- Building Local Uploader ---" -ForegroundColor Cyan +dotnet build $uploaderProj --configuration Release --output "$deployRoot\bin\uploader" + +# 2. Prepare staging +Write-Host "--- Preparing Staging ---" -ForegroundColor Cyan +Copy-Item "$deployRoot\bin\tool\$toolExe" $stagingDir +Copy-Item "$deployRoot\bin\tool\$updaterExe" $stagingDir + +# 3. Create Manifest +# Origin must be an absolute URI for the manifest creator. +# Using 127.0.0.1 and file:// is tricky with Flurl/DownloadManager sometimes. +# We'll use the local path and ensure it's formatted correctly. +$serverPath = (Resolve-Path $serverDir).Path +$serverUri = "file:///$($serverPath.Replace('\', '/'))" +# If we have 3 slashes, Flurl/DownloadManager might still fail on Windows if it expects a certain format. +# However, the ManifestCreator just needs a valid URI for the 'Origin' field in the manifest. +Write-Host "--- Creating Manifest (Origin: $serverUri) ---" -ForegroundColor Cyan +dotnet "$deployRoot\bin\creator\$manifestCreatorDll" ` + -a "$stagingDir\$toolExe" ` + --appDataFiles "$stagingDir\$updaterExe" ` + --origin "$serverUri" ` + -o "$stagingDir" ` + -b "beta" + +# 4. "Deploy" to server using the local uploader +Write-Host "--- Deploying to Local Server ---" -ForegroundColor Cyan +dotnet "$deployRoot\bin\uploader\$uploaderDll" local --base "$serverDir" --source "$stagingDir" + +# 5. Setup a "test" installation +Write-Host "--- Setting up Test Installation ---" -ForegroundColor Cyan +Copy-Item "$deployRoot\bin\tool\*" $installDir -Recurse + +Write-Host "`nLocal deployment complete!" -ForegroundColor Green +Write-Host "Server directory: $serverDir" +Write-Host "Install directory: $installDir" +Write-Host "`nTo test the update:" +Write-Host "1. (Optional) Modify the version in version.json and run this script again to 'push' a new version to the local server." +Write-Host "2. Run ModVerify from the install directory with the following command:" +Write-Host " cd '$installDir'" +Write-Host " .\ModVerify.exe updateApplication --updateManifestUrl '$serverUri'" +Write-Host "`n Note: You can also specify a different branch using --updateBranch if needed." diff --git a/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 index 479a088..0657db4 160000 --- a/modules/ModdingToolBase +++ b/modules/ModdingToolBase @@ -1 +1 @@ -Subproject commit 479a088a2b26dd4a3e2342b2e34f5359b0252e88 +Subproject commit 0657db489e65d288bd3cc27b44d15bbb55fcac05 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/GameFinder/GameFinderService.cs b/src/ModVerify.CliApp/GameFinder/GameFinderService.cs index a88ebd9..0ac8346 100644 --- a/src/ModVerify.CliApp/GameFinder/GameFinderService.cs +++ b/src/ModVerify.CliApp/GameFinder/GameFinderService.cs @@ -1,11 +1,17 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO.Abstractions; +using System.Runtime.InteropServices; +using AET.ModVerify.App.Utilities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine; using PG.StarWarsGame.Infrastructure.Clients.Steam; using PG.StarWarsGame.Infrastructure.Games; +using PG.StarWarsGame.Infrastructure.Games.Registry; using PG.StarWarsGame.Infrastructure.Mods; using PG.StarWarsGame.Infrastructure.Services; using PG.StarWarsGame.Infrastructure.Services.Detection; @@ -29,7 +35,7 @@ public GameFinderService(IServiceProvider serviceProvider) _logger = _serviceProvider.GetService()?.CreateLogger(GetType()); } - public GameFinderResult FindGames() + public GameFinderResult FindGames(GameFinderSettings settings) { var detectors = new List { @@ -37,10 +43,47 @@ public GameFinderResult FindGames() new SteamPetroglyphStarWarsGameDetector(_serviceProvider), }; - return FindGames(detectors); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var registryFactory = _serviceProvider.GetRequiredService(); + detectors.Add(new RegistryGameDetector( + registryFactory.CreateRegistry(GameType.Eaw), + registryFactory.CreateRegistry(GameType.Foc), + false, _serviceProvider)); + } + + return FindGames(detectors, settings); + } + + public IGame FindGame(string gamePath, GameFinderSettings settings) + { + var detectors = new List + { + new DirectoryGameDetector(_fileSystem.DirectoryInfo.New(gamePath), _serviceProvider), + }; + return FindGames(detectors, settings).Game; } - public GameFinderResult FindGamesFromPathOrGlobal(string path) + public bool TryFindGame(string gamePath, GameFinderSettings settings, [NotNullWhen(true)]out IGame? game) + { + var detectors = new List + { + new DirectoryGameDetector(_fileSystem.DirectoryInfo.New(gamePath), _serviceProvider), + }; + + try + { + game = FindGames(detectors, settings).Game; + return true; + } + catch (GameNotFoundException) + { + game = null; + return false; + } + } + + public GameFinderResult FindGamesFromPathOrGlobal(string path, GameFinderSettings settings) { // There are four common situations: // 1. path points to the actual game directory @@ -49,87 +92,136 @@ public GameFinderResult FindGamesFromPathOrGlobal(string path) // 4. path points to a "detached mod" at a completely different location var givenDirectory = _fileSystem.DirectoryInfo.New(path); var possibleGameDir = givenDirectory.Parent?.Parent; + + + // We need to check the local paths first, before falling back to global detectors, + // to ensure that we always find the correct game installation, + // especially if the user did not request a specific engine. - var detectors = new List + var localDetectors = new List { - // Case 1 - new DirectoryGameDetector(givenDirectory, _serviceProvider) + new DirectoryGameDetector(givenDirectory, _serviceProvider), }; + if (possibleGameDir is not null) + localDetectors.Add(new DirectoryGameDetector(possibleGameDir, _serviceProvider)); - // Case 2 - if (possibleGameDir is not null) - detectors.Add(new DirectoryGameDetector(possibleGameDir, _serviceProvider)); + // Case 1 & 2 + if (TryFindGames(localDetectors, settings, out var finderResult)) + return finderResult; + + + // There is the rare scenario where the user specified a specific engine, + // but path points to a game with the opposite engine. + // This does not make sense and the global detectors can not handle this. + // Thus, we need to check against this. + if (settings.Engine is not null) + { + if (TryFindGame(path, new GameFinderSettings + { + Engine = settings.Engine.Value.Opposite(), + InitMods = false, + SearchFallbackGame = false + }, out _)) + { + var e = new GameNotFoundException( + $"The specified game engine '{settings.Engine.Value}' does not match engine of the specified path '{path}'."); + _logger?.LogTrace(e, e.Message); + throw e; + } + } // Cases 3 & 4 - detectors.Add(new SteamPetroglyphStarWarsGameDetector(_serviceProvider)); - return FindGames(detectors); + return FindGames(CreateGlobalDetectors(), settings); } - private bool TryDetectGame(GameType gameType, IList detectors, out GameDetectionResult result) + private bool TryFindGames(IList detectors, GameFinderSettings settings, + [NotNullWhen(true)] out GameFinderResult? finderResult) { - var gd = new CompositeGameDetector(detectors, _serviceProvider); - try { - result = gd.Detect(gameType); - if (result.GameLocation is null) - return false; + finderResult = FindGames(detectors, settings); return true; } - catch (Exception e) + catch (GameNotFoundException) { - result = GameDetectionResult.NotInstalled(gameType); - _logger?.LogTrace("Unable to find game installation: {Message}", e.Message); + finderResult = null; return false; } } - private GameFinderResult FindGames(IList detectors) + private GameFinderResult FindGames(IList detectors, GameFinderSettings settings) { - // FoC needs to be tried first - if (!TryDetectGame(GameType.Foc, detectors, out var result)) + GameDetectionResult? detectionResult = null; + if (settings.Engine is GameEngineType.Eaw) { + _logger?.LogTrace("Trying to find requested EaW installation."); + if (!TryDetectGame(GameType.Eaw, detectors, out detectionResult)) + { + var e = new GameNotFoundException($"Unable to find requested game installation '{settings.Engine}'. Wrong install path?"); + _logger?.LogTrace(e, e.Message); + throw e; + } + } + + if (detectionResult is null && !TryDetectGame(GameType.Foc, detectors, out detectionResult)) + { + if (settings.Engine is GameEngineType.Foc) + { + var e = new GameNotFoundException($"Unable to find requested game installation '{settings.Engine}'. Wrong install path?"); + _logger?.LogTrace(e, e.Message); + throw e; + } + + // If the engine is unspecified, we also need to check for EaW. _logger?.LogTrace("Unable to find FoC installation. Trying again with EaW..."); - if (!TryDetectGame(GameType.Eaw, detectors, out result)) + if (!TryDetectGame(GameType.Eaw, detectors, out detectionResult)) throw new GameNotFoundException("Unable to find game installation: Wrong install path?"); } - if (result.GameLocation is null) - throw new GameNotFoundException("Unable to find game installation: Wrong install path?"); + Debug.Assert(detectionResult.GameLocation is not null); - _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, - "Found game installation: {ResultGameIdentity} at {GameLocationFullName}", result.GameIdentity, result.GameLocation.FullName); + _logger?.LogDebug("Found game installation: {ResultGameIdentity} at {GameLocationFullName}", + detectionResult.GameIdentity, detectionResult.GameLocation!.FullName); - var game = _gameFactory.CreateGame(result, CultureInfo.InvariantCulture); - - SetupMods(game); + var game = _gameFactory.CreateGame(detectionResult, CultureInfo.InvariantCulture); + if (settings.InitMods) + SetupMods(game); IGame? fallbackGame = null; - // If the game is Foc we want to set up Eaw as well as the fallbackGame - if (game.Type == GameType.Foc) + if (SearchForFallbackGame(settings, detectionResult)) { - var fallbackDetectors = new List(); - - if (game.Platform == GamePlatform.SteamGold) - fallbackDetectors.Add(new SteamPetroglyphStarWarsGameDetector(_serviceProvider)); - else - throw new NotImplementedException("Searching fallback game for non-Steam games is currently is not yet implemented."); - - if (!TryDetectGame(GameType.Eaw, fallbackDetectors, out var fallbackResult) || fallbackResult.GameLocation is null) + if (!TryDetectGame(GameType.Eaw, CreateGlobalDetectors(), out var fallbackResult)) throw new GameNotFoundException("Unable to find fallback game installation: Wrong install path?"); - _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, - "Found fallback game installation: {FallbackResultGameIdentity} at {GameLocationFullName}", fallbackResult.GameIdentity, fallbackResult.GameLocation.FullName); + _logger?.LogDebug("Found fallback game installation: {FallbackResultGameIdentity} at {GameLocationFullName}", + fallbackResult.GameIdentity, fallbackResult.GameLocation!.FullName); fallbackGame = _gameFactory.CreateGame(fallbackResult, CultureInfo.InvariantCulture); - SetupMods(fallbackGame); + if (settings.InitMods) + SetupMods(fallbackGame); } return new GameFinderResult(game, fallbackGame); } + private static bool SearchForFallbackGame(GameFinderSettings settings, GameDetectionResult? foundGame) + { + if (settings.Engine is GameEngineType.Eaw) + return false; + if (foundGame is { Installed: true, GameIdentity.Type: GameType.Eaw }) + return false; + return settings.SearchFallbackGame; + } + + private bool TryDetectGame(GameType gameType, IList detectors, out GameDetectionResult result) + { + var gd = new CompositeGameDetector(detectors, _serviceProvider, true); + result = gd.Detect(gameType); + return result.GameLocation is not null; + } + private void SetupMods(IGame game) { var modFinder = _serviceProvider.GetRequiredService(); @@ -152,4 +244,21 @@ private void SetupMods(IGame game) mod.ResolveDependencies(); } } + + private IList CreateGlobalDetectors() + { + var detectors = new List + { + new SteamPetroglyphStarWarsGameDetector(_serviceProvider), + }; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var registryFactory = _serviceProvider.GetRequiredService(); + detectors.Add(new RegistryGameDetector( + registryFactory.CreateRegistry(GameType.Eaw), + registryFactory.CreateRegistry(GameType.Foc), + false, _serviceProvider)); + } + return detectors; + } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/GameFinder/GameFinderSettings.cs b/src/ModVerify.CliApp/GameFinder/GameFinderSettings.cs new file mode 100644 index 0000000..fc57b24 --- /dev/null +++ b/src/ModVerify.CliApp/GameFinder/GameFinderSettings.cs @@ -0,0 +1,14 @@ +using PG.StarWarsGame.Engine; + +namespace AET.ModVerify.App.GameFinder; + +internal sealed class GameFinderSettings +{ + internal static readonly GameFinderSettings Default = new(); + + public bool InitMods { get; init; } = true; + + public bool SearchFallbackGame { get; init; } = true; + + public GameEngineType? Engine { get; init; } = null; +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectors/AutomaticModSelector.cs b/src/ModVerify.CliApp/ModSelectors/AutomaticModSelector.cs deleted file mode 100644 index 717db7b..0000000 --- a/src/ModVerify.CliApp/ModSelectors/AutomaticModSelector.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; -using System.Globalization; -using System.IO.Abstractions; -using System.Linq; -using AET.ModVerify.App.GameFinder; -using AET.ModVerify.App.Settings; -using AET.ModVerify.App.Utilities; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Infrastructure; -using PG.StarWarsGame.Infrastructure.Games; -using PG.StarWarsGame.Infrastructure.Mods; -using PG.StarWarsGame.Infrastructure.Services; -using PG.StarWarsGame.Infrastructure.Services.Detection; - -namespace AET.ModVerify.App.ModSelectors; - -internal class AutomaticModSelector(IServiceProvider serviceProvider) : ModSelectorBase(serviceProvider) -{ - private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); - - public override GameLocations? Select( - GameInstallationsSettings settings, - out IPhysicalPlayableObject? targetObject, - out GameEngineType? actualEngineType) - { - var pathToVerify = settings.AutoPath; - if (pathToVerify is null) - throw new InvalidOperationException("path to verify cannot be null."); - - actualEngineType = settings.EngineType; - - GameFinderResult finderResult; - try - { - finderResult = GameFinderService.FindGamesFromPathOrGlobal(pathToVerify); - } - catch (GameNotFoundException) - { - Logger?.LogError(ModVerifyConstants.ConsoleEventId, "Unable to find games based of the given location '{SettingsGamePath}'. Consider specifying all paths manually.", settings.GamePath); - targetObject = null!; - return null; - } - - - var modOrGame = GetAttachedModOrGame(finderResult, actualEngineType, pathToVerify); - - if (modOrGame is not null) - { - var actualType = modOrGame.Game.Type.ToEngineType(); - actualEngineType ??= actualType; - if (actualEngineType != actualType) - throw new ArgumentException($"The specified game type '{actualEngineType}' does not match the actual type of the game or mod to verify."); - - targetObject = modOrGame; - return GetLocations(targetObject, finderResult, settings.AdditionalFallbackPaths); - } - - if (!settings.EngineType.HasValue) - throw new ArgumentException("Unable to determine game type. Use --type argument to set the game type."); - - Logger?.LogDebug("The requested mod at '{PathToVerify}' is detached from its games.", pathToVerify); - - // The path is a detached mod, that exists on a different location than the game. - var result = GetDetachedModLocations(pathToVerify, finderResult, settings, out var mod); - targetObject = mod; - return result; - } - - private IPhysicalPlayableObject? GetAttachedModOrGame(GameFinderResult finderResult, GameEngineType? requestedEngineType, string searchPath) - { - var fullSearchPath = _fileSystem.Path.GetFullPath(searchPath); - - if (finderResult.Game.Directory.FullName.Equals(fullSearchPath, StringComparison.OrdinalIgnoreCase)) - { - if (finderResult.Game.Type.ToEngineType() != requestedEngineType) - throw new ArgumentException($"The specified game type '{requestedEngineType}' does not match the actual type of the game '{searchPath}' to verify."); - return finderResult.Game; - } - - if (finderResult.FallbackGame is not null && - finderResult.FallbackGame.Directory.FullName.Equals(fullSearchPath, StringComparison.OrdinalIgnoreCase)) - { - if (finderResult.FallbackGame.Type.ToEngineType() != requestedEngineType) - throw new ArgumentException($"The specified game type '{requestedEngineType}' does not match the actual type of the game '{searchPath}' to verify."); - return finderResult.FallbackGame; - } - - return GetMatchingModFromGame(finderResult.Game, requestedEngineType, fullSearchPath) ?? - GetMatchingModFromGame(finderResult.FallbackGame, requestedEngineType, fullSearchPath); - } - - private GameLocations GetDetachedModLocations(string modPath, GameFinderResult gameResult, GameInstallationsSettings settings, out IPhysicalMod mod) - { - IGame game = null!; - - if (gameResult.Game.Type.ToEngineType() == settings.EngineType) - game = gameResult.Game; - if (gameResult.FallbackGame is not null && gameResult.FallbackGame.Type.ToEngineType() == settings.EngineType) - game = gameResult.FallbackGame; - - if (game is null) - throw new GameNotFoundException($"Unable to find game of type '{settings.EngineType}'"); - - var modFinder = ServiceProvider.GetRequiredService(); - var modRef = modFinder.FindMods(game, _fileSystem.DirectoryInfo.New(modPath)).FirstOrDefault(); - - if (modRef is null) - throw new NotSupportedException($"The mod at '{modPath}' is not compatible to the found game '{game}'."); - - var modFactory = ServiceProvider.GetRequiredService(); - mod = modFactory.CreatePhysicalMod(game, modRef, CultureInfo.InvariantCulture); - - game.AddMod(mod); - - mod.ResolveDependencies(); - - return GetLocations(mod, gameResult, settings.AdditionalFallbackPaths); - } - - private static IPhysicalMod? GetMatchingModFromGame(IGame? game, GameEngineType? requestedEngineType, string modPath) - { - if (game is null) - return null; - - var isGameSupported = !requestedEngineType.HasValue || game.Type.ToEngineType() == requestedEngineType; - foreach (var mod in game.Game.Mods) - { - if (mod is IPhysicalMod physicalMod) - { - if (physicalMod.Directory.FullName.Equals(modPath, StringComparison.OrdinalIgnoreCase)) - { - if (!isGameSupported) - throw new ArgumentException($"The specified game type '{requestedEngineType}' does not match the actual type of the mod '{modPath}' to verify."); - return physicalMod; - } - } - } - - return null; - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectors/IModSelector.cs b/src/ModVerify.CliApp/ModSelectors/IModSelector.cs deleted file mode 100644 index a04858c..0000000 --- a/src/ModVerify.CliApp/ModSelectors/IModSelector.cs +++ /dev/null @@ -1,13 +0,0 @@ -using AET.ModVerify.App.Settings; -using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Infrastructure; - -namespace AET.ModVerify.App.ModSelectors; - -internal interface IModSelector -{ - GameLocations? Select( - GameInstallationsSettings settings, - out IPhysicalPlayableObject? targetObject, - out GameEngineType? actualEngineType); -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectors/ManualModSelector.cs b/src/ModVerify.CliApp/ModSelectors/ManualModSelector.cs deleted file mode 100644 index 34cf39d..0000000 --- a/src/ModVerify.CliApp/ModSelectors/ManualModSelector.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using AET.ModVerify.App.Settings; -using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Infrastructure; - -namespace AET.ModVerify.App.ModSelectors; - -internal class ManualModSelector(IServiceProvider serviceProvider) : ModSelectorBase(serviceProvider) -{ - public override GameLocations Select( - GameInstallationsSettings settings, - out IPhysicalPlayableObject? targetObject, - out GameEngineType? actualEngineType) - { - actualEngineType = settings.EngineType; - targetObject = null; - - if (!actualEngineType.HasValue) - throw new ArgumentException("Unable to determine game type. Use --type argument to set the game type."); - - if (string.IsNullOrEmpty(settings.GamePath)) - throw new ArgumentException("Argument --game must be set."); - - return new GameLocations( - settings.ModPaths, - settings.GamePath!, - GetFallbackPaths(settings.FallbackGamePath, settings.AdditionalFallbackPaths)); - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectors/ModSelectorBase.cs b/src/ModVerify.CliApp/ModSelectors/ModSelectorBase.cs deleted file mode 100644 index 8dd1d90..0000000 --- a/src/ModVerify.CliApp/ModSelectors/ModSelectorBase.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using AET.ModVerify.App.GameFinder; -using AET.ModVerify.App.Settings; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Infrastructure; -using PG.StarWarsGame.Infrastructure.Mods; -using PG.StarWarsGame.Infrastructure.Services.Dependencies; - -namespace AET.ModVerify.App.ModSelectors; - -internal abstract class ModSelectorBase : IModSelector -{ - protected readonly ILogger? Logger; - protected readonly GameFinderService GameFinderService; - protected readonly IServiceProvider ServiceProvider; - - protected ModSelectorBase(IServiceProvider serviceProvider) - { - ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - Logger = serviceProvider.GetService()?.CreateLogger(GetType()); - GameFinderService = new GameFinderService(serviceProvider); - } - - public abstract GameLocations? Select( - GameInstallationsSettings settings, - out IPhysicalPlayableObject? targetObject, - out GameEngineType? actualEngineType); - - protected GameLocations GetLocations(IPhysicalPlayableObject playableObject, GameFinderResult finderResult, IList additionalFallbackPaths) - { - var fallbacks = GetFallbackPaths(finderResult, playableObject, additionalFallbackPaths); - var modPaths = GetModPaths(playableObject); - return new GameLocations(modPaths, playableObject.Game.Directory.FullName, fallbacks); - } - - private static IList GetFallbackPaths(GameFinderResult finderResult, IPlayableObject gameOrMod, IList additionalFallbackPaths) - { - var coercedFallbackGame = finderResult.FallbackGame; - if (gameOrMod.Equals(finderResult.FallbackGame)) - coercedFallbackGame = null; - else if (gameOrMod.Game.Equals(finderResult.FallbackGame)) - coercedFallbackGame = null; - - return GetFallbackPaths(coercedFallbackGame?.Directory.FullName, additionalFallbackPaths); - } - - - protected static IList GetFallbackPaths(string? fallbackGame, IList additionalFallbackPaths) - { - var fallbacks = new List(); - if (fallbackGame is not null) - fallbacks.Add(fallbackGame); - foreach (var fallback in additionalFallbackPaths) - fallbacks.Add(fallback); - - return fallbacks; - } - - - private IList GetModPaths(IPhysicalPlayableObject modOrGame) - { - if (modOrGame is not IMod mod) - return Array.Empty(); - - var traverser = ServiceProvider.GetRequiredService(); - return traverser.Traverse(mod) - .OfType().Select(x => x.Directory.FullName) - .ToList(); - } - -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectors/ModSelectorFactory.cs b/src/ModVerify.CliApp/ModSelectors/ModSelectorFactory.cs deleted file mode 100644 index 07ef263..0000000 --- a/src/ModVerify.CliApp/ModSelectors/ModSelectorFactory.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using AET.ModVerify.App.Settings; - -namespace AET.ModVerify.App.ModSelectors; - -internal class ModSelectorFactory(IServiceProvider serviceProvider) -{ - public IModSelector CreateSelector(GameInstallationsSettings settings) - { - if (settings.Interactive) - return new ConsoleModSelector(serviceProvider); - if (settings.UseAutoDetection) - return new AutomaticModSelector(serviceProvider); - if (settings.ManualSetup) - return new ManualModSelector(serviceProvider); - throw new ArgumentException("Unknown option configuration provided."); - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectors/SettingsBasedModSelector.cs b/src/ModVerify.CliApp/ModSelectors/SettingsBasedModSelector.cs deleted file mode 100644 index 221bb9e..0000000 --- a/src/ModVerify.CliApp/ModSelectors/SettingsBasedModSelector.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Linq; -using AET.ModVerify.App.GameFinder; -using AET.ModVerify.App.Settings; -using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Infrastructure; - -namespace AET.ModVerify.App.ModSelectors; - -internal class SettingsBasedModSelector(IServiceProvider serviceProvider) -{ - public VerifyInstallationData CreateInstallationDataFromSettings(GameInstallationsSettings settings) - { - var gameLocations = new ModSelectorFactory(serviceProvider) - .CreateSelector(settings) - .Select(settings, out var targetObject, out var engineType); - - if (gameLocations is null) - throw new GameNotFoundException("Unable to get game locations"); - - if (engineType is null) - throw new InvalidOperationException("Engine type not specified."); - - return new VerifyInstallationData - { - EngineType = engineType.Value, - GameLocations = gameLocations, - Name = GetNameFromGameLocations(targetObject, gameLocations, engineType.Value) - }; - } - - private static string GetNameFromGameLocations(IPlayableObject? targetObject, GameLocations gameLocations, GameEngineType engineType) - { - if (targetObject is not null) - return targetObject.Name; - - var mod = gameLocations.ModPaths.FirstOrDefault(); - return mod ?? gameLocations.GamePath; - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectors/VerifyInstallationData.cs b/src/ModVerify.CliApp/ModSelectors/VerifyInstallationData.cs deleted file mode 100644 index 1a1fcd2..0000000 --- a/src/ModVerify.CliApp/ModSelectors/VerifyInstallationData.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Text; -using PG.StarWarsGame.Engine; - -namespace AET.ModVerify.App.ModSelectors; - -internal sealed class VerifyInstallationData -{ - public required string Name { get; init; } - - public required GameEngineType EngineType { get; init; } - - public required GameLocations GameLocations { get; init; } - - public override string ToString() - { - var sb = new StringBuilder(); - - sb.AppendLine($"ObjectToVerify={Name};EngineType={EngineType};Locations=["); - if (GameLocations.ModPaths.Count > 0) - sb.AppendLine($"Mods=[{string.Join(";", GameLocations.ModPaths)}];"); - sb.AppendLine($"Game=[{GameLocations.GamePath}];"); - if (GameLocations.FallbackPaths.Count > 0) - sb.AppendLine($"Fallbacks=[{string.Join(";", GameLocations.FallbackPaths)}];"); - sb.AppendLine("]"); - - return sb.ToString(); - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModVerify.CliApp.csproj b/src/ModVerify.CliApp/ModVerify.CliApp.csproj index 0073b2b..ce20eed 100644 --- a/src/ModVerify.CliApp/ModVerify.CliApp.csproj +++ b/src/ModVerify.CliApp/ModVerify.CliApp.csproj @@ -21,6 +21,10 @@ en + + true + + @@ -30,18 +34,18 @@ - - + + - + - - - - - + + + + + @@ -51,10 +55,6 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - all @@ -64,9 +64,18 @@ - - true - + + + + compile + runtime; build; native; contentfiles; analyzers; buildtransitive + + + compile + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/src/ModVerify.CliApp/ModVerify.CliApp.csproj.DotSettings b/src/ModVerify.CliApp/ModVerify.CliApp.csproj.DotSettings new file mode 100644 index 0000000..0bcf4ed --- /dev/null +++ b/src/ModVerify.CliApp/ModVerify.CliApp.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModVerifyAppEnvironment.cs b/src/ModVerify.CliApp/ModVerifyAppEnvironment.cs index 86bfc40..192d97b 100644 --- a/src/ModVerify.CliApp/ModVerifyAppEnvironment.cs +++ b/src/ModVerify.CliApp/ModVerifyAppEnvironment.cs @@ -3,6 +3,8 @@ 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; @@ -26,15 +28,31 @@ internal sealed class ModVerifyAppEnvironment(Assembly assembly, IFileSystem fil public override ICollection UpdateMirrors { get; } = new List { #if DEBUG - new("C:\\Test\\ModVerify"), + new(CreateDebugPath()), #endif new($"https://republicatwar.com/downloads/{ModVerifyConstants.ModVerifyToolPath}") }; + private static string CreateDebugPath() + { + var dir = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "../../../../..")); + return Path.Combine(dir, ".local_deploy/server"); + } + public override string UpdateRegistryPath => $@"SOFTWARE\{ModVerifyConstants.ModVerifyToolPath}\Update"; - - protected override UpdateConfiguration CreateUpdateConfiguration() + +#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"), diff --git a/src/ModVerify.CliApp/ModVerifyApplication.cs b/src/ModVerify.CliApp/ModVerifyApplication.cs deleted file mode 100644 index 7ea461c..0000000 --- a/src/ModVerify.CliApp/ModVerifyApplication.cs +++ /dev/null @@ -1,251 +0,0 @@ -using AET.ModVerify.App.ModSelectors; -using AET.ModVerify.App.Reporting; -using AET.ModVerify.App.Settings; -using AET.ModVerify.Pipeline; -using AET.ModVerify.Reporting; -using AET.ModVerify.Reporting.Settings; -using AnakinRaW.ApplicationBase; -using AnakinRaW.ApplicationBase.Utilities; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using PG.StarWarsGame.Engine; -using Serilog; -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using AET.ModVerify.App.GameFinder; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace AET.ModVerify.App; - -internal sealed class ModVerifyApplication(ModVerifyAppSettings settings, IServiceProvider services) -{ - private readonly ILogger? _logger = services.GetService()?.CreateLogger(typeof(ModVerifyApplication)); - private readonly IFileSystem _fileSystem = services.GetRequiredService(); - private readonly ModVerifyAppEnvironment _appEnvironment = services.GetRequiredService(); - - public async Task Run() - { - using (new UnhandledExceptionHandler(services)) - using (new UnobservedTaskExceptionHandler(services)) - return await RunCore().ConfigureAwait(false); - } - - private async Task RunCore() - { - _logger?.LogDebug("Raw command line: {CommandLine}", Environment.CommandLine); - - var interactive = settings.Interactive; - try - { - return await RunVerify().ConfigureAwait(false); - } - catch (Exception e) - { - _logger?.LogCritical(e, e.Message); - ConsoleUtilities.WriteApplicationFatalError(ModVerifyConstants.AppNameString, e); - return e.HResult; - } - finally - { -#if NET - await Log.CloseAndFlushAsync(); -#else - Log.CloseAndFlush(); -#endif - if (interactive) - { - Console.WriteLine(); - ConsoleUtilities.WriteHorizontalLine('-'); - Console.WriteLine("Press any key to exit"); - Console.ReadLine(); - } - } - } - - - private async Task RunVerify() - { - VerifyInstallationData installData; - try - { - installData = new SettingsBasedModSelector(services) - .CreateInstallationDataFromSettings(settings.GameInstallationsSettings); - } - catch (GameNotFoundException ex) - { - ConsoleUtilities.WriteApplicationFatalError(_appEnvironment.ApplicationName, - "Unable to find an installation of Empire at War or Forces of Corruption."); - _logger?.LogError(ex, "Game not found: {Message}", ex.Message); - return ex.HResult; - } - - var reportSettings = CreateGlobalReportSettings(installData); - - _logger?.LogDebug("Verify install data: {InstallData}", installData); - _logger?.LogTrace("Verify settings: {Settings}", settings); - - var allErrors = await Verify(installData, reportSettings) - .ConfigureAwait(false); - - try - { - await ReportErrors(allErrors).ConfigureAwait(false); - } - catch (GameVerificationException e) - { - return e.HResult; - } - - if (!settings.CreateNewBaseline) - return 0; - - await WriteBaseline(reportSettings, allErrors, settings.NewBaselinePath).ConfigureAwait(false); - _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Baseline successfully created."); - - return 0; - } - - private async Task> Verify( - VerifyInstallationData installData, - GlobalVerifyReportSettings reportSettings) - { - var gameEngineService = services.GetRequiredService(); - var engineErrorReporter = new ConcurrentGameEngineErrorReporter(); - - IStarWarsGameEngine gameEngine; - - try - { - var initProgress = new Progress(); - var initProgressReporter = new EngineInitializeProgressReporter(initProgress); - - try - { - _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Creating Game Engine '{Engine}'", installData.EngineType); - gameEngine = await gameEngineService.InitializeAsync( - installData.EngineType, - installData.GameLocations, - engineErrorReporter, - initProgress, - false, - CancellationToken.None).ConfigureAwait(false); - _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Game Engine created"); - } - finally - { - initProgressReporter.Dispose(); - } - } - catch (Exception e) - { - _logger?.LogError(e, "Creating game engine failed: {Message}", e.Message); - throw; - } - - var progressReporter = new VerifyConsoleProgressReporter(installData.Name); - - using var verifyPipeline = new GameVerifyPipeline( - gameEngine, - engineErrorReporter, - settings.VerifyPipelineSettings, - reportSettings, - progressReporter, - services); - - try - { - try - { - _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Verifying '{Target}'...", installData.Name); - await verifyPipeline.RunAsync().ConfigureAwait(false); - progressReporter.Report(string.Empty, 1.0); - } - catch - { - progressReporter.ReportError("Verification failed", null); - throw; - } - finally - { - progressReporter.Dispose(); - } - } - catch (OperationCanceledException) - { - _logger?.LogWarning(ModVerifyConstants.ConsoleEventId, "Verification stopped due to enabled failFast setting."); - } - catch (Exception e) - { - _logger?.LogError(e, "Verification failed: {Message}", e.Message); - throw; - } - - _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Finished verification"); - return verifyPipeline.FilteredErrors; - } - - private async Task ReportErrors(IReadOnlyCollection errors) - { - _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Reporting Errors..."); - - var reportBroker = new VerificationReportBroker(services); - - await reportBroker.ReportAsync(errors); - - if (errors.Any(x => x.Severity >= settings.AppThrowsOnMinimumSeverity)) - throw new GameVerificationException(errors); - } - - private async Task WriteBaseline( - GlobalVerifyReportSettings reportSettings, - IEnumerable errors, - string baselineFile) - { - var baseline = new VerificationBaseline(reportSettings.MinimumReportSeverity, errors); - - var fullPath = _fileSystem.Path.GetFullPath(baselineFile); - _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Writing Baseline to '{FullPath}'", fullPath); - -#if NET - await -#endif - using var fs = _fileSystem.FileStream.New(fullPath, FileMode.Create, FileAccess.Write, FileShare.None); - await baseline.ToJsonAsync(fs); - } - - private GlobalVerifyReportSettings CreateGlobalReportSettings(VerifyInstallationData installData) - { - var baselineSelector = new BaselineSelector(settings, services); - var baseline = baselineSelector.SelectBaseline(installData, out var baselinePath); - - if (baseline.Count > 0) - _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Using baseline '{Baseline}'", baselinePath); - - var suppressionsFile = settings.ReportSettings.SuppressionsPath; - SuppressionList suppressions; - - if (string.IsNullOrEmpty(suppressionsFile)) - suppressions = SuppressionList.Empty; - else - { - using var fs = _fileSystem.File.OpenRead(suppressionsFile); - suppressions = SuppressionList.FromJson(fs); - - if (suppressions.Count > 0) - _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Using suppressions from '{Suppressions}'", suppressionsFile); - } - - - return new GlobalVerifyReportSettings - { - Baseline = baseline, - Suppressions = suppressions, - MinimumReportSeverity = settings.ReportSettings.MinimumReportSeverity, - }; - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModVerifyConstants.cs b/src/ModVerify.CliApp/ModVerifyConstants.cs index 6b60f06..041a4a5 100644 --- a/src/ModVerify.CliApp/ModVerifyConstants.cs +++ b/src/ModVerify.CliApp/ModVerifyConstants.cs @@ -9,5 +9,9 @@ internal static class ModVerifyConstants public const string ModVerifyToolPath = "ModVerify"; public const int ConsoleEventIdValue = 1138; + public const int Success = 0; + public const int CompletedWithFindings = 1; + public const int ErrorBadArguments = 0xA0; + public static readonly EventId ConsoleEventId = new(ConsoleEventIdValue, "LogToConsole"); } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Program.cs b/src/ModVerify.CliApp/Program.cs index cd4747b..2d23b3b 100644 --- a/src/ModVerify.CliApp/Program.cs +++ b/src/ModVerify.CliApp/Program.cs @@ -40,6 +40,7 @@ using System.IO.Abstractions; using System.Runtime.InteropServices; using System.Threading.Tasks; +using AET.ModVerify.App.Reporting; using Testably.Abstractions; using ILogger = Serilog.ILogger; @@ -61,21 +62,22 @@ internal class Program : SelfUpdateableAppLifecycle private static readonly string ModVerifyRootNameSpace = typeof(Program).Namespace!; private static readonly CompiledExpression PrintToConsoleExpression = SerilogExpression.Compile($"EventId.Id = {ModVerifyConstants.ConsoleEventIdValue}"); - private static ModVerifyOptionsContainer _optionsContainer = null!; - - protected override async Task InitializeAppAsync(IReadOnlyList args) + private AppSettingsBase? _modVerifyAppSettings; + private ApplicationUpdateOptions _updateOptions = new(); + private bool _offlineMode; + private bool _verboseMode; + private bool _isLaunchedWithoutArguments; + + protected override async Task InitializeAppAsync(IReadOnlyList args, IServiceProvider bootstrapServices) { + await base.InitializeAppAsync(args, bootstrapServices); + ModVerifyConsoleUtilities.WriteHeader(ApplicationEnvironment.AssemblyInfo.InformationalVersion); - await base.InitializeAppAsync(args); - + ModVerifyOptionsContainer parsedOptions; try { - var settings = new ModVerifyOptionsParser(ApplicationEnvironment, BootstrapLoggerFactory).Parse(args); - if (!settings.HasOptions) - return 0xA0; - _optionsContainer = settings; - return 0; + parsedOptions = new ModVerifyOptionsParser(ApplicationEnvironment, BootstrapLoggerFactory).Parse(args); } catch (Exception e) { @@ -83,6 +85,41 @@ protected override async Task InitializeAppAsync(IReadOnlyList args ConsoleUtilities.WriteApplicationFatalError(ModVerifyConstants.AppNameString, e); return e.HResult; } + + if (!parsedOptions.HasOptions) + return ModVerifyConstants.ErrorBadArguments; + + if (parsedOptions.UpdateOptions is not null) + _updateOptions = parsedOptions.UpdateOptions; + + if (parsedOptions.ModVerifyOptions?.Verbose is true || parsedOptions.UpdateOptions?.Verbose is true) + _verboseMode = true; + + if (parsedOptions.ModVerifyOptions is null) + return ModVerifyConstants.Success; + + _offlineMode = parsedOptions.ModVerifyOptions.OfflineMode; + _isLaunchedWithoutArguments = parsedOptions.ModVerifyOptions.LaunchedWithoutArguments(); + + try + { + _modVerifyAppSettings = new SettingsBuilder(bootstrapServices) + .BuildSettings(parsedOptions.ModVerifyOptions); + } + catch (AppArgumentException e) + { + Logger?.LogCritical(e, "Invalid arguments specified by the user: {Message}", e.Message); + ConsoleUtilities.WriteApplicationFatalError(ModVerifyConstants.AppNameString, e.Message); + return e.HResult; + } + catch (Exception e) + { + Logger?.LogCritical(e, "Failed to create settings form commandline arguments: {Message}", e.Message); + ConsoleUtilities.WriteApplicationFatalError(ModVerifyConstants.AppNameString, e); + return e.HResult; + } + + return ModVerifyConstants.Success; } protected override void CreateAppServices(IServiceCollection services, IReadOnlyList args) @@ -106,8 +143,8 @@ protected override void CreateAppServices(IServiceCollection services, IReadOnly sp => new CosturaApplicationProductService(ApplicationEnvironment, sp), sp => new JsonManifestLoader(sp)); } - - if (_optionsContainer.ModVerifyOptions is null) + + if (_modVerifyAppSettings is null) return; SteamAbstractionLayer.InitializeServices(services); @@ -122,10 +159,11 @@ protected override void CreateAppServices(IServiceCollection services, IReadOnly PetroglyphEngineServiceContribution.ContributeServices(services); services.RegisterVerifierCache(); - + services.AddSingleton(sp => new BaselineFactory(sp)); + SetupVerifyReporting(services); - if (_optionsContainer.ModVerifyOptions.OfflineMode) + if (_offlineMode) { services.AddSingleton(sp => new OfflineModNameResolver(sp)); services.AddSingleton(sp => new OfflineModGameTypeResolver(sp)); @@ -157,59 +195,39 @@ protected override IRegistry CreateRegistry() protected override async Task RunAppAsync(string[] args, IServiceProvider appServiceProvider) { var result = await HandleUpdate(appServiceProvider); - if (result != 0 || _optionsContainer.ModVerifyOptions is null) + if (result != 0 || _modVerifyAppSettings is null) return result; - - ModVerifyAppSettings modVerifySettings; - - try - { - modVerifySettings = new SettingsBuilder(appServiceProvider).BuildSettings(_optionsContainer.ModVerifyOptions); - } - catch (Exception e) - { - Logger?.LogCritical(e, "Failed to create settings form commandline arguments: {EMessage}", e.Message); - ConsoleUtilities.WriteApplicationFatalError(ModVerifyConstants.AppNameString, e); - return e.HResult; - } - - return await new ModVerifyApplication(modVerifySettings, appServiceProvider).Run().ConfigureAwait(false); + return await new ModVerifyApplication(_modVerifyAppSettings, appServiceProvider).RunAsync().ConfigureAwait(false); } private void SetupVerifyReporting(IServiceCollection serviceCollection) { - var options = _optionsContainer.ModVerifyOptions; - Debug.Assert(options is not null); - + Debug.Assert(_modVerifyAppSettings is not null); - var verifyVerb = options as VerifyVerbOption; + var verifySettings = _modVerifyAppSettings as AppVerifySettings; - // Console should be in minimal summary mode if we are not in verify mode. - var printOnlySummary = verifyVerb is null; - - serviceCollection.RegisterConsoleReporter(new VerifyReportSettings + // Console should be in minimal summary mode if we are in a different mode than verify. + serviceCollection.RegisterConsoleReporter(new ReporterSettings { - MinimumReportSeverity = VerificationSeverity.Error - }, printOnlySummary); + MinimumReportSeverity = verifySettings?.VerifyPipelineSettings.FailFastSettings.IsFailFast is true + ? VerificationSeverity.Information + : VerificationSeverity.Error + }, summaryOnly: verifySettings is null); - if (verifyVerb == null) + if (verifySettings == null) return; - var outputDirectory = Environment.CurrentDirectory; - - if (!string.IsNullOrEmpty(verifyVerb.OutputDirectory)) - outputDirectory = FileSystem.Path.GetFullPath(FileSystem.Path.Combine(Environment.CurrentDirectory, verifyVerb.OutputDirectory!)); - + var outputDirectory = verifySettings.ReportDirectory; serviceCollection.RegisterJsonReporter(new JsonReporterSettings { OutputDirectory = outputDirectory!, - MinimumReportSeverity = options.MinimumSeverity + MinimumReportSeverity = _modVerifyAppSettings.ReportSettings.MinimumReportSeverity }); serviceCollection.RegisterTextFileReporter(new TextFileReporterSettings { OutputDirectory = outputDirectory!, - MinimumReportSeverity = options.MinimumSeverity + MinimumReportSeverity = _modVerifyAppSettings.ReportSettings.MinimumReportSeverity }); } @@ -224,7 +242,7 @@ private void ConfigureLogging(ILoggingBuilder loggingBuilder) loggingBuilder.AddDebug(); #endif - if (_optionsContainer.ModVerifyOptions?.Verbose == true || _optionsContainer.UpdateOptions?.Verbose == true) + if (_verboseMode) { logLevel = LogEventLevel.Verbose; loggingBuilder.AddDebug(); @@ -297,31 +315,30 @@ static bool IsXmlParserLogging(LogEvent logEvent) private async Task HandleUpdate(IServiceProvider serviceProvider) { - var updateOptions = _optionsContainer.UpdateOptions ?? new ApplicationUpdateOptions(); + if (_offlineMode) + { + Logger?.LogTrace("Running in offline mode. There will be nothing to update."); + return ModVerifyConstants.Success; + } + ModVerifyUpdateMode updateMode; - if (_optionsContainer.ModVerifyOptions is not null) + if (_isLaunchedWithoutArguments) + updateMode = ModVerifyUpdateMode.InteractiveUpdate; + else { - if (_optionsContainer.ModVerifyOptions.OfflineMode) - { - Logger?.LogTrace("Running in offline mode. There will be nothing to update."); - return 0; - } - - updateMode = _optionsContainer.ModVerifyOptions.LaunchedWithoutArguments() - ? ModVerifyUpdateMode.InteractiveUpdate - : ModVerifyUpdateMode.CheckOnly; + updateMode = _modVerifyAppSettings is not null + ? ModVerifyUpdateMode.CheckOnly + : ModVerifyUpdateMode.AutoUpdate; } - else - updateMode = ModVerifyUpdateMode.AutoUpdate; - + try { Logger?.LogDebug("Running update with mode '{ModVerifyUpdateMode}'", updateMode); var modVerifyUpdater = new ModVerifyUpdater(serviceProvider); - await modVerifyUpdater.RunUpdateProcedure(updateOptions, updateMode).ConfigureAwait(false); + await modVerifyUpdater.RunUpdateProcedure(_updateOptions, updateMode).ConfigureAwait(false); Logger?.LogDebug("Update procedure completed successfully."); - return 0; + return ModVerifyConstants.Success; } catch (Exception e) { diff --git a/src/ModVerify.CliApp/Properties/launchSettings.json b/src/ModVerify.CliApp/Properties/launchSettings.json index e47583a..299ce46 100644 --- a/src/ModVerify.CliApp/Properties/launchSettings.json +++ b/src/ModVerify.CliApp/Properties/launchSettings.json @@ -1,21 +1,20 @@ { "profiles": { - "Run": { + "Verify": { "commandName": "Project", "commandLineArgs": "" }, - "Interactive Verify": { + "Verify (Interactive)": { "commandName": "Project", - "commandLineArgs": "verify -o verifyResults --minFailSeverity Information --offline" + "commandLineArgs": "verify -o verifyResults --offline --minFailSeverity Information" }, - "Interactive Baseline": { + "Verify (Automatic Target Selection)": { "commandName": "Project", - "commandLineArgs": "createBaseline -o focBaseline.json --offline" + "commandLineArgs": "verify -o verifyResults --path \"C:\\Program Files (x86)\\Steam\\steamapps\\common\\Star Wars Empire at War\\corruption\"" }, - - "FromModPath": { + "Create Baseline Interactive": { "commandName": "Project", - "commandLineArgs": "-o verifyResults --baseline focBaseline.json --path C:/test --type Foc" + "commandLineArgs": "createBaseline -o baseline.json --offline --skipLocation" } } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Reporting/BaselineFactory.cs b/src/ModVerify.CliApp/Reporting/BaselineFactory.cs index 9fcc911..a83cea9 100644 --- a/src/ModVerify.CliApp/Reporting/BaselineFactory.cs +++ b/src/ModVerify.CliApp/Reporting/BaselineFactory.cs @@ -1,24 +1,31 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.IO.Abstractions; +using AET.ModVerify.App.Settings; using AET.ModVerify.Reporting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using System.Threading.Tasks; +using PG.StarWarsGame.Engine; +using AET.ModVerify.App.Utilities; namespace AET.ModVerify.App.Reporting; -internal sealed class BaselineFactory(IServiceProvider serviceProvider) +internal sealed class BaselineFactory(IServiceProvider serviceProvider) : IBaselineFactory { private readonly ILogger? _logger = serviceProvider.GetService()?.CreateLogger(typeof(BaselineFactory)); private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); - public bool TryCreateBaseline( + public bool TryFindBaselineInDirectory( string directory, - out VerificationBaseline baseline, + Predicate baselineSelector, + [NotNullWhen(true)] out VerificationBaseline? baseline, [NotNullWhen(true)] out string? path) { - baseline = VerificationBaseline.Empty; + baseline = null; path = null; if (!_fileSystem.Directory.Exists(directory)) @@ -42,9 +49,17 @@ public bool TryCreateBaseline( { try { - baseline = CreateBaselineFromFilePath(jsonFile); - path = jsonFile; - _logger?.LogDebug("Create baseline from file: {JsonFile}", jsonFile); + var parsedBaseline = CreateBaselineFromFilePath(jsonFile); + if (!baselineSelector(parsedBaseline)) + { + _logger?.LogDebug("Baseline '{JsonFile}' was denied by selector.", jsonFile); + continue; + } + + baseline = parsedBaseline; + path = _fileSystem.Path.GetFullPath(jsonFile); + + _logger?.LogDebug("Create baseline from file '{JsonFile}'", jsonFile); return true; } catch (InvalidBaselineException e) @@ -54,15 +69,50 @@ public bool TryCreateBaseline( } } + baseline = null; path = null; return false; } - public VerificationBaseline CreateBaseline(string filePath) + public VerificationBaseline ParseBaseline(string filePath) { return CreateBaselineFromFilePath(filePath); } + public VerificationBaseline CreateBaseline( + VerificationTarget target, + AppBaselineSettings settings, + IEnumerable errors) + { + var baselineTarget = new BaselineVerificationTarget + { + Engine = target.Engine, + Name = target.Name, + Version = target.Version, + Location = settings.WriteLocations ? MaskUsername(target.Location) : null, + IsGame = target.IsGame, + }; + + return new VerificationBaseline(settings.ReportSettings.MinimumReportSeverity, errors, baselineTarget); + } + + private static GameLocations MaskUsername(GameLocations targetLocation) + { + return new GameLocations( + targetLocation.ModPaths.Select(PathUtilities.MaskUsername).ToList(), + PathUtilities.MaskUsername(targetLocation.GamePath), + targetLocation.FallbackPaths.Select(PathUtilities.MaskUsername).ToList()); + } + + public async Task WriteBaselineAsync(VerificationBaseline baseline, string filePath) + { +#if NET + await +#endif + using var fs = _fileSystem.FileStream.New(filePath, FileMode.Create, FileAccess.Write, FileShare.None); + await baseline.ToJsonAsync(fs); + } + private VerificationBaseline CreateBaselineFromFilePath(string baselineFile) { using var fs = _fileSystem.FileStream.New(baselineFile, FileMode.Open, FileAccess.Read); diff --git a/src/ModVerify.CliApp/Reporting/BaselineSelector.cs b/src/ModVerify.CliApp/Reporting/BaselineSelector.cs index 95953f1..791eaae 100644 --- a/src/ModVerify.CliApp/Reporting/BaselineSelector.cs +++ b/src/ModVerify.CliApp/Reporting/BaselineSelector.cs @@ -1,5 +1,4 @@ -using AET.ModVerify.App.ModSelectors; -using AET.ModVerify.App.Resources.Baselines; +using AET.ModVerify.App.Resources.Baselines; using AET.ModVerify.App.Settings; using AET.ModVerify.Reporting; using AnakinRaW.ApplicationBase; @@ -8,15 +7,17 @@ using PG.StarWarsGame.Engine; using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text; namespace AET.ModVerify.App.Reporting; -internal sealed class BaselineSelector(ModVerifyAppSettings settings, IServiceProvider services) +internal sealed class BaselineSelector(AppVerifySettings settings, IServiceProvider services) { private readonly ILogger? _logger = services.GetService()?.CreateLogger(typeof(ModVerifyApplication)); - private readonly BaselineFactory _baselineFactory = new(services); + private readonly IBaselineFactory _baselineFactory = services.GetRequiredService(); - public VerificationBaseline SelectBaseline(VerifyInstallationData installationData, out string? usedBaselinePath) + public VerificationBaseline SelectBaseline(VerificationTarget verificationTarget, out string? usedBaselinePath) { var baselinePath = settings.ReportSettings.BaselinePath; if (!string.IsNullOrEmpty(baselinePath)) @@ -24,7 +25,7 @@ public VerificationBaseline SelectBaseline(VerifyInstallationData installationDa try { usedBaselinePath = baselinePath; - return _baselineFactory.CreateBaseline(baselinePath!); + return _baselineFactory.ParseBaseline(baselinePath); } catch (InvalidBaselineException e) { @@ -43,20 +44,21 @@ public VerificationBaseline SelectBaseline(VerifyInstallationData installationDa if (!settings.ReportSettings.SearchBaselineLocally) { - _logger?.LogDebug(ModVerifyConstants.ConsoleEventId, "No baseline path specified and local search is not enabled. Using empty baseline."); + _logger?.LogDebug(ModVerifyConstants.ConsoleEventId, + "No baseline path specified and local search is not enabled. Using empty baseline."); usedBaselinePath = null; return VerificationBaseline.Empty; } - if (settings.Interactive) - return FindBaselineInteractive(installationData, out usedBaselinePath); + if (settings.IsInteractive) + return FindBaselineInteractive(verificationTarget, out usedBaselinePath); // If the application is not interactive, we only use a baseline file present in the directory of the verification target. - return FindBaselineNonInteractive(installationData.GameLocations.TargetPath, out usedBaselinePath); + return FindBaselineNonInteractive(verificationTarget, out usedBaselinePath); } - private VerificationBaseline FindBaselineInteractive(VerifyInstallationData installationData, out string? baselinePath) + private VerificationBaseline FindBaselineInteractive(VerificationTarget verificationTarget, out string? baselinePath) { // The application is in interactive mode. We apply the following lookup: // 1. Use a baseline found in the directory of the verification target. @@ -66,48 +68,52 @@ private VerificationBaseline FindBaselineInteractive(VerifyInstallationData inst _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Searching for local baseline files..."); - if (!_baselineFactory.TryCreateBaseline(installationData.GameLocations.TargetPath, out var baseline, + if (!_baselineFactory.TryFindBaselineInDirectory( + verificationTarget.Location.TargetPath, + b => IsBaselineCompatible(b, verificationTarget), + out var baseline, out baselinePath)) { - if (!_baselineFactory.TryCreateBaseline("./", out baseline, out baselinePath)) + if (!_baselineFactory.TryFindBaselineInDirectory( + Environment.CurrentDirectory, + b => IsBaselineCompatible(b, verificationTarget), + out baseline, + out baselinePath)) { - // It does not make sense to load the game's default baselines if the user wants to verify the game, - // as the verification result would always be empty (at least in a non-development scenario) - if (installationData.GameLocations.ModPaths.Count == 0) - { - _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "No local baseline file found."); - return VerificationBaseline.Empty; - } - + Console.ForegroundColor = ConsoleColor.Cyan; Console.WriteLine("No baseline found locally."); - return TryGetDefaultBaseline(installationData.EngineType, out baselinePath); + Console.ResetColor(); + baselinePath = null; + TryGetDefaultBaseline(verificationTarget.Engine, out baseline); + return baseline ?? VerificationBaseline.Empty; } } - Debug.Assert(baselinePath is not null); + Debug.Assert(baselinePath is not null && baseline is not null); - return ConsoleUtilities.UserYesNoQuestion($"ModVerify found the baseline file '{baselinePath}'. Do you want to use it?") - ? baseline + return ShouldUseBaseline(baseline, baselinePath) + ? baseline : VerificationBaseline.Empty; } - private VerificationBaseline TryGetDefaultBaseline(GameEngineType engineType, out string? baselinePath) + private static bool TryGetDefaultBaseline( + GameEngineType engineType, + [NotNullWhen(true)] out VerificationBaseline? baseline) { - baselinePath = null; + baseline = null; if (engineType == GameEngineType.Eaw) { // TODO: EAW currently not implemented - return VerificationBaseline.Empty; + return false; } if (!ConsoleUtilities.UserYesNoQuestion($"Do you want to load the default baseline for game engine '{engineType}'?")) - return VerificationBaseline.Empty; - - baselinePath = $"{engineType} (Default)"; + return false; try { - return LoadEmbeddedBaseline(engineType); + baseline = LoadEmbeddedBaseline(engineType); + return true; } catch (InvalidBaselineException) { @@ -116,7 +122,7 @@ private VerificationBaseline TryGetDefaultBaseline(GameEngineType engineType, ou } } - internal VerificationBaseline LoadEmbeddedBaseline(GameEngineType engineType) + internal static VerificationBaseline LoadEmbeddedBaseline(GameEngineType engineType) { var baselineFileName = $"baseline-{engineType.ToString().ToLower()}.json"; var resourcePath = $"{typeof(BaselineResources).Namespace}.{baselineFileName}"; @@ -125,15 +131,39 @@ internal VerificationBaseline LoadEmbeddedBaseline(GameEngineType engineType) return VerificationBaseline.FromJson(baselineStream); } - private VerificationBaseline FindBaselineNonInteractive(string targetPath, out string? usedPath) + private VerificationBaseline FindBaselineNonInteractive(VerificationTarget target, out string? usedPath) { - if (_baselineFactory.TryCreateBaseline(targetPath, out var baseline, out usedPath)) + if (_baselineFactory.TryFindBaselineInDirectory( + target.Location.TargetPath, + b => IsBaselineCompatible(b, target), + out var baseline, + out usedPath)) { _logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Automatically applying local baseline file '{Path}'.", usedPath); return baseline; } - _logger?.LogTrace("No baseline file found in taget path '{TargetPath}'.", targetPath); + _logger?.LogTrace("No baseline file found in taget path '{TargetPath}'.", target.Location.TargetPath); usedPath = null; return VerificationBaseline.Empty; } + + + private static bool IsBaselineCompatible(VerificationBaseline baseline, VerificationTarget target) + { + return baseline.Target?.Engine == target.Engine; + } + + private static bool ShouldUseBaseline(VerificationBaseline baseline, string baselinePath) + { + var sb = new StringBuilder("Found baseline "); + if (baseline.Target is not null) + sb.Append($"for '{baseline.Target.Name}' "); + + sb.Append($"at '{baselinePath}'."); + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine(sb.ToString()); + + return ConsoleUtilities.UserYesNoQuestion("Do you want to use it?"); + } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Reporting/EngineInitializeProgressReporter.cs b/src/ModVerify.CliApp/Reporting/EngineInitializeProgressReporter.cs index b994e97..d93462f 100644 --- a/src/ModVerify.CliApp/Reporting/EngineInitializeProgressReporter.cs +++ b/src/ModVerify.CliApp/Reporting/EngineInitializeProgressReporter.cs @@ -1,30 +1,25 @@ using System; +using PG.StarWarsGame.Engine; namespace AET.ModVerify.App.Reporting; -internal sealed class EngineInitializeProgressReporter : IDisposable -{ - private Progress? _progress; - - public EngineInitializeProgressReporter(Progress? progress) +internal sealed class EngineInitializeProgressReporter(GameEngineType engine) : IGameEngineInitializationReporter +{ + public void ReportProgress(string message) { - if (progress is null) - return; - progress.ProgressChanged += OnProgress; + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine(message); + Console.ResetColor(); } - private void OnProgress(object sender, string e) + public void ReportStarted() { - Console.ForegroundColor = ConsoleColor.DarkGray; - Console.WriteLine(e); - Console.ResetColor(); + Console.WriteLine($"Initializing game engine '{engine}'..."); } - public void Dispose() + public void ReportFinished() { + Console.WriteLine($"Game engine initialized."); Console.WriteLine(); - if (_progress is not null) - _progress.ProgressChanged -= OnProgress; - _progress = null; } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Reporting/IBaselineFactory.cs b/src/ModVerify.CliApp/Reporting/IBaselineFactory.cs new file mode 100644 index 0000000..721a7ce --- /dev/null +++ b/src/ModVerify.CliApp/Reporting/IBaselineFactory.cs @@ -0,0 +1,26 @@ +using AET.ModVerify.App.Settings; +using AET.ModVerify.Reporting; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +namespace AET.ModVerify.App.Reporting; + +internal interface IBaselineFactory +{ + bool TryFindBaselineInDirectory( + string directory, + Predicate baselineSelector, + [NotNullWhen(true)] out VerificationBaseline? baseline, + [NotNullWhen(true)] out string? path); + + VerificationBaseline ParseBaseline(string filePath); + + Task WriteBaselineAsync(VerificationBaseline baseline, string filePath); + + VerificationBaseline CreateBaseline( + VerificationTarget target, + AppBaselineSettings settings, + IEnumerable errors); +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs b/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs index 4700457..b2ce170 100644 --- a/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs +++ b/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs @@ -1,5 +1,6 @@ using System; using System.Threading; +using AET.ModVerify.App.Settings; using AET.ModVerify.Pipeline.Progress; using AnakinRaW.CommonUtilities; using AnakinRaW.CommonUtilities.SimplePipeline.Progress; @@ -7,7 +8,8 @@ namespace AET.ModVerify.App.Reporting; -public sealed class VerifyConsoleProgressReporter(string toVerifyName) : DisposableObject, IVerifyProgressReporter +public sealed class VerifyConsoleProgressReporter(string toVerifyName, AppReportSettings reportSettings) + : DisposableObject, IVerifyProgressReporter { private static readonly ProgressBarOptions ProgressBarOptions = new() { @@ -17,6 +19,7 @@ public sealed class VerifyConsoleProgressReporter(string toVerifyName) : Disposa WriteQueuedMessage = WriteQueuedMessage, }; + private readonly bool _verbose = reportSettings.Verbose; private ProgressBar? _progressBar; public void ReportError(string message, string? errorLine) @@ -38,8 +41,8 @@ public void Report(double progress, string? progressText, ProgressType type, Ver var progressBar = EnsureProgressBar(); - // TODO: Only recognize detailed mode - progressBar.Message = progressText; + if (detailedProgress.IsDetailed) + progressBar.Message = progressText; if (progress >= 1.0) progressBar.Message = $"Verified '{toVerifyName}'"; @@ -47,8 +50,8 @@ public void Report(double progress, string? progressText, ProgressType type, Ver var cpb = progressBar.AsProgress(); cpb.Report(progress); - // TODO: Only in verbose mode - //progressBar.WriteLine(progressText); + if (_verbose) + progressBar.WriteLine(progressText); } protected override void DisposeResources() diff --git a/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json b/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json index c94d121..ce70f8a 100644 --- a/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json +++ b/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json @@ -1,5 +1,11 @@ { - "version": "2.0", + "version": "2.1", + "target": { + "name": "Forces of Corruption (SteamGold)", + "engine": "Foc", + "isGame": true, + "version": "1.121.13.7360" + }, "minSeverity": "Information", "errors": [ { @@ -36,12 +42,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_SKIPRAY.ALO\u0027.", + "message": "Unable to find .ALO file \u0027CIN_Reb_CelebHall.alo\u0027", "severity": "Error", - "context": [ - "DATA\\ART\\MODELS\\UV_SKIPRAY.ALO" - ], - "asset": "Default.fx" + "context": [], + "asset": "CIN_Reb_CelebHall.alo" }, { "id": "FILE00", @@ -49,36 +53,26 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_PRISON.ALO\u0027", + "message": "Proxy particle \u0027p_ssd_debris\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_ECLIPSE_UC_DC.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\NB_PRISON.ALO" - ], - "asset": "p_smoke_small_thin2" - }, - { - "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + "DATA\\ART\\MODELS\\UV_ECLIPSE_UC_DC.ALO" ], - "message": "Unable to find .ALO file \u0027W_Kamino_Reflect.ALO\u0027", - "severity": "Error", - "context": [], - "asset": "W_Kamino_Reflect.ALO" + "asset": "p_ssd_debris" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" ], - "message": "Proxy particle \u0027p_ssd_debris\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_ECLIPSE_UC_DC.ALO\u0027", + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall.tga\u0027 for context: [W_SITH_LEFTHALL.ALO].", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\UV_ECLIPSE_UC_DC.ALO" + "W_SITH_LEFTHALL.ALO" ], - "asset": "p_ssd_debris" + "asset": "Cin_Reb_CelebHall_Wall.tga" }, { "id": "FILE00", @@ -86,10 +80,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_p_proton_torpedo.alo\u0027", + "message": "Unable to find .ALO file \u0027Cin_ImperialCraft.alo\u0027", "severity": "Error", "context": [], - "asset": "CIN_p_proton_torpedo.alo" + "asset": "Cin_ImperialCraft.alo" }, { "id": "FILE00", @@ -97,10 +91,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_DStar_LeverPanel.alo\u0027", + "message": "Unable to find .ALO file \u0027Cin_Officer.alo\u0027", "severity": "Error", "context": [], - "asset": "Cin_DStar_LeverPanel.alo" + "asset": "Cin_Officer.alo" }, { "id": "FILE00", @@ -108,12 +102,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027lookat\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_ECLIPSE.ALO\u0027", + "message": "Unable to find .ALO file \u0027Cin_DStar_protons.alo\u0027", "severity": "Error", - "context": [ - "DATA\\ART\\MODELS\\UV_ECLIPSE.ALO" - ], - "asset": "lookat" + "context": [], + "asset": "Cin_DStar_protons.alo" }, { "id": "FILE00", @@ -122,12 +114,12 @@ "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", "AET.ModVerify.Verifiers.Commons.TextureVeifier" ], - "message": "Could not find texture \u0027Cin_DeathStar.tga\u0027 for context: [ALTTEST.ALO].", + "message": "Could not find texture \u0027w_grenade.tga\u0027 for context: [W_GRENADE.ALO].", "severity": "Error", "context": [ - "ALTTEST.ALO" + "W_GRENADE.ALO" ], - "asset": "Cin_DeathStar.tga" + "asset": "w_grenade.tga" }, { "id": "FILE00", @@ -135,12 +127,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_prison_light\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_PRISON.ALO\u0027", + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_03_STATION_D.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\NB_PRISON.ALO" + "DATA\\ART\\MODELS\\UB_03_STATION_D.ALO" ], - "asset": "p_prison_light" + "asset": "p_uwstation_death" }, { "id": "FILE00", @@ -148,26 +140,21 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\EV_MDU_SENSORNODE.ALO\u0027.", + "message": "Unable to find .ALO file \u0027Cin_EI_Vader.alo\u0027", "severity": "Error", - "context": [ - "DATA\\ART\\MODELS\\EV_MDU_SENSORNODE.ALO" - ], - "asset": "Default.fx" + "context": [], + "asset": "Cin_EI_Vader.alo" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", - "AET.ModVerify.Verifiers.Commons.TextureVeifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Could not find texture \u0027w_grenade.tga\u0027 for context: [W_GRENADE.ALO].", + "message": "Unable to find .ALO file \u0027MODELS\u0027", "severity": "Error", - "context": [ - "W_GRENADE.ALO" - ], - "asset": "w_grenade.tga" + "context": [], + "asset": "MODELS" }, { "id": "FILE00", @@ -175,10 +162,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_Rbel_NavyRow.alo\u0027", + "message": "Unable to find .ALO file \u0027Cin_DeathStar_Wall.alo\u0027", "severity": "Error", "context": [], - "asset": "CIN_Rbel_NavyRow.alo" + "asset": "Cin_DeathStar_Wall.alo" }, { "id": "FILE00", @@ -186,10 +173,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_Planet_Alderaan_High.alo\u0027", + "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_PRISON.ALO\u0027", "severity": "Error", - "context": [], - "asset": "Cin_Planet_Alderaan_High.alo" + "context": [ + "DATA\\ART\\MODELS\\NB_PRISON.ALO" + ], + "asset": "p_smoke_small_thin2" }, { "id": "FILE00", @@ -197,12 +186,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027lookat\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_ECLIPSE_UC.ALO\u0027", + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_01_STATION_D.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\UV_ECLIPSE_UC.ALO" + "DATA\\ART\\MODELS\\UB_01_STATION_D.ALO" ], - "asset": "lookat" + "asset": "p_uwstation_death" }, { "id": "FILE00", @@ -210,12 +199,23 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\EI_MARAJADE.ALO\u0027", + "message": "Unable to find .ALO file \u0027CIN_Officer_Row.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Officer_Row.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_MEDIUM.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\EI_MARAJADE.ALO" + "DATA\\ART\\MODELS\\W_STARS_MEDIUM.ALO" ], - "asset": "p_desert_ground_dust" + "asset": "Lensflare0" }, { "id": "FILE00", @@ -223,10 +223,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027p_splash_wake_lava.alo\u0027", + "message": "Unable to find .ALO file \u0027CIN_DeathStar_Hangar.alo\u0027", "severity": "Error", "context": [], - "asset": "p_splash_wake_lava.alo" + "asset": "CIN_DeathStar_Hangar.alo" }, { "id": "FILE00", @@ -234,10 +234,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_rv_XWingProp.alo\u0027", + "message": "Unable to find .ALO file \u0027Cin_EV_lambdaShuttle_150.alo\u0027", "severity": "Error", "context": [], - "asset": "Cin_rv_XWingProp.alo" + "asset": "Cin_EV_lambdaShuttle_150.alo" }, { "id": "FILE00", @@ -245,10 +245,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_Fire_Huge.alo\u0027", + "message": "Proxy particle \u0027p_smoke_small_thin4\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_PRISON.ALO\u0027", "severity": "Error", - "context": [], - "asset": "CIN_Fire_Huge.alo" + "context": [ + "DATA\\ART\\MODELS\\NB_PRISON.ALO" + ], + "asset": "p_smoke_small_thin4" }, { "id": "FILE00", @@ -256,10 +258,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_Probe_Droid.alo\u0027", + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_CINE.ALO\u0027", "severity": "Error", - "context": [], - "asset": "CIN_Probe_Droid.alo" + "context": [ + "DATA\\ART\\MODELS\\W_STARS_CINE.ALO" + ], + "asset": "Lensflare0" }, { "id": "FILE00", @@ -267,12 +271,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_05_STATION_D.ALO\u0027", + "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_SKIPRAY.ALO\u0027.", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\UB_05_STATION_D.ALO" + "DATA\\ART\\MODELS\\UV_SKIPRAY.ALO" ], - "asset": "p_uwstation_death" + "asset": "Default.fx" }, { "id": "FILE00", @@ -280,10 +284,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\RI_KYLEKATARN.ALO\u0027", + "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\UI_IG88.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\RI_KYLEKATARN.ALO" + "DATA\\ART\\MODELS\\UI_IG88.ALO" ], "asset": "p_desert_ground_dust" }, @@ -293,26 +297,34 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_steam_small\u0027 not found for model \u0027DATA\\ART\\MODELS\\RB_HEAVYVEHICLEFACTORY.ALO\u0027", + "message": "Unable to find .ALO file \u0027CIN_p_proton_torpedo.alo\u0027", "severity": "Error", - "context": [ - "DATA\\ART\\MODELS\\RB_HEAVYVEHICLEFACTORY.ALO" + "context": [], + "asset": "CIN_p_proton_torpedo.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "asset": "p_steam_small" + "message": "Unable to find .ALO file \u0027CIN_Fire_Huge.alo\u0027", + "severity": "Error", + "context": [], + "asset": "CIN_Fire_Huge.alo" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", - "AET.ModVerify.Verifiers.Commons.TextureVeifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Could not find texture \u0027p_particle_master\u0027 for context: [P_DIRT_EMITTER_TEST1.ALO].", + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_04_STATION_D.ALO\u0027", "severity": "Error", "context": [ - "P_DIRT_EMITTER_TEST1.ALO" + "DATA\\ART\\MODELS\\UB_04_STATION_D.ALO" ], - "asset": "p_particle_master" + "asset": "p_uwstation_death" }, { "id": "FILE00", @@ -320,12 +332,23 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_smoke_small_thin4\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_PRISON.ALO\u0027", + "message": "Unable to find .ALO file \u0027RV_nebulonb_D_death_00.ALO\u0027", + "severity": "Error", + "context": [], + "asset": "RV_nebulonb_D_death_00.ALO" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_02_STATION_D.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\NB_PRISON.ALO" + "DATA\\ART\\MODELS\\UB_02_STATION_D.ALO" ], - "asset": "p_smoke_small_thin4" + "asset": "p_uwstation_death" }, { "id": "FILE00", @@ -333,10 +356,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_EI_Vader.alo\u0027", + "message": "Unable to find .ALO file \u0027W_Kamino_Reflect.ALO\u0027", "severity": "Error", "context": [], - "asset": "Cin_EI_Vader.alo" + "asset": "W_Kamino_Reflect.ALO" }, { "id": "FILE00", @@ -344,10 +367,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027DATA\\ART\\MODELS\\RB_HYPERVELOCITYGUN.ALO\u0027", + "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_MONCAL_BUILDING.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\RB_HYPERVELOCITYGUN.ALO" + "DATA\\ART\\MODELS\\NB_MONCAL_BUILDING.ALO" ], "asset": "p_smoke_small_thin2" }, @@ -357,12 +380,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\EV_TIE_LANCET.ALO\u0027.", + "message": "Proxy particle \u0027p_steam_small\u0027 not found for model \u0027DATA\\ART\\MODELS\\RB_HEAVYVEHICLEFACTORY.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\EV_TIE_LANCET.ALO" + "DATA\\ART\\MODELS\\RB_HEAVYVEHICLEFACTORY.ALO" ], - "asset": "Default.fx" + "asset": "p_steam_small" }, { "id": "FILE00", @@ -370,10 +393,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_DeathStar_Wall.alo\u0027", + "message": "Unable to find .ALO file \u0027Cin_EV_Stardestroyer_Warp.alo\u0027", "severity": "Error", "context": [], - "asset": "Cin_DeathStar_Wall.alo" + "asset": "Cin_EV_Stardestroyer_Warp.alo" }, { "id": "FILE00", @@ -381,10 +404,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027W_droid_steam.alo\u0027", + "message": "Unable to find .ALO file \u0027Cin_DStar_TurretLasers.alo\u0027", "severity": "Error", "context": [], - "asset": "W_droid_steam.alo" + "asset": "Cin_DStar_TurretLasers.alo" }, { "id": "FILE00", @@ -392,10 +415,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_DeathStar_High.alo\u0027", + "message": "Unable to find .ALO file \u0027CIN_Rbel_GreyGroup.alo\u0027", "severity": "Error", "context": [], - "asset": "Cin_DeathStar_High.alo" + "asset": "CIN_Rbel_GreyGroup.alo" }, { "id": "FILE00", @@ -403,10 +426,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027MODELS\u0027", + "message": "Unable to find .ALO file \u0027Cin_Planet_Hoth_High.alo\u0027", "severity": "Error", "context": [], - "asset": "MODELS" + "asset": "Cin_Planet_Hoth_High.alo" }, { "id": "FILE00", @@ -414,10 +437,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027W_AllShaders.ALO\u0027", + "message": "Unable to find .ALO file \u0027CIN_Trooper_Row.alo\u0027", "severity": "Error", "context": [], - "asset": "W_AllShaders.ALO" + "asset": "CIN_Trooper_Row.alo" }, { "id": "FILE00", @@ -425,12 +448,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_bomb_spin\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_THERMAL_DETONATOR_EMPIRE.ALO\u0027", + "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\UI_SABOTEUR.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\W_THERMAL_DETONATOR_EMPIRE.ALO" + "DATA\\ART\\MODELS\\UI_SABOTEUR.ALO" ], - "asset": "p_bomb_spin" + "asset": "p_desert_ground_dust" }, { "id": "FILE00", @@ -438,12 +461,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_03_STATION_D.ALO\u0027", + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_CINE_LUA.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\UB_03_STATION_D.ALO" + "DATA\\ART\\MODELS\\W_STARS_CINE_LUA.ALO" ], - "asset": "p_uwstation_death" + "asset": "Lensflare0" }, { "id": "FILE00", @@ -451,10 +474,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_Rbel_GreyGroup.alo\u0027", + "message": "Unable to find .ALO file \u0027W_AllShaders.ALO\u0027", "severity": "Error", "context": [], - "asset": "CIN_Rbel_GreyGroup.alo" + "asset": "W_AllShaders.ALO" }, { "id": "FILE00", @@ -462,12 +485,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_cold_tiny01\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_SCH.ALO\u0027", + "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\EV_TIE_LANCET.ALO\u0027.", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\NB_SCH.ALO" + "DATA\\ART\\MODELS\\EV_TIE_LANCET.ALO" ], - "asset": "p_cold_tiny01" + "asset": "Default.fx" }, { "id": "FILE00", @@ -486,34 +509,38 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_hp_archammer-damage\u0027 not found for model \u0027DATA\\ART\\MODELS\\EV_ARCHAMMER.ALO\u0027", + "message": "Unable to find .ALO file \u0027Cin_EI_Palpatine.alo\u0027", "severity": "Error", - "context": [ - "DATA\\ART\\MODELS\\EV_ARCHAMMER.ALO" - ], - "asset": "p_hp_archammer-damage" - }, + "context": [], + "asset": "Cin_EI_Palpatine.alo" + }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" ], - "message": "Unable to find .ALO file \u0027CIN_Rbel_grey.alo\u0027", + "message": "Could not find texture \u0027Cin_DeathStar.tga\u0027 for context: [ALTTEST.ALO].", "severity": "Error", - "context": [], - "asset": "CIN_Rbel_grey.alo" + "context": [ + "ALTTEST.ALO" + ], + "asset": "Cin_DeathStar.tga" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" ], - "message": "Unable to find .ALO file \u0027CIN_Reb_CelebHall.alo\u0027", + "message": "Could not find texture \u0027UB_girder_B.tga\u0027 for context: [UV_MDU_CAGE.ALO].", "severity": "Error", - "context": [], - "asset": "CIN_Reb_CelebHall.alo" + "context": [ + "UV_MDU_CAGE.ALO" + ], + "asset": "UB_girder_B.tga" }, { "id": "FILE00", @@ -534,10 +561,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_ImperialCraft.alo\u0027", + "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\EV_MDU_SENSORNODE.ALO\u0027.", "severity": "Error", - "context": [], - "asset": "Cin_ImperialCraft.alo" + "context": [ + "DATA\\ART\\MODELS\\EV_MDU_SENSORNODE.ALO" + ], + "asset": "Default.fx" }, { "id": "FILE00", @@ -545,10 +574,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_DStar_Dish_close.alo\u0027", + "message": "Unable to find .ALO file \u0027Cin_Planet_Alderaan_High.alo\u0027", "severity": "Error", "context": [], - "asset": "Cin_DStar_Dish_close.alo" + "asset": "Cin_Planet_Alderaan_High.alo" }, { "id": "FILE00", @@ -556,12 +585,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027pe_bwing_yellow\u0027 not found for model \u0027DATA\\ART\\MODELS\\RV_BWING.ALO\u0027", + "message": "Proxy particle \u0027p_hp_archammer-damage\u0027 not found for model \u0027DATA\\ART\\MODELS\\EV_ARCHAMMER.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\RV_BWING.ALO" + "DATA\\ART\\MODELS\\EV_ARCHAMMER.ALO" ], - "asset": "pe_bwing_yellow" + "asset": "p_hp_archammer-damage" }, { "id": "FILE00", @@ -569,10 +598,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_bridge.alo\u0027", + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_05_STATION_D.ALO\u0027", "severity": "Error", - "context": [], - "asset": "Cin_bridge.alo" + "context": [ + "DATA\\ART\\MODELS\\UB_05_STATION_D.ALO" + ], + "asset": "p_uwstation_death" }, { "id": "FILE00", @@ -580,12 +611,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\UI_SABOTEUR.ALO\u0027", + "message": "Proxy particle \u0027p_explosion_smoke_small_thin5\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_NOGHRI_HUT.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\UI_SABOTEUR.ALO" + "DATA\\ART\\MODELS\\NB_NOGHRI_HUT.ALO" ], - "asset": "p_desert_ground_dust" + "asset": "p_explosion_smoke_small_thin5" }, { "id": "FILE00", @@ -593,10 +624,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_Trooper_Row.alo\u0027", + "message": "Unable to find .ALO file \u0027CIN_Probe_Droid.alo\u0027", "severity": "Error", "context": [], - "asset": "CIN_Trooper_Row.alo" + "asset": "CIN_Probe_Droid.alo" }, { "id": "FILE00", @@ -604,10 +635,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_EV_TieAdvanced.alo\u0027", + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_HIGH.ALO\u0027", "severity": "Error", - "context": [], - "asset": "Cin_EV_TieAdvanced.alo" + "context": [ + "DATA\\ART\\MODELS\\W_STARS_HIGH.ALO" + ], + "asset": "Lensflare0" }, { "id": "FILE00", @@ -615,38 +648,58 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027w_sith_arch.alo\u0027", + "message": "Unable to find .ALO file \u0027W_Volcano_Rock02.ALO\u0027", "severity": "Error", "context": [], - "asset": "w_sith_arch.alo" + "asset": "W_Volcano_Rock02.ALO" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", - "AET.ModVerify.Verifiers.Commons.TextureVeifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Could not find texture \u0027NB_YsalamiriTree_B.tga\u0027 for context: [UV_MDU_CAGE.ALO].", + "message": "Proxy particle \u0027lookat\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_ECLIPSE.ALO\u0027", "severity": "Error", "context": [ - "UV_MDU_CAGE.ALO" + "DATA\\ART\\MODELS\\UV_ECLIPSE.ALO" ], - "asset": "NB_YsalamiriTree_B.tga" + "asset": "lookat" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", - "AET.ModVerify.Verifiers.Commons.TextureVeifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Could not find texture \u0027W_TE_Rock_f_02_b.tga\u0027 for context: [EV_TIE_PHANTOM.ALO].", + "message": "Unable to find .ALO file \u0027Cin_Shuttle_Tyderium.alo\u0027", + "severity": "Error", + "context": [], + "asset": "Cin_Shuttle_Tyderium.alo" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Unable to find .ALO file \u0027W_SwampGasEmit.ALO\u0027", + "severity": "Error", + "context": [], + "asset": "W_SwampGasEmit.ALO" + }, + { + "id": "FILE00", + "verifiers": [ + "AET.ModVerify.Verifiers.ReferencedModelsVerifier", + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + ], + "message": "Proxy particle \u0027P_heat_small01\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_VCH.ALO\u0027", "severity": "Error", "context": [ - "EV_TIE_PHANTOM.ALO" + "DATA\\ART\\MODELS\\NB_VCH.ALO" ], - "asset": "W_TE_Rock_f_02_b.tga" + "asset": "P_heat_small01" }, { "id": "FILE00", @@ -654,10 +707,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_EI_Palpatine.alo\u0027", + "message": "Unable to find .ALO file \u0027CIN_Rbel_NavyRow.alo\u0027", "severity": "Error", "context": [], - "asset": "Cin_EI_Palpatine.alo" + "asset": "CIN_Rbel_NavyRow.alo" }, { "id": "FILE00", @@ -665,10 +718,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_Rbel_Soldier.alo\u0027", + "message": "Unable to find .ALO file \u0027CIN_Fire_Medium.alo\u0027", "severity": "Error", "context": [], - "asset": "CIN_Rbel_Soldier.alo" + "asset": "CIN_Fire_Medium.alo" }, { "id": "FILE00", @@ -676,12 +729,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_MONCAL_BUILDING.ALO\u0027", + "message": "Proxy particle \u0027p_ewok_drag_dirt\u0027 not found for model \u0027DATA\\ART\\MODELS\\UI_EWOK_HANDLER.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\NB_MONCAL_BUILDING.ALO" + "DATA\\ART\\MODELS\\UI_EWOK_HANDLER.ALO" ], - "asset": "p_smoke_small_thin2" + "asset": "p_ewok_drag_dirt" }, { "id": "FILE00", @@ -700,10 +753,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_Officer_Row.alo\u0027", + "message": "Unable to find .ALO file \u0027W_droid_steam.alo\u0027", "severity": "Error", "context": [], - "asset": "CIN_Officer_Row.alo" + "asset": "W_droid_steam.alo" }, { "id": "FILE00", @@ -711,26 +764,21 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_HIGH.ALO\u0027", + "message": "Unable to find .ALO file \u0027CIN_Biker_Row.alo\u0027", "severity": "Error", - "context": [ - "DATA\\ART\\MODELS\\W_STARS_HIGH.ALO" - ], - "asset": "Lensflare0" + "context": [], + "asset": "CIN_Biker_Row.alo" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", - "AET.ModVerify.Verifiers.Commons.TextureVeifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall_B.tga\u0027 for context: [W_SITH_LEFTHALL.ALO].", + "message": "Unable to find .ALO file \u0027w_planet_volcanic.alo\u0027", "severity": "Error", - "context": [ - "W_SITH_LEFTHALL.ALO" - ], - "asset": "Cin_Reb_CelebHall_Wall_B.tga" + "context": [], + "asset": "w_planet_volcanic.alo" }, { "id": "FILE00", @@ -738,10 +786,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_Coruscant.alo\u0027", + "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027DATA\\ART\\MODELS\\RB_HYPERVELOCITYGUN.ALO\u0027", "severity": "Error", - "context": [], - "asset": "Cin_Coruscant.alo" + "context": [ + "DATA\\ART\\MODELS\\RB_HYPERVELOCITYGUN.ALO" + ], + "asset": "p_smoke_small_thin2" }, { "id": "FILE00", @@ -749,12 +799,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_ewok_drag_dirt\u0027 not found for model \u0027DATA\\ART\\MODELS\\UI_EWOK_HANDLER.ALO\u0027", + "message": "Proxy particle \u0027lookat\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_ECLIPSE_UC.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\UI_EWOK_HANDLER.ALO" + "DATA\\ART\\MODELS\\UV_ECLIPSE_UC.ALO" ], - "asset": "p_ewok_drag_dirt" + "asset": "lookat" }, { "id": "FILE00", @@ -762,12 +812,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027P_heat_small01\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_VCH.ALO\u0027", + "message": "Unable to find .ALO file \u0027CIN_REb_CelebCharacters.alo\u0027", "severity": "Error", - "context": [ - "DATA\\ART\\MODELS\\NB_VCH.ALO" - ], - "asset": "P_heat_small01" + "context": [], + "asset": "CIN_REb_CelebCharacters.alo" }, { "id": "FILE00", @@ -775,36 +823,38 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027W_Vol_Steam01.ALO\u0027", + "message": "Unable to find .ALO file \u0027Cin_DeathStar_High.alo\u0027", "severity": "Error", "context": [], - "asset": "W_Vol_Steam01.ALO" + "asset": "Cin_DeathStar_High.alo" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" ], - "message": "Proxy particle \u0027p_explosion_smoke_small_thin5\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_NOGHRI_HUT.ALO\u0027", + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall_B.tga\u0027 for context: [W_SITH_LEFTHALL.ALO].", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\NB_NOGHRI_HUT.ALO" + "W_SITH_LEFTHALL.ALO" ], - "asset": "p_explosion_smoke_small_thin5" + "asset": "Cin_Reb_CelebHall_Wall_B.tga" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" ], - "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_MEDIUM.ALO\u0027", + "message": "Could not find texture \u0027NB_YsalamiriTree_B.tga\u0027 for context: [UV_MDU_CAGE.ALO].", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\W_STARS_MEDIUM.ALO" + "UV_MDU_CAGE.ALO" ], - "asset": "Lensflare0" + "asset": "NB_YsalamiriTree_B.tga" }, { "id": "FILE00", @@ -812,12 +862,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_02_STATION_D.ALO\u0027", + "message": "Unable to find .ALO file \u0027Cin_Coruscant.alo\u0027", "severity": "Error", - "context": [ - "DATA\\ART\\MODELS\\UB_02_STATION_D.ALO" - ], - "asset": "p_uwstation_death" + "context": [], + "asset": "Cin_Coruscant.alo" }, { "id": "FILE00", @@ -825,10 +873,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_Officer.alo\u0027", + "message": "Proxy particle \u0027p_prison_light\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_PRISON.ALO\u0027", "severity": "Error", - "context": [], - "asset": "Cin_Officer.alo" + "context": [ + "DATA\\ART\\MODELS\\NB_PRISON.ALO" + ], + "asset": "p_prison_light" }, { "id": "FILE00", @@ -836,12 +886,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_04_STATION_D.ALO\u0027", + "message": "Proxy particle \u0027p_cold_tiny01\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_SCH.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\UB_04_STATION_D.ALO" + "DATA\\ART\\MODELS\\NB_SCH.ALO" ], - "asset": "p_uwstation_death" + "asset": "p_cold_tiny01" }, { "id": "FILE00", @@ -849,10 +899,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_Lambda_Head.alo\u0027", + "message": "Unable to find .ALO file \u0027CIN_NavyTrooper_Row.alo\u0027", "severity": "Error", "context": [], - "asset": "CIN_Lambda_Head.alo" + "asset": "CIN_NavyTrooper_Row.alo" }, { "id": "FILE00", @@ -860,10 +910,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_Biker_Row.alo\u0027", + "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_CRUSADERCLASSCORVETTE.ALO\u0027.", "severity": "Error", - "context": [], - "asset": "CIN_Biker_Row.alo" + "context": [ + "DATA\\ART\\MODELS\\UV_CRUSADERCLASSCORVETTE.ALO" + ], + "asset": "Default.fx" }, { "id": "FILE00", @@ -871,10 +923,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_DStar_protons.alo\u0027", + "message": "Unable to find .ALO file \u0027CIN_Rbel_Soldier.alo\u0027", "severity": "Error", "context": [], - "asset": "Cin_DStar_protons.alo" + "asset": "CIN_Rbel_Soldier.alo" }, { "id": "FILE00", @@ -882,10 +934,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\UI_IG88.ALO\u0027", + "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\RI_KYLEKATARN.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\UI_IG88.ALO" + "DATA\\ART\\MODELS\\RI_KYLEKATARN.ALO" ], "asset": "p_desert_ground_dust" }, @@ -904,12 +956,15 @@ "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" ], - "message": "Unable to find .ALO file \u0027Cin_Planet_Hoth_High.alo\u0027", + "message": "Could not find texture \u0027p_particle_master\u0027 for context: [P_DIRT_EMITTER_TEST1.ALO].", "severity": "Error", - "context": [], - "asset": "Cin_Planet_Hoth_High.alo" + "context": [ + "P_DIRT_EMITTER_TEST1.ALO" + ], + "asset": "p_particle_master" }, { "id": "FILE00", @@ -917,12 +972,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_LOW.ALO\u0027", + "message": "Unable to find .ALO file \u0027Cin_bridge.alo\u0027", "severity": "Error", - "context": [ - "DATA\\ART\\MODELS\\W_STARS_LOW.ALO" - ], - "asset": "Lensflare0" + "context": [], + "asset": "Cin_bridge.alo" }, { "id": "FILE00", @@ -930,10 +983,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_NavyTrooper_Row.alo\u0027", + "message": "Unable to find .ALO file \u0027W_Vol_Steam01.ALO\u0027", "severity": "Error", "context": [], - "asset": "CIN_NavyTrooper_Row.alo" + "asset": "W_Vol_Steam01.ALO" }, { "id": "FILE00", @@ -941,10 +994,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_DStar_TurretLasers.alo\u0027", + "message": "Unable to find .ALO file \u0027CIN_Rbel_grey.alo\u0027", "severity": "Error", "context": [], - "asset": "Cin_DStar_TurretLasers.alo" + "asset": "CIN_Rbel_grey.alo" }, { "id": "FILE00", @@ -952,10 +1005,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027W_SwampGasEmit.ALO\u0027", + "message": "Unable to find .ALO file \u0027w_sith_arch.alo\u0027", "severity": "Error", "context": [], - "asset": "W_SwampGasEmit.ALO" + "asset": "w_sith_arch.alo" }, { "id": "FILE00", @@ -963,10 +1016,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_Shuttle_Tyderium.alo\u0027", + "message": "Unable to find .ALO file \u0027Cin_rv_XWingProp.alo\u0027", "severity": "Error", "context": [], - "asset": "Cin_Shuttle_Tyderium.alo" + "asset": "Cin_rv_XWingProp.alo" }, { "id": "FILE00", @@ -974,23 +1027,24 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_EV_Stardestroyer_Warp.alo\u0027", + "message": "Unable to find .ALO file \u0027Cin_DStar_Dish_close.alo\u0027", "severity": "Error", "context": [], - "asset": "Cin_EV_Stardestroyer_Warp.alo" + "asset": "Cin_DStar_Dish_close.alo" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", + "AET.ModVerify.Verifiers.Commons.TextureVeifier" ], - "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_CINE_LUA.ALO\u0027", + "message": "Could not find texture \u0027W_TE_Rock_f_02_b.tga\u0027 for context: [EV_TIE_PHANTOM.ALO].", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\W_STARS_CINE_LUA.ALO" + "EV_TIE_PHANTOM.ALO" ], - "asset": "Lensflare0" + "asset": "W_TE_Rock_f_02_b.tga" }, { "id": "FILE00", @@ -998,10 +1052,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_DeathStar_Hangar.alo\u0027", + "message": "Proxy particle \u0027pe_bwing_yellow\u0027 not found for model \u0027DATA\\ART\\MODELS\\RV_BWING.ALO\u0027", "severity": "Error", - "context": [], - "asset": "CIN_DeathStar_Hangar.alo" + "context": [ + "DATA\\ART\\MODELS\\RV_BWING.ALO" + ], + "asset": "pe_bwing_yellow" }, { "id": "FILE00", @@ -1009,10 +1065,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_Fire_Medium.alo\u0027", + "message": "Unable to find .ALO file \u0027CIN_Lambda_Head.alo\u0027", "severity": "Error", "context": [], - "asset": "CIN_Fire_Medium.alo" + "asset": "CIN_Lambda_Head.alo" }, { "id": "FILE00", @@ -1020,12 +1076,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_CINE.ALO\u0027", + "message": "Proxy particle \u0027p_explosion_small_delay00\u0027 not found for model \u0027DATA\\ART\\MODELS\\EB_COMMANDCENTER.ALO\u0027", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\W_STARS_CINE.ALO" + "DATA\\ART\\MODELS\\EB_COMMANDCENTER.ALO" ], - "asset": "Lensflare0" + "asset": "p_explosion_small_delay00" }, { "id": "FILE00", @@ -1033,10 +1089,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_Rbel_Soldier_Group.alo\u0027", + "message": "Unable to find .ALO file \u0027Cin_DStar_LeverPanel.alo\u0027", "severity": "Error", "context": [], - "asset": "CIN_Rbel_Soldier_Group.alo" + "asset": "Cin_DStar_LeverPanel.alo" }, { "id": "FILE00", @@ -1044,10 +1100,10 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027RV_nebulonb_D_death_00.ALO\u0027", + "message": "Unable to find .ALO file \u0027p_splash_wake_lava.alo\u0027", "severity": "Error", "context": [], - "asset": "RV_nebulonb_D_death_00.ALO" + "asset": "p_splash_wake_lava.alo" }, { "id": "FILE00", @@ -1055,10 +1111,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027W_Volcano_Rock02.ALO\u0027", + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_LOW.ALO\u0027", "severity": "Error", - "context": [], - "asset": "W_Volcano_Rock02.ALO" + "context": [ + "DATA\\ART\\MODELS\\W_STARS_LOW.ALO" + ], + "asset": "Lensflare0" }, { "id": "FILE00", @@ -1066,10 +1124,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027w_planet_volcanic.alo\u0027", + "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\EI_MARAJADE.ALO\u0027", "severity": "Error", - "context": [], - "asset": "w_planet_volcanic.alo" + "context": [ + "DATA\\ART\\MODELS\\EI_MARAJADE.ALO" + ], + "asset": "p_desert_ground_dust" }, { "id": "FILE00", @@ -1077,10 +1137,12 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Unable to find .ALO file \u0027CIN_REb_CelebCharacters.alo\u0027", + "message": "Proxy particle \u0027p_bomb_spin\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_THERMAL_DETONATOR_EMPIRE.ALO\u0027", "severity": "Error", - "context": [], - "asset": "CIN_REb_CelebCharacters.alo" + "context": [ + "DATA\\ART\\MODELS\\W_THERMAL_DETONATOR_EMPIRE.ALO" + ], + "asset": "p_bomb_spin" }, { "id": "FILE00", @@ -1088,257 +1150,249 @@ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_CRUSADERCLASSCORVETTE.ALO\u0027.", + "message": "Unable to find .ALO file \u0027Cin_EV_TieAdvanced.alo\u0027", "severity": "Error", - "context": [ - "DATA\\ART\\MODELS\\UV_CRUSADERCLASSCORVETTE.ALO" - ], - "asset": "Default.fx" + "context": [], + "asset": "Cin_EV_TieAdvanced.alo" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", - "AET.ModVerify.Verifiers.Commons.TextureVeifier" + "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" ], - "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall.tga\u0027 for context: [W_SITH_LEFTHALL.ALO].", + "message": "Unable to find .ALO file \u0027CIN_Rbel_Soldier_Group.alo\u0027", "severity": "Error", - "context": [ - "W_SITH_LEFTHALL.ALO" - ], - "asset": "Cin_Reb_CelebHall_Wall.tga" + "context": [], + "asset": "CIN_Rbel_Soldier_Group.alo" }, { "id": "FILE00", "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Unable to find .ALO file \u0027Cin_EV_lambdaShuttle_150.alo\u0027", + "message": "Audio file \u0027U000_LEI0213_ENG.WAV\u0027 could not be found.", "severity": "Error", - "context": [], - "asset": "Cin_EV_lambdaShuttle_150.alo" + "context": [ + "Unit_Move_Leia" + ], + "asset": "U000_LEI0213_ENG.WAV" }, { "id": "FILE00", "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Proxy particle \u0027p_explosion_small_delay00\u0027 not found for model \u0027DATA\\ART\\MODELS\\EB_COMMANDCENTER.ALO\u0027", + "message": "Audio file \u0027U000_LEI0113_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\EB_COMMANDCENTER.ALO" + "Unit_Select_Leia" ], - "asset": "p_explosion_small_delay00" + "asset": "U000_LEI0113_ENG.WAV" }, { "id": "FILE00", "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", - "AET.ModVerify.Verifiers.Commons.TextureVeifier" + "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Could not find texture \u0027UB_girder_B.tga\u0027 for context: [UV_MDU_CAGE.ALO].", + "message": "Audio file \u0027U000_LEI0603_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "UV_MDU_CAGE.ALO" + "Unit_Increase_Production_Leia" ], - "asset": "UB_girder_B.tga" + "asset": "U000_LEI0603_ENG.WAV" }, { "id": "FILE00", "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" + "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_01_STATION_D.ALO\u0027", + "message": "Audio file \u0027U000_LEI0309_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "DATA\\ART\\MODELS\\UB_01_STATION_D.ALO" + "Unit_Attack_Leia" ], - "asset": "p_uwstation_death" + "asset": "U000_LEI0309_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0206_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0212_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ "Unit_Move_Leia" ], - "asset": "U000_LEI0206_ENG.WAV" + "asset": "U000_LEI0212_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0204_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_MAL0503_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Move_Leia" + "Unit_Assist_Move_Missile_Launcher" ], - "asset": "U000_LEI0204_ENG.WAV" + "asset": "U000_MAL0503_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0102_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_MCF1601_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Select_Leia" + "Unit_StarDest_MC30_Frigate" ], - "asset": "U000_LEI0102_ENG.WAV" + "asset": "U000_MCF1601_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0215_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0111_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Move_Leia" + "Unit_Select_Leia" ], - "asset": "U000_LEI0215_ENG.WAV" + "asset": "U000_LEI0111_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0107_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_ARC3106_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Select_Leia" + "Unit_Complete_Troops_Arc_Hammer" ], - "asset": "U000_LEI0107_ENG.WAV" + "asset": "U000_ARC3106_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0504_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0303_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Remove_Corruption_Leia" + "Unit_Attack_Leia" ], - "asset": "U000_LEI0504_ENG.WAV" + "asset": "U000_LEI0303_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027AMB_DES_CLEAR_LOOP_1.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0404_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Weather_Ambient_Clear_Sandstorm_Loop" + "Unit_Guard_Leia" ], - "asset": "AMB_DES_CLEAR_LOOP_1.WAV" + "asset": "U000_LEI0404_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0105_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027TESTUNITMOVE_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Select_Leia" + "Unit_Move_Gneneric_Test" ], - "asset": "U000_LEI0105_ENG.WAV" + "asset": "TESTUNITMOVE_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0213_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0401_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Move_Leia" + "Unit_Guard_Leia" ], - "asset": "U000_LEI0213_ENG.WAV" + "asset": "U000_LEI0401_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0201_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027EGL_STAR_VIPER_SPINNING_1.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Move_Leia" + "Unit_Star_Viper_Spinning_By" ], - "asset": "U000_LEI0201_ENG.WAV" + "asset": "EGL_STAR_VIPER_SPINNING_1.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0303_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_TMC0212_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Attack_Leia" + "Unit_Move_Tie_Mauler" ], - "asset": "U000_LEI0303_ENG.WAV" + "asset": "U000_TMC0212_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0103_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0110_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ "Unit_Select_Leia" ], - "asset": "U000_LEI0103_ENG.WAV" + "asset": "U000_LEI0110_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0207_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0314_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Move_Leia" + "Unit_Attack_Leia" ], - "asset": "U000_LEI0207_ENG.WAV" + "asset": "U000_LEI0314_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_DEF3006_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0305_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Corrupt_Sabateur" + "Unit_Attack_Leia" ], - "asset": "U000_DEF3006_ENG.WAV" + "asset": "U000_LEI0305_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0309_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0112_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Attack_Leia" + "Unit_Select_Leia" ], - "asset": "U000_LEI0309_ENG.WAV" + "asset": "U000_LEI0112_ENG.WAV" }, { "id": "FILE00", @@ -1357,120 +1411,120 @@ "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_DEF3106_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0211_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Weaken_Sabateur" + "Unit_Move_Leia" ], - "asset": "U000_DEF3106_ENG.WAV" + "asset": "U000_LEI0211_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0503_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0205_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Remove_Corruption_Leia" + "Unit_Move_Leia" ], - "asset": "U000_LEI0503_ENG.WAV" + "asset": "U000_LEI0205_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0502_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0115_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Remove_Corruption_Leia" + "Unit_Select_Leia" ], - "asset": "U000_LEI0502_ENG.WAV" + "asset": "U000_LEI0115_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0212_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0604_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Move_Leia" + "Unit_Increase_Production_Leia" ], - "asset": "U000_LEI0212_ENG.WAV" + "asset": "U000_LEI0604_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027AMB_URB_CLEAR_LOOP_1.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0602_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Weather_Ambient_Clear_Urban_Loop" + "Unit_Increase_Production_Leia" ], - "asset": "AMB_URB_CLEAR_LOOP_1.WAV" + "asset": "U000_LEI0602_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0311_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0315_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ "Unit_Attack_Leia" ], - "asset": "U000_LEI0311_ENG.WAV" + "asset": "U000_LEI0315_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0115_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_DEF3006_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Select_Leia" + "Unit_Corrupt_Sabateur" ], - "asset": "U000_LEI0115_ENG.WAV" + "asset": "U000_DEF3006_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0101_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0210_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Select_Leia" + "Unit_Move_Leia" ], - "asset": "U000_LEI0101_ENG.WAV" + "asset": "U000_LEI0210_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0401_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0105_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Guard_Leia" + "Unit_Select_Leia" ], - "asset": "U000_LEI0401_ENG.WAV" + "asset": "U000_LEI0105_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0315_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0208_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Attack_Leia" + "Unit_Move_Leia" ], - "asset": "U000_LEI0315_ENG.WAV" + "asset": "U000_LEI0208_ENG.WAV" }, { "id": "FILE00", @@ -1489,593 +1543,1505 @@ "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0603_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0202_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Increase_Production_Leia" + "Unit_Move_Leia" ], - "asset": "U000_LEI0603_ENG.WAV" + "asset": "U000_LEI0202_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0104_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0306_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Select_Leia" + "Unit_Attack_Leia" ], - "asset": "U000_LEI0104_ENG.WAV" + "asset": "U000_LEI0306_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0501_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0101_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Remove_Corruption_Leia" + "Unit_Select_Leia" ], - "asset": "U000_LEI0501_ENG.WAV" + "asset": "U000_LEI0101_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027TESTUNITMOVE_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027C000_DST0102_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Move_Gneneric_Test" + "EHD_Death_Star_Activate" ], - "asset": "TESTUNITMOVE_ENG.WAV" + "asset": "C000_DST0102_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0108_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0103_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ "Unit_Select_Leia" ], - "asset": "U000_LEI0108_ENG.WAV" + "asset": "U000_LEI0103_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_MCF1601_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0403_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_StarDest_MC30_Frigate" + "Unit_Guard_Leia" ], - "asset": "U000_MCF1601_ENG.WAV" + "asset": "U000_LEI0403_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0111_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027FS_BEETLE_2.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Select_Leia" + "SFX_Anim_Beetle_Footsteps" ], - "asset": "U000_LEI0111_ENG.WAV" + "asset": "FS_BEETLE_2.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0211_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0201_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ "Unit_Move_Leia" ], - "asset": "U000_LEI0211_ENG.WAV" + "asset": "U000_LEI0201_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0110_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0203_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Select_Leia" + "Unit_Move_Leia" ], - "asset": "U000_LEI0110_ENG.WAV" + "asset": "U000_LEI0203_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0403_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0114_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Guard_Leia" + "Unit_Select_Leia" ], - "asset": "U000_LEI0403_ENG.WAV" + "asset": "U000_LEI0114_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0306_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027FS_BEETLE_1.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Attack_Leia" + "SFX_Anim_Beetle_Footsteps" ], - "asset": "U000_LEI0306_ENG.WAV" + "asset": "FS_BEETLE_1.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0308_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0304_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ "Unit_Attack_Leia" ], - "asset": "U000_LEI0308_ENG.WAV" + "asset": "U000_LEI0304_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0112_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0301_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Select_Leia" + "Unit_Attack_Leia" ], - "asset": "U000_LEI0112_ENG.WAV" + "asset": "U000_LEI0301_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0301_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0503_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Attack_Leia" + "Unit_Remove_Corruption_Leia" ], - "asset": "U000_LEI0301_ENG.WAV" + "asset": "U000_LEI0503_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0404_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0109_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Guard_Leia" + "Unit_Select_Leia" ], - "asset": "U000_LEI0404_ENG.WAV" + "asset": "U000_LEI0109_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_TMC0212_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0308_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Move_Tie_Mauler" + "Unit_Attack_Leia" ], - "asset": "U000_TMC0212_ENG.WAV" + "asset": "U000_LEI0308_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027EGL_STAR_VIPER_SPINNING_1.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0402_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Star_Viper_Spinning_By" + "Unit_Guard_Leia" ], - "asset": "EGL_STAR_VIPER_SPINNING_1.WAV" + "asset": "U000_LEI0402_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0208_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0108_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Move_Leia" + "Unit_Select_Leia" ], - "asset": "U000_LEI0208_ENG.WAV" + "asset": "U000_LEI0108_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0604_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0307_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Increase_Production_Leia" + "Unit_Attack_Leia" ], - "asset": "U000_LEI0604_ENG.WAV" + "asset": "U000_LEI0307_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027FS_BEETLE_3.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0311_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "SFX_Anim_Beetle_Footsteps" + "Unit_Attack_Leia" ], - "asset": "FS_BEETLE_3.WAV" + "asset": "U000_LEI0311_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0109_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0102_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ "Unit_Select_Leia" ], - "asset": "U000_LEI0109_ENG.WAV" + "asset": "U000_LEI0102_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0202_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0104_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Move_Leia" + "Unit_Select_Leia" ], - "asset": "U000_LEI0202_ENG.WAV" + "asset": "U000_LEI0104_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0602_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027FS_BEETLE_3.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Increase_Production_Leia" + "SFX_Anim_Beetle_Footsteps" ], - "asset": "U000_LEI0602_ENG.WAV" + "asset": "FS_BEETLE_3.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0305_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0313_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ "Unit_Attack_Leia" ], - "asset": "U000_LEI0305_ENG.WAV" + "asset": "U000_LEI0313_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_MAL0503_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0206_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Assist_Move_Missile_Launcher" + "Unit_Move_Leia" ], - "asset": "U000_MAL0503_ENG.WAV" + "asset": "U000_LEI0206_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0601_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_ARC3104_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Increase_Production_Leia" + "Unit_Produce_Troops_Arc_Hammer" ], - "asset": "U000_LEI0601_ENG.WAV" + "asset": "U000_ARC3104_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_ARC3106_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0312_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Complete_Troops_Arc_Hammer" + "Unit_Attack_Leia" ], - "asset": "U000_ARC3106_ENG.WAV" + "asset": "U000_LEI0312_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027FS_BEETLE_4.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0215_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "SFX_Anim_Beetle_Footsteps" + "Unit_Move_Leia" ], - "asset": "FS_BEETLE_4.WAV" + "asset": "U000_LEI0215_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027FS_BEETLE_1.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0107_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "SFX_Anim_Beetle_Footsteps" + "Unit_Select_Leia" ], - "asset": "FS_BEETLE_1.WAV" + "asset": "U000_LEI0107_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0205_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0501_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Move_Leia" + "Unit_Remove_Corruption_Leia" ], - "asset": "U000_LEI0205_ENG.WAV" + "asset": "U000_LEI0501_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0113_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027AMB_DES_CLEAR_LOOP_1.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Select_Leia" + "Weather_Ambient_Clear_Sandstorm_Loop" ], - "asset": "U000_LEI0113_ENG.WAV" + "asset": "AMB_DES_CLEAR_LOOP_1.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0314_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0504_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Attack_Leia" + "Unit_Remove_Corruption_Leia" ], - "asset": "U000_LEI0314_ENG.WAV" + "asset": "U000_LEI0504_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0304_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0502_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Attack_Leia" + "Unit_Remove_Corruption_Leia" ], - "asset": "U000_LEI0304_ENG.WAV" + "asset": "U000_LEI0502_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0203_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_DEF3106_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Move_Leia" + "Unit_Weaken_Sabateur" ], - "asset": "U000_LEI0203_ENG.WAV" + "asset": "U000_DEF3106_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027C000_DST0102_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_ARC3105_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "EHD_Death_Star_Activate" + "Unit_Complete_Troops_Arc_Hammer" ], - "asset": "C000_DST0102_ENG.WAV" + "asset": "U000_ARC3105_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0114_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0601_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Select_Leia" + "Unit_Increase_Production_Leia" ], - "asset": "U000_LEI0114_ENG.WAV" + "asset": "U000_LEI0601_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_ARC3104_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0204_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Produce_Troops_Arc_Hammer" + "Unit_Move_Leia" ], - "asset": "U000_ARC3104_ENG.WAV" + "asset": "U000_LEI0204_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027FS_BEETLE_2.WAV\u0027 could not be found.", + "message": "Audio file \u0027U000_LEI0207_ENG.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "SFX_Anim_Beetle_Footsteps" + "Unit_Move_Leia" ], - "asset": "FS_BEETLE_2.WAV" + "asset": "U000_LEI0207_ENG.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_ARC3105_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027FS_BEETLE_4.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Complete_Troops_Arc_Hammer" + "SFX_Anim_Beetle_Footsteps" ], - "asset": "U000_ARC3105_ENG.WAV" + "asset": "FS_BEETLE_4.WAV" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.AudioFilesVerifier" ], - "message": "Audio file \u0027U000_LEI0307_ENG.WAV\u0027 could not be found.", + "message": "Audio file \u0027AMB_URB_CLEAR_LOOP_1.WAV\u0027 could not be found.", "severity": "Error", "context": [ - "Unit_Attack_Leia" + "Weather_Ambient_Clear_Urban_Loop" ], - "asset": "U000_LEI0307_ENG.WAV" + "asset": "AMB_URB_CLEAR_LOOP_1.WAV" }, { "id": "FILE00", "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" + "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" ], - "message": "Audio file \u0027U000_LEI0402_ENG.WAV\u0027 could not be found.", + "message": "Could not find GUI texture \u0027i_dialogue_button_large_middle_off.tga\u0027 at location \u0027Repository\u0027.", "severity": "Error", "context": [ - "Unit_Guard_Leia" + "IDC_PLAY_FACTION_B_BUTTON_BIG", + "Repository" ], - "asset": "U000_LEI0402_ENG.WAV" + "asset": "i_dialogue_button_large_middle_off.tga" }, { "id": "FILE00", "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" + "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" ], - "message": "Audio file \u0027U000_LEI0312_ENG.WAV\u0027 could not be found.", + "message": "Could not find GUI texture \u0027underworld_logo_selected.tga\u0027 at location \u0027MegaTexture\u0027.", "severity": "Error", "context": [ - "Unit_Attack_Leia" + "IDC_PLAY_FACTION_A_BUTTON_BIG", + "MegaTexture" ], - "asset": "U000_LEI0312_ENG.WAV" + "asset": "underworld_logo_selected.tga" }, { "id": "FILE00", "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" + "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" ], - "message": "Audio file \u0027U000_LEI0210_ENG.WAV\u0027 could not be found.", + "message": "Could not find GUI texture \u0027underworld_logo_off.tga\u0027 at location \u0027MegaTexture\u0027.", "severity": "Error", "context": [ - "Unit_Move_Leia" + "IDC_PLAY_FACTION_A_BUTTON_BIG", + "MegaTexture" ], - "asset": "U000_LEI0210_ENG.WAV" + "asset": "underworld_logo_off.tga" }, { "id": "FILE00", "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" + "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" ], - "message": "Audio file \u0027U000_LEI0313_ENG.WAV\u0027 could not be found.", + "message": "Could not find GUI texture \u0027i_button_petro_sliver.tga\u0027 at location \u0027MegaTexture\u0027.", "severity": "Error", "context": [ - "Unit_Attack_Leia" + "IDC_MENU_PETRO_LOGO", + "MegaTexture" ], - "asset": "U000_LEI0313_ENG.WAV" + "asset": "i_button_petro_sliver.tga" }, { "id": "FILE00", "verifiers": [ "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" ], - "message": "Could not find GUI texture \u0027underworld_logo_selected.tga\u0027 at location \u0027MegaTexture\u0027.", + "message": "Could not find GUI texture \u0027underworld_logo_rollover.tga\u0027 at location \u0027MegaTexture\u0027.", "severity": "Error", "context": [ "IDC_PLAY_FACTION_A_BUTTON_BIG", "MegaTexture" ], - "asset": "underworld_logo_selected.tga" + "asset": "underworld_logo_rollover.tga" }, { - "id": "FILE00", + "id": "CMDBAR05", "verifiers": [ - "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" + "AET.ModVerify.Verifiers.CommandBarVerifier" ], - "message": "Could not find GUI texture \u0027i_button_petro_sliver.tga\u0027 at location \u0027MegaTexture\u0027.", - "severity": "Error", - "context": [ - "IDC_MENU_PETRO_LOGO", - "MegaTexture" + "message": "The CommandBar component \u0027g_planet_land_forces\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_planet_land_forces" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" ], - "asset": "i_button_petro_sliver.tga" + "message": "The CommandBar component \u0027g_ground_sell\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_ground_sell" }, { - "id": "FILE00", + "id": "CMDBAR05", "verifiers": [ - "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" + "AET.ModVerify.Verifiers.CommandBarVerifier" ], - "message": "Could not find GUI texture \u0027i_dialogue_button_large_middle_off.tga\u0027 at location \u0027Repository\u0027.", - "severity": "Error", - "context": [ - "IDC_PLAY_FACTION_B_BUTTON_BIG", - "Repository" + "message": "The CommandBar component \u0027st_bracket_medium\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_bracket_medium" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" ], - "asset": "i_dialogue_button_large_middle_off.tga" + "message": "The CommandBar component \u0027b_planet_right\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "b_planet_right" }, { - "id": "FILE00", + "id": "CMDBAR04", "verifiers": [ - "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" + "AET.ModVerify.Verifiers.CommandBarVerifier" ], - "message": "Could not find GUI texture \u0027underworld_logo_rollover.tga\u0027 at location \u0027MegaTexture\u0027.", - "severity": "Error", - "context": [ - "IDC_PLAY_FACTION_A_BUTTON_BIG", - "MegaTexture" + "message": "The CommandBar component \u0027g_credit_bar\u0027 is not supported by the game.", + "severity": "Information", + "context": [], + "asset": "g_credit_bar" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" ], - "asset": "underworld_logo_rollover.tga" + "message": "The CommandBar component \u0027zoomed_header_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "zoomed_header_text" }, { - "id": "FILE00", + "id": "CMDBAR05", "verifiers": [ - "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" + "AET.ModVerify.Verifiers.CommandBarVerifier" ], - "message": "Could not find GUI texture \u0027underworld_logo_off.tga\u0027 at location \u0027MegaTexture\u0027.", - "severity": "Error", - "context": [ - "IDC_PLAY_FACTION_A_BUTTON_BIG", - "MegaTexture" + "message": "The CommandBar component \u0027g_space_level_pips\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_space_level_pips" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" ], - "asset": "underworld_logo_off.tga" + "message": "The CommandBar component \u0027bribed_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "bribed_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027encyclopedia_header_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "encyclopedia_header_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tutorial_text_back\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tutorial_text_back" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027encyclopedia_back\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "encyclopedia_back" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027encyclopedia_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "encyclopedia_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027balance_pip\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "balance_pip" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027objective_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "objective_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027skirmish_upgrade\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "skirmish_upgrade" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027surface_mod_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "surface_mod_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_hero_health\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_hero_health" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027bribe_display\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "bribe_display" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_build\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_build" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027garrison_slot_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "garrison_slot_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_conflict\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_conflict" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tooltip_name\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tooltip_name" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027garrison_respawn_counter\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "garrison_respawn_counter" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_ability_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_ability_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_shields_medium\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_shields_medium" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_health\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_health" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_weather\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_weather" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_health_medium\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_health_medium" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_power\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_power" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_ground_level_pips\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_ground_level_pips" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027zoomed_cost_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "zoomed_cost_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027bm_title_4011\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "bm_title_4011" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_planet_name\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_planet_name" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_shields_large\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_shields_large" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_hero_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_hero_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027generic_flytext\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "generic_flytext" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027reinforcement_counter\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "reinforcement_counter" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_planet_value\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_planet_value" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027radar_blip\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "radar_blip" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_political_control\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_political_control" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_planet_ring\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_planet_ring" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_garrison_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_garrison_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027encyclopedia_right_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "encyclopedia_right_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027b_quick_ref\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "b_quick_ref" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027objective_back\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "objective_back" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027encyclopedia_center_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "encyclopedia_center_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_shields\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_shields" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_grab_bar\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_grab_bar" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_smuggler\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_smuggler" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_enemy_hero\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_enemy_hero" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027cs_ability_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "cs_ability_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027encyclopedia_cost_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "encyclopedia_cost_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_planet_ability\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_planet_ability" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_control_group\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_control_group" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027gui_dialog_tooltip\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "gui_dialog_tooltip" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027remote_bomb_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "remote_bomb_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tutorial_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tutorial_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_space_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_space_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_bracket_large\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_bracket_large" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027zoomed_back\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "zoomed_back" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027encyclopedia_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "encyclopedia_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027zoomed_right_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "zoomed_right_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027b_beacon_t\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "b_beacon_t" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_bounty_hunter\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_bounty_hunter" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_credit_bar\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_credit_bar" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_hero\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_hero" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027bm_title_4010\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "bm_title_4010" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_planet_fleet\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_planet_fleet" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_corruption_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_corruption_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_smuggled\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_smuggled" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027help_back\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "help_back" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_bracket_small\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_bracket_small" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027objective_header_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "objective_header_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_corruption_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_corruption_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_ground_level\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_ground_level" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027lt_weather_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "lt_weather_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027cs_ability_button\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "cs_ability_button" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_radar_view\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_radar_view" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027objective_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "objective_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tooltip_back\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tooltip_back" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027zoomed_center_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "zoomed_center_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_health_bar\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_health_bar" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027zoomed_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "zoomed_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027generic_collision\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "generic_collision" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tooltip_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tooltip_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_radar_blip\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_radar_blip" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_hero_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_hero_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_space_level\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_space_level" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027b_planet_left\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "b_planet_left" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tooltip_icon_land\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tooltip_icon_land" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_ground_icon\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_ground_icon" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tooltip_price\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tooltip_price" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tooltip_left_text\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tooltip_left_text" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027st_health_large\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "st_health_large" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027tactical_sell\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "tactical_sell" + }, + { + "id": "CMDBAR05", + "verifiers": [ + "AET.ModVerify.Verifiers.CommandBarVerifier" + ], + "message": "The CommandBar component \u0027g_special_ability\u0027 is not connected to a shell component.", + "severity": "Warning", + "context": [], + "asset": "g_special_ability" } ] } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/CommandLine/BaseModVerifyOptions.cs b/src/ModVerify.CliApp/Settings/CommandLine/BaseModVerifyOptions.cs index 0e5e203..36c8509 100644 --- a/src/ModVerify.CliApp/Settings/CommandLine/BaseModVerifyOptions.cs +++ b/src/ModVerify.CliApp/Settings/CommandLine/BaseModVerifyOptions.cs @@ -21,9 +21,9 @@ internal abstract class BaseModVerifyOptions public string? Suppressions { get; init; } [Option("path", SetName = "autoDetection", Required = false, Default = null, - HelpText = "Specifies the path to verify. The path may be a game or mod. The application will try to find all necessary sub-mods or base games itself. " + + HelpText = "Specifies the path to verify. The path may be a game or mod. The application will try to find all necessary sub-mods and base games itself. " + "The argument cannot be combined with any of --mods, --game or --fallbackGame")] - public string? AutoPath { get; init; } + public string? TargetPath { get; init; } [Option("mods", SetName = "manualPaths", Required = false, Default = null, Separator = ';', HelpText = "The path of the mod to verify. To support submods, multiple paths can be separated using the ';' (semicolon) character. " + @@ -41,10 +41,10 @@ internal abstract class BaseModVerifyOptions public string? FallbackGamePath { get; init; } - [Option("type", Required = false, Default = null, - HelpText = "The game type of the mod that shall be verified. Skip this value to auto-determine the type. Valid values are 'Eaw' and 'Foc'. " + + [Option("engine", Required = false, Default = null, + HelpText = "The game engine of the target that shall be verified. Skip this value to auto-determine the type. Valid values are 'Eaw' and 'Foc'. " + "This argument is required, if the first mod of '--mods' points to a directory outside of the common folder hierarchy (e.g, /MODS/MOD_NAME or /32470/WORKSHOP_ID")] - public GameEngineType? GameType { get; init; } + public GameEngineType? Engine { get; init; } [Option("additionalFallbackPaths", Required = false, Separator = ';', diff --git a/src/ModVerify.CliApp/Settings/CommandLine/CreateBaselineVerbOption.cs b/src/ModVerify.CliApp/Settings/CommandLine/CreateBaselineVerbOption.cs index dc60a73..6593245 100644 --- a/src/ModVerify.CliApp/Settings/CommandLine/CreateBaselineVerbOption.cs +++ b/src/ModVerify.CliApp/Settings/CommandLine/CreateBaselineVerbOption.cs @@ -7,4 +7,7 @@ internal sealed class CreateBaselineVerbOption : BaseModVerifyOptions { [Option('o', "outFile", Required = true, HelpText = "The file path of the new baseline file.")] public required string OutputFile { get; init; } + + [Option("skipLocation", Required = false, HelpText = "Skips writing the target location to the baseline.")] + public bool SkipLocation { get; init; } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/CommandLine/VerifyVerbOption.cs b/src/ModVerify.CliApp/Settings/CommandLine/VerifyVerbOption.cs index 97f1536..e3be836 100644 --- a/src/ModVerify.CliApp/Settings/CommandLine/VerifyVerbOption.cs +++ b/src/ModVerify.CliApp/Settings/CommandLine/VerifyVerbOption.cs @@ -16,17 +16,19 @@ internal sealed class VerifyVerbOption : BaseModVerifyOptions public string? OutputDirectory { get; init; } [Option("failFast", Required = false, Default = false, - HelpText = "When set, the application will abort on the first failure. The option also recognized the 'MinimumFailureSeverity' setting.")] + HelpText = "When set, the application will abort on the first failure. " + + "The option requires 'minFailSeverity' to be set.")] public bool FailFast { get; init; } [Option("minFailSeverity", Required = false, Default = null, - HelpText = "When set, the application return with an error, if any finding has at least the specified severity value.")] + HelpText = "When set, the application returns with an error, if any finding has at least the specified severity value.")] public VerificationSeverity? MinimumFailureSeverity { get; set; } [Option("ignoreAsserts", Required = false, HelpText = "When this flag is present, the application will not report engine assertions.")] public bool IgnoreAsserts { get; init; } + [Option("baseline", SetName = "baselineSelection", Required = false, HelpText = "Path to a JSON baseline file. Cannot be used together with --searchBaseline.")] public string? Baseline { get; init; } diff --git a/src/ModVerify.CliApp/Settings/CommandLineHelper.cs b/src/ModVerify.CliApp/Settings/CommandLineHelper.cs new file mode 100644 index 0000000..f799591 --- /dev/null +++ b/src/ModVerify.CliApp/Settings/CommandLineHelper.cs @@ -0,0 +1,18 @@ +using System; +using System.Linq; +using System.Reflection; +using CommandLine; + +namespace AET.ModVerify.App.Settings; + +internal static class CommandLineHelper +{ + public static string GetOptionName(this Type type, string optionPropertyName) + { + var property = type.GetProperties().FirstOrDefault(p => p.Name.Equals(optionPropertyName)); + var optionAttribute = property?.GetCustomAttribute(); + return optionAttribute is null + ? throw new InvalidOperationException($"Unable to get option data for {type}:{optionAttribute}") + : $"--{optionAttribute.LongName}"; + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/GameInstallationsSettings.cs b/src/ModVerify.CliApp/Settings/GameInstallationsSettings.cs deleted file mode 100644 index 00bc4f0..0000000 --- a/src/ModVerify.CliApp/Settings/GameInstallationsSettings.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using PG.StarWarsGame.Engine; - -namespace AET.ModVerify.App.Settings; - -internal sealed record GameInstallationsSettings -{ - public bool Interactive => string.IsNullOrEmpty(AutoPath) && ModPaths.Count == 0 && string.IsNullOrEmpty(GamePath); - - [MemberNotNullWhen(true, nameof(AutoPath))] - public bool UseAutoDetection => !string.IsNullOrEmpty(AutoPath); - - [MemberNotNullWhen(true, nameof(GamePath))] - public bool ManualSetup => !string.IsNullOrEmpty(GamePath); - - public string? AutoPath { get; init; } - - public IList ModPaths { get; init; } = []; - - public string? GamePath { get; init; } - - public string? FallbackGamePath { get; init; } - - public IList AdditionalFallbackPaths { get; init; } = []; - - public GameEngineType? EngineType { get; init; } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/ModVerifyAppSettings.cs b/src/ModVerify.CliApp/Settings/ModVerifyAppSettings.cs index 3376354..e4488a0 100644 --- a/src/ModVerify.CliApp/Settings/ModVerifyAppSettings.cs +++ b/src/ModVerify.CliApp/Settings/ModVerifyAppSettings.cs @@ -1,23 +1,77 @@ -using System.Diagnostics.CodeAnalysis; +using System; using AET.ModVerify.Reporting; using AET.ModVerify.Settings; namespace AET.ModVerify.App.Settings; -internal sealed class ModVerifyAppSettings +public class AppReportSettings { - public bool Interactive => GameInstallationsSettings.Interactive; + public VerificationSeverity MinimumReportSeverity { get; init; } + + public string? SuppressionsPath { get; init; } - public required VerifyPipelineSettings VerifyPipelineSettings { get; init; } + public bool Verbose { get; init; } +} - public required ModVerifyReportSettings ReportSettings { get; init; } +public sealed class VerifyReportSettings : AppReportSettings +{ + public string? BaselinePath { get; init; } + public bool SearchBaselineLocally { get; init; } +} + +internal abstract class AppSettingsBase(AppReportSettings reportSettings) +{ + public bool IsInteractive => VerificationTargetSettings.Interactive; + + public required VerificationTargetSettings VerificationTargetSettings + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } + + public required VerifyPipelineSettings VerifyPipelineSettings + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } - public required GameInstallationsSettings GameInstallationsSettings { get; init; } + public AppReportSettings ReportSettings { get; } = reportSettings ?? throw new ArgumentNullException(nameof(reportSettings)); +} - public VerificationSeverity? AppThrowsOnMinimumSeverity { get; init; } +internal abstract class AppSettingsBase(T reportSettings) : AppSettingsBase(reportSettings) + where T : AppReportSettings +{ + public new T ReportSettings { get; } = reportSettings ?? throw new ArgumentNullException(nameof(reportSettings)); +} + +internal sealed class AppVerifySettings(VerifyReportSettings reportSettings) : AppSettingsBase(reportSettings) +{ + public VerificationSeverity? AppFailsOnMinimumSeverity { get; init; } - [MemberNotNullWhen(true, nameof(NewBaselinePath))] - public bool CreateNewBaseline => !string.IsNullOrEmpty(NewBaselinePath); + public required string ReportDirectory + { + get; + init + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentNullException(nameof(value)); + field = value; + } + } +} + +internal sealed class AppBaselineSettings(AppReportSettings reportSettings) : AppSettingsBase(reportSettings) +{ + public required string NewBaselinePath + { + get; + init + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentNullException(nameof(value)); + field = value; + } + } - public string? NewBaselinePath { get; init; } + public bool WriteLocations { get; init; } = true; } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/ModVerifyReportSettings.cs b/src/ModVerify.CliApp/Settings/ModVerifyReportSettings.cs deleted file mode 100644 index 482b844..0000000 --- a/src/ModVerify.CliApp/Settings/ModVerifyReportSettings.cs +++ /dev/null @@ -1,14 +0,0 @@ -using AET.ModVerify.Reporting; - -namespace AET.ModVerify.App.Settings; - -internal sealed class ModVerifyReportSettings -{ - public VerificationSeverity MinimumReportSeverity { get; init; } - - public string? SuppressionsPath { get; init; } - - public string? BaselinePath { get; init; } - - public bool SearchBaselineLocally { get; init; } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/SettingsBuilder.cs b/src/ModVerify.CliApp/Settings/SettingsBuilder.cs index 3c5535f..23a89ff 100644 --- a/src/ModVerify.CliApp/Settings/SettingsBuilder.cs +++ b/src/ModVerify.CliApp/Settings/SettingsBuilder.cs @@ -1,22 +1,18 @@ -using System; -using System.Collections.Generic; -using System.IO.Abstractions; -using AET.ModVerify.App.Settings.CommandLine; -using AET.ModVerify.App.Utilities; +using AET.ModVerify.App.Settings.CommandLine; using AET.ModVerify.Pipeline; -using AET.ModVerify.Reporting; using AET.ModVerify.Settings; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO.Abstractions; namespace AET.ModVerify.App.Settings; internal sealed class SettingsBuilder(IServiceProvider serviceProvider) { - private readonly ILogger? _logger = serviceProvider.GetService()?.CreateLogger(typeof(SettingsBuilder)); private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); - public ModVerifyAppSettings BuildSettings(BaseModVerifyOptions options) + public AppSettingsBase BuildSettings(BaseModVerifyOptions options) { switch (options) { @@ -28,90 +24,103 @@ public ModVerifyAppSettings BuildSettings(BaseModVerifyOptions options) throw new NotSupportedException($"The option '{options.GetType().Name}' is not supported!"); } - private ModVerifyAppSettings BuildFromVerifyVerb(VerifyVerbOption verifyOptions) + private AppVerifySettings BuildFromVerifyVerb(VerifyVerbOption verifyOptions) { - return new ModVerifyAppSettings + ValidateVerb(); + var failFastSetting = GetFailFastSetting(); + return new AppVerifySettings(BuildReportSettings()) { + ReportDirectory = GetReportDirectory(), VerifyPipelineSettings = new VerifyPipelineSettings { ParallelVerifiers = verifyOptions.Parallel ? 4 : 1, VerifiersProvider = new DefaultGameVerifiersProvider(), - FailFast = verifyOptions.FailFast, + FailFastSettings = failFastSetting, GameVerifySettings = new GameVerifySettings { IgnoreAsserts = verifyOptions.IgnoreAsserts, - ThrowsOnMinimumSeverity = GetVerifierMinimumThrowSeverity() + ThrowsOnMinimumSeverity = failFastSetting.IsFailFast + ? failFastSetting.MinumumSeverity + // The app shall not make a specific verifier throw, but it should always run to completion. + : null } }, - AppThrowsOnMinimumSeverity = verifyOptions.MinimumFailureSeverity, - GameInstallationsSettings = BuildInstallationSettings(verifyOptions), - ReportSettings = BuildReportSettings(verifyOptions), + AppFailsOnMinimumSeverity = verifyOptions.MinimumFailureSeverity, + VerificationTargetSettings = BuildTargetSettings(verifyOptions), }; - VerificationSeverity? GetVerifierMinimumThrowSeverity() + void ValidateVerb() { - var minFailSeverity = verifyOptions.MinimumFailureSeverity; - if (verifyOptions.FailFast) + if (verifyOptions.SearchBaselineLocally && !string.IsNullOrEmpty(verifyOptions.Baseline)) { - if (minFailSeverity == null) - { - _logger?.LogWarning(ModVerifyConstants.ConsoleEventId, - "Verification is configured to fail fast but 'minFailSeverity' is not specified. Using severity '{Info}'.", VerificationSeverity.Information); - minFailSeverity = VerificationSeverity.Information; - } + var searchOption = typeof(VerifyVerbOption).GetOptionName(nameof(VerifyVerbOption.SearchBaselineLocally)); + var baselineOption = typeof(VerifyVerbOption).GetOptionName(nameof(VerifyVerbOption.Baseline)); + throw new AppArgumentException($"Options {searchOption} and {baselineOption} cannot be used together."); + } - return minFailSeverity; + if (verifyOptions is { FailFast: true, MinimumFailureSeverity: null }) + { + var failFast = typeof(VerifyVerbOption).GetOptionName(nameof(VerifyVerbOption.FailFast)); + var minThrowSeverity = typeof(VerifyVerbOption).GetOptionName(nameof(VerifyVerbOption.MinimumFailureSeverity)); + throw new AppArgumentException($"Option {failFast} requires to set {minThrowSeverity}."); } + } + + FailFastSetting GetFailFastSetting() + { + return !verifyOptions.FailFast + ? FailFastSetting.NoFailFast + : new FailFastSetting(verifyOptions.MinimumFailureSeverity!.Value); + } + + string GetReportDirectory() + { + return _fileSystem.Path.GetFullPath(_fileSystem.Path.Combine( + Environment.CurrentDirectory, + verifyOptions.OutputDirectory ?? "ModVerifyResults")); + } - // Only in a failFast scenario we want the verifier to throw. - // In a normal run, the verifier should simply store the error. - return null; + VerifyReportSettings BuildReportSettings() + { + return new VerifyReportSettings + { + BaselinePath = verifyOptions.Baseline, + MinimumReportSeverity = verifyOptions.MinimumSeverity, + SearchBaselineLocally = verifyOptions.SearchBaselineLocally, + SuppressionsPath = verifyOptions.Suppressions, + Verbose = verifyOptions.Verbose + }; } } - private ModVerifyAppSettings BuildFromCreateBaselineVerb(CreateBaselineVerbOption baselineVerb) + private AppBaselineSettings BuildFromCreateBaselineVerb(CreateBaselineVerbOption baselineVerb) { - return new ModVerifyAppSettings + return new AppBaselineSettings(BuildReportSettings()) { VerifyPipelineSettings = new VerifyPipelineSettings { ParallelVerifiers = baselineVerb.Parallel ? 4 : 1, - GameVerifySettings = new GameVerifySettings - { - IgnoreAsserts = false, - ThrowsOnMinimumSeverity = null, - }, VerifiersProvider = new DefaultGameVerifiersProvider(), - FailFast = false, + GameVerifySettings = GameVerifySettings.Default, + FailFastSettings = FailFastSetting.NoFailFast, }, - AppThrowsOnMinimumSeverity = null, - GameInstallationsSettings = BuildInstallationSettings(baselineVerb), - ReportSettings = BuildReportSettings(baselineVerb), + VerificationTargetSettings = BuildTargetSettings(baselineVerb), NewBaselinePath = baselineVerb.OutputFile, + WriteLocations = !baselineVerb.SkipLocation }; - } - private static ModVerifyReportSettings BuildReportSettings(BaseModVerifyOptions options) - { - var baselinePath = (options as VerifyVerbOption)?.Baseline; - - return new ModVerifyReportSettings - { - BaselinePath = baselinePath, - MinimumReportSeverity = options.MinimumSeverity, - SearchBaselineLocally = SearchLocally(options), - SuppressionsPath = options.Suppressions - }; - - static bool SearchLocally(BaseModVerifyOptions o) + AppReportSettings BuildReportSettings() { - if (o is not VerifyVerbOption v) - return false; - return v.SearchBaselineLocally || v.LaunchedWithoutArguments(); + return new AppReportSettings + { + MinimumReportSeverity = baselineVerb.MinimumSeverity, + SuppressionsPath = baselineVerb.Suppressions, + Verbose = baselineVerb.Verbose + }; } } - private GameInstallationsSettings BuildInstallationSettings(BaseModVerifyOptions options) + private VerificationTargetSettings BuildTargetSettings(BaseModVerifyOptions options) { var modPaths = new List(); if (options.ModPaths is not null) @@ -142,18 +151,18 @@ private GameInstallationsSettings BuildInstallationSettings(BaseModVerifyOptions if (!string.IsNullOrEmpty(gamePath) && !string.IsNullOrEmpty(options.FallbackGamePath)) fallbackGamePath = _fileSystem.Path.GetFullPath(options.FallbackGamePath!); - var autoPath = options.AutoPath; - if (!string.IsNullOrEmpty(autoPath)) - autoPath = _fileSystem.Path.GetFullPath(autoPath!); + var targetPath = options.TargetPath; + if (!string.IsNullOrEmpty(targetPath)) + targetPath = _fileSystem.Path.GetFullPath(targetPath!); - return new GameInstallationsSettings + return new VerificationTargetSettings { - AutoPath = autoPath, + TargetPath = targetPath, ModPaths = modPaths, GamePath = gamePath, FallbackGamePath = fallbackGamePath, AdditionalFallbackPaths = fallbackPaths, - EngineType = options.GameType + Engine = options.Engine }; } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Settings/VerificationTargetSettings.cs b/src/ModVerify.CliApp/Settings/VerificationTargetSettings.cs new file mode 100644 index 0000000..b5e1a40 --- /dev/null +++ b/src/ModVerify.CliApp/Settings/VerificationTargetSettings.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using PG.StarWarsGame.Engine; + +namespace AET.ModVerify.App.Settings; + +internal sealed record VerificationTargetSettings +{ + public bool Interactive => string.IsNullOrEmpty(TargetPath) && ModPaths.Count == 0 && string.IsNullOrEmpty(GamePath) && string.IsNullOrEmpty(FallbackGamePath); + + [MemberNotNullWhen(true, nameof(TargetPath))] + public bool UseAutoDetection => !string.IsNullOrEmpty(TargetPath) && ModPaths.Count == 0 && string.IsNullOrEmpty(GamePath) && string.IsNullOrEmpty(FallbackGamePath); + + [MemberNotNullWhen(true, nameof(GamePath))] + public bool ManualSetup => !string.IsNullOrEmpty(GamePath); + + public string? TargetPath { get; init; } + + public IReadOnlyList ModPaths { get; init; } = []; + + public string? GamePath { get; init; } + + public string? FallbackGamePath { get; init; } + + public IReadOnlyList AdditionalFallbackPaths { get; init; } = []; + + public GameEngineType? Engine { get; init; } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/TargetSelectors/AutomaticSelector.cs b/src/ModVerify.CliApp/TargetSelectors/AutomaticSelector.cs new file mode 100644 index 0000000..5250ec4 --- /dev/null +++ b/src/ModVerify.CliApp/TargetSelectors/AutomaticSelector.cs @@ -0,0 +1,163 @@ +using AET.ModVerify.App.GameFinder; +using AET.ModVerify.App.Settings; +using AET.ModVerify.App.Utilities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Infrastructure; +using PG.StarWarsGame.Infrastructure.Games; +using PG.StarWarsGame.Infrastructure.Mods; +using PG.StarWarsGame.Infrastructure.Services; +using PG.StarWarsGame.Infrastructure.Services.Detection; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO.Abstractions; +using System.Linq; +using AnakinRaW.CommonUtilities.FileSystem; + +namespace AET.ModVerify.App.TargetSelectors; + +internal class AutomaticSelector(IServiceProvider serviceProvider) : VerificationTargetSelectorBase(serviceProvider) +{ + private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); + + internal override SelectionResult SelectTarget(VerificationTargetSettings settings) + { + if (!settings.UseAutoDetection) + throw new ArgumentException("wrong settings format provided.", nameof(settings)); + + var targetPath = settings.TargetPath; + if (!_fileSystem.Directory.Exists(targetPath)) + { + Logger?.LogError(ModVerifyConstants.ConsoleEventId, "The specified path '{Path}' does not exist.", targetPath); + throw new TargetNotFoundException(targetPath); + } + + var engine = settings.Engine; + + GameFinderResult finderResult; + try + { + var finderSettings = new GameFinderSettings + { + Engine = engine, + InitMods = true, + SearchFallbackGame = true + }; + finderResult = GameFinderService.FindGamesFromPathOrGlobal(targetPath, finderSettings); + } + catch (GameNotFoundException) + { + Logger?.LogError(ModVerifyConstants.ConsoleEventId, + "Unable to find games based of the specified target path '{Path}'. Consider specifying all paths manually.", targetPath); + throw; + } + + GameLocations locations; + + var targetObject = GetAttachedModOrGame(finderResult, targetPath, engine); + + if (targetObject is not null) + { + var actualType = targetObject.Game.Type; + Debug.Assert(IsEngineTypeSupported(engine, actualType)); + engine ??= actualType.ToEngineType(); + locations = GetLocations(targetObject, finderResult.FallbackGame, settings.AdditionalFallbackPaths); + } + else + { + if (!engine.HasValue) + throw new ArgumentException("Game engine not specified. Use --engine argument to set it."); + + Logger?.LogDebug("The requested mod at '{TargetPath}' is detached from its games.", targetPath); + + // The path is a detached mod, that exists on a different location than the game. + locations = GetDetachedModLocations(targetPath, finderResult, engine.Value, settings.AdditionalFallbackPaths, out var mod); + targetObject = mod; + } + + return new(locations, engine.Value, targetObject); + } + + private IPhysicalPlayableObject? GetAttachedModOrGame(GameFinderResult finderResult, string targetPath, GameEngineType? requestedEngineType) + { + var targetFullPath = _fileSystem.Path.GetFullPath(targetPath); + + IPhysicalPlayableObject? target = null; + + // If the target is the game directory itself. + if (targetFullPath.Equals(finderResult.Game.Directory.FullName, StringComparison.OrdinalIgnoreCase)) + target = finderResult.Game; + + target ??= GetMatchingModFromGame(finderResult.Game, targetFullPath, requestedEngineType) ?? + GetMatchingModFromGame(finderResult.FallbackGame, targetFullPath, requestedEngineType); + + return target; + } + + private GameLocations GetDetachedModLocations( + string modPath, + GameFinderResult gameResult, + GameEngineType requestedGameEngine, + IReadOnlyList additionalFallbackPaths, + out IPhysicalMod mod) + { + // Because requestedGameEngine must be set, GameFinderService already ensures + // gameResult.Game is the correct type. + var game = gameResult.Game; + var modFinder = ServiceProvider.GetRequiredService(); + var modRef = modFinder.FindMods(game, _fileSystem.DirectoryInfo.New(modPath)).FirstOrDefault(); + + if (modRef is null) + ThrowEngineNotSupported(requestedGameEngine, modPath); + + var modFactory = ServiceProvider.GetRequiredService(); + mod = modFactory.CreatePhysicalMod(game, modRef, CultureInfo.InvariantCulture); + + game.AddMod(mod); + + mod.ResolveDependencies(); + + return GetLocations(mod, gameResult.FallbackGame, additionalFallbackPaths); + } + + private IPhysicalMod? GetMatchingModFromGame(IGame? game, string modPath, GameEngineType? requestedEngineType) + { + if (game is null || !IsEngineTypeSupported(requestedEngineType, game.Type)) + return null; + + foreach (var mod in game.Game.Mods) + { + if (mod is not IPhysicalMod physicalMod) + continue; + + if (_fileSystem.Path.AreEqual(modPath, physicalMod.Directory.FullName)) + return physicalMod; + } + + return null; + } + + private static IGame? GetTargetGame(GameFinderResult finderResult, GameEngineType? requestedEngine) + { + if (finderResult.Game.Type.ToEngineType() == requestedEngine) + return finderResult.Game; + if (finderResult.FallbackGame is not null && finderResult.FallbackGame.Type.ToEngineType() == requestedEngine) + return finderResult.FallbackGame; + return null; + } + + private static bool IsEngineTypeSupported([NotNullWhen(false)] GameEngineType? requestedEngineType, GameType actualGameType) + { + return !requestedEngineType.HasValue || actualGameType.ToEngineType() == requestedEngineType; + } + + [DoesNotReturn] + private static void ThrowEngineNotSupported(GameEngineType requested, string targetPath) + { + throw new ArgumentException($"The specified game engine '{requested}' does not match engine of the verification target '{targetPath}'."); + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectors/ConsoleModSelector.cs b/src/ModVerify.CliApp/TargetSelectors/ConsoleSelector.cs similarity index 79% rename from src/ModVerify.CliApp/ModSelectors/ConsoleModSelector.cs rename to src/ModVerify.CliApp/TargetSelectors/ConsoleSelector.cs index c776d6d..17b9791 100644 --- a/src/ModVerify.CliApp/ModSelectors/ConsoleModSelector.cs +++ b/src/ModVerify.CliApp/TargetSelectors/ConsoleSelector.cs @@ -5,22 +5,21 @@ using AET.ModVerify.App.Settings; using AET.ModVerify.App.Utilities; using AnakinRaW.ApplicationBase; -using PG.StarWarsGame.Engine; using PG.StarWarsGame.Infrastructure; using PG.StarWarsGame.Infrastructure.Games; using PG.StarWarsGame.Infrastructure.Mods; -namespace AET.ModVerify.App.ModSelectors; +namespace AET.ModVerify.App.TargetSelectors; -internal class ConsoleModSelector(IServiceProvider serviceProvider) : ModSelectorBase(serviceProvider) +internal class ConsoleSelector(IServiceProvider serviceProvider) : VerificationTargetSelectorBase(serviceProvider) { - public override GameLocations Select(GameInstallationsSettings settings, out IPhysicalPlayableObject targetObject, - out GameEngineType? actualEngineType) + internal override SelectionResult SelectTarget(VerificationTargetSettings settings) { - var gameResult = GameFinderService.FindGames(); - targetObject = SelectPlayableObject(gameResult); - actualEngineType = targetObject.Game.Type.ToEngineType(); - return GetLocations(targetObject, gameResult, settings.AdditionalFallbackPaths); + var gameResult = GameFinderService.FindGames(GameFinderSettings.Default); + var targetObject = SelectPlayableObject(gameResult); + var engine = targetObject.Game.Type.ToEngineType(); + var locations = GetLocations(targetObject, gameResult.FallbackGame, settings.AdditionalFallbackPaths); + return new SelectionResult(locations, engine, targetObject); } private static IPhysicalPlayableObject SelectPlayableObject(GameFinderResult finderResult) @@ -31,6 +30,9 @@ private static IPhysicalPlayableObject SelectPlayableObject(GameFinderResult fin list.Add(finderResult.Game); Console.WriteLine(); + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine("Listing Games and Mods:"); + Console.ResetColor(); ConsoleUtilities.WriteHorizontalLine(); Console.WriteLine($"0: {game.Name}"); diff --git a/src/ModVerify.CliApp/TargetSelectors/IVerificationTargetSelector.cs b/src/ModVerify.CliApp/TargetSelectors/IVerificationTargetSelector.cs new file mode 100644 index 0000000..6d452e8 --- /dev/null +++ b/src/ModVerify.CliApp/TargetSelectors/IVerificationTargetSelector.cs @@ -0,0 +1,8 @@ +using AET.ModVerify.App.Settings; + +namespace AET.ModVerify.App.TargetSelectors; + +internal interface IVerificationTargetSelector +{ + VerificationTarget Select(VerificationTargetSettings settings); +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/TargetSelectors/ManualSelector.cs b/src/ModVerify.CliApp/TargetSelectors/ManualSelector.cs new file mode 100644 index 0000000..f963b4f --- /dev/null +++ b/src/ModVerify.CliApp/TargetSelectors/ManualSelector.cs @@ -0,0 +1,73 @@ +using System; +using System.Globalization; +using System.Linq; +using AET.ModVerify.App.GameFinder; +using AET.ModVerify.App.Settings; +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Infrastructure; +using PG.StarWarsGame.Infrastructure.Games; +using PG.StarWarsGame.Infrastructure.Services; +using PG.StarWarsGame.Infrastructure.Services.Detection; + +namespace AET.ModVerify.App.TargetSelectors; + +internal class ManualSelector(IServiceProvider serviceProvider) : VerificationTargetSelectorBase(serviceProvider) +{ + internal override SelectionResult SelectTarget(VerificationTargetSettings settings) + { + if (string.IsNullOrEmpty(settings.GamePath)) + throw new ArgumentException("Argument --game must be set."); + if (!settings.Engine.HasValue) + throw new ArgumentException("Unable to determine game type. Use --engine argument to set the game type."); + + var engine = settings.Engine.Value; + + var gameLocations = new GameLocations( + settings.ModPaths.ToList(), + settings.GamePath!, + GetFallbackPaths(settings.FallbackGamePath, settings.AdditionalFallbackPaths).ToList()); + + + // For the manual selector the whole game and mod detection is optional. + // This allows user to use the application for unusual scenarios, + // not known to the detection service. + if (!GameFinderService.TryFindGame(gameLocations.GamePath, + new GameFinderSettings { Engine = engine, InitMods = false, SearchFallbackGame = false }, + out var game)) + { + // TODO: Log + } + + // If the fallback game path is specified we simply try to detect the game and report a warning to the user if not found. + var fallbackGamePath = settings.FallbackGamePath; + if (!string.IsNullOrEmpty(fallbackGamePath)) + { + + if (!GameFinderService.TryFindGame(fallbackGamePath, + new GameFinderSettings { InitMods = false, SearchFallbackGame = false }, + out _)) + { + // TODO: Log + } + } + + var target = TryGetPlayableObject(game, gameLocations.ModPaths.FirstOrDefault()); + return new SelectionResult(gameLocations, engine, target); + } + + private IPhysicalPlayableObject? TryGetPlayableObject(IGame? game, string? modPath) + { + if (game is null) + return null; + if (string.IsNullOrEmpty(modPath)) + return game; + + var modFinder = ServiceProvider.GetRequiredService(); + var modFactory = ServiceProvider.GetRequiredService(); + + var mods = modFinder.FindMods(game, FileSystem.DirectoryInfo.New(modPath)); + var mod = modFactory.CreatePhysicalMod(game, mods.First(), CultureInfo.InvariantCulture); + return mod; + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/TargetSelectors/TargetNotFoundException.cs b/src/ModVerify.CliApp/TargetSelectors/TargetNotFoundException.cs new file mode 100644 index 0000000..d6b052e --- /dev/null +++ b/src/ModVerify.CliApp/TargetSelectors/TargetNotFoundException.cs @@ -0,0 +1,6 @@ +using System.IO; + +namespace AET.ModVerify.App.TargetSelectors; + +internal class TargetNotFoundException(string path) + : DirectoryNotFoundException($"The target path '{path}' does not exist"); \ No newline at end of file diff --git a/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorBase.cs b/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorBase.cs new file mode 100644 index 0000000..68f0f39 --- /dev/null +++ b/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorBase.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Linq; +using System.Runtime.InteropServices; +using AET.ModVerify.App.GameFinder; +using AET.ModVerify.App.Settings; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Infrastructure; +using PG.StarWarsGame.Infrastructure.Clients; +using PG.StarWarsGame.Infrastructure.Clients.Utilities; +using PG.StarWarsGame.Infrastructure.Games; +using PG.StarWarsGame.Infrastructure.Mods; +using PG.StarWarsGame.Infrastructure.Services.Dependencies; + +namespace AET.ModVerify.App.TargetSelectors; + +internal abstract class VerificationTargetSelectorBase : IVerificationTargetSelector +{ + internal sealed record SelectionResult( + GameLocations Locations, + GameEngineType Engine, + IPhysicalPlayableObject? Target); + + protected readonly ILogger? Logger; + protected readonly GameFinderService GameFinderService; + protected readonly IServiceProvider ServiceProvider; + protected readonly IFileSystem FileSystem; + + protected VerificationTargetSelectorBase(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + Logger = serviceProvider.GetService()?.CreateLogger(GetType()); + GameFinderService = new GameFinderService(serviceProvider); + FileSystem = serviceProvider.GetRequiredService(); + } + + public VerificationTarget Select(VerificationTargetSettings settings) + { + var selectedTarget = SelectTarget(settings); + + return new VerificationTarget + { + Location = selectedTarget.Locations, + Engine = selectedTarget.Engine, + Name = GetTargetName(selectedTarget.Target, selectedTarget.Locations), + Version = GetTargetVersion(selectedTarget.Target) + }; + } + + + internal abstract SelectionResult SelectTarget(VerificationTargetSettings settings); + + + protected GameLocations GetLocations( + IPhysicalPlayableObject target, + IGame? fallbackGame, + IReadOnlyList additionalFallbackPaths) + { + var fallbacks = GetFallbackPaths(target, fallbackGame, additionalFallbackPaths); + var modPaths = GetModPaths(target); + return new GameLocations(modPaths, target.Game.Directory.FullName, fallbacks); + } + + private static IReadOnlyList GetFallbackPaths(IPhysicalPlayableObject target, IGame? fallbackGame, IReadOnlyList additionalFallbackPaths) + { + var coercedFallbackGame = fallbackGame; + if (target is IGame tGame && tGame.Equals(fallbackGame)) + coercedFallbackGame = null; + else if (target.Game.Equals(fallbackGame)) + coercedFallbackGame = null; + return GetFallbackPaths(coercedFallbackGame?.Directory.FullName, additionalFallbackPaths); + } + + + protected static IReadOnlyList GetFallbackPaths(string? fallbackGame, IReadOnlyList additionalFallbackPaths) + { + var fallbacks = new List(); + if (fallbackGame is not null) + fallbacks.Add(fallbackGame); + foreach (var fallback in additionalFallbackPaths) + fallbacks.Add(fallback); + return fallbacks; + } + + + private IReadOnlyList GetModPaths(IPhysicalPlayableObject modOrGame) + { + if (modOrGame is not IMod mod) + return []; + + var traverser = ServiceProvider.GetRequiredService(); + return traverser.Traverse(mod) + .OfType().Select(x => x.Directory.FullName) + .ToList(); + } + + protected static string GetTargetName(IPhysicalPlayableObject? targetObject, GameLocations gameLocations) + { + if (targetObject is not null) + return targetObject.Name; + + // TODO: Reuse name beautifier from GameInfrastructure lib + var mod = gameLocations.ModPaths.FirstOrDefault(); + return mod ?? gameLocations.GamePath; + } + + protected string? GetTargetVersion(IPhysicalPlayableObject? targetObject) + { + return targetObject switch + { + IMod mod => mod.Version?.ToString(), + IGame game => GetVersionFromGame(game), + _ => null + }; + } + + private string? GetVersionFromGame(IGame game) + { + var exeFile = GameExecutableFileUtilities.GetExecutableForGame(game, GameBuildType.Release); + if (exeFile is null) + { + Logger?.LogWarning(ModVerifyConstants.ConsoleEventId, + "Unable to get game version of target path '{Path}'. Is this a game directory?", + game.Directory.FullName); + return null; + } + + var versionInfo = FileSystem.FileVersionInfo.GetVersionInfo(exeFile.FullName); + var version = + $"{versionInfo.FileMajorPart}.{versionInfo.FileMinorPart}.{versionInfo.FileBuildPart}.{versionInfo.FilePrivatePart}"; + return version; + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorFactory.cs b/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorFactory.cs new file mode 100644 index 0000000..5e106eb --- /dev/null +++ b/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorFactory.cs @@ -0,0 +1,18 @@ +using System; +using AET.ModVerify.App.Settings; + +namespace AET.ModVerify.App.TargetSelectors; + +internal sealed class VerificationTargetSelectorFactory(IServiceProvider serviceProvider) +{ + public IVerificationTargetSelector CreateSelector(VerificationTargetSettings settings) + { + if (settings.Interactive) + return new ConsoleSelector(serviceProvider); + if (settings.UseAutoDetection) + return new AutomaticSelector(serviceProvider); + if (settings.ManualSetup) + return new ManualSelector(serviceProvider); + throw new ArgumentException("Unknown option configuration provided."); + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Utilities/ExtensionMethods.cs b/src/ModVerify.CliApp/Utilities/ExtensionMethods.cs index b2e7ab0..fc07469 100644 --- a/src/ModVerify.CliApp/Utilities/ExtensionMethods.cs +++ b/src/ModVerify.CliApp/Utilities/ExtensionMethods.cs @@ -8,16 +8,23 @@ namespace AET.ModVerify.App.Utilities; internal static class ExtensionMethods { - public static GameEngineType ToEngineType(this GameType type) + extension(GameEngineType type) { - return type == GameType.Foc ? GameEngineType.Foc : GameEngineType.Eaw; + public GameType FromEngineType() + { + return (GameType)(int)type; + } + + public GameEngineType Opposite() + { + return (GameEngineType)((int)type ^ 1); + } } - public static GameType FromEngineType(this GameEngineType type) + public static GameEngineType ToEngineType(this GameType type) { - return type == GameEngineType.Foc ? GameType.Foc : GameType.Eaw; + return (GameEngineType)(int)type; } - extension(ApplicationEnvironment modVerifyEnvironment) { public bool IsUpdatable() diff --git a/src/ModVerify.CliApp/Utilities/ModVerifyConsoleUtilities.cs b/src/ModVerify.CliApp/Utilities/ModVerifyConsoleUtilities.cs index 6e6664a..b9ebff0 100644 --- a/src/ModVerify.CliApp/Utilities/ModVerifyConsoleUtilities.cs +++ b/src/ModVerify.CliApp/Utilities/ModVerifyConsoleUtilities.cs @@ -1,6 +1,8 @@ using AnakinRaW.ApplicationBase; using Figgle; using System; +using System.Collections.Generic; +using AET.ModVerify.Reporting; namespace AET.ModVerify.App.Utilities; @@ -21,10 +23,68 @@ public static void WriteHeader(string? version = null) Console.ResetColor(); Console.WriteLine(); } + ConsoleUtilities.WriteHorizontalLine('*', lineLength); ConsoleUtilities.WriteLineRight(author, lineLength); Console.WriteLine(); Console.WriteLine(); } + + public static void WriteSelectedTarget(VerificationTarget target) + { + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine("Selected Target:"); + Console.ForegroundColor = ConsoleColor.DarkGray; + ConsoleUtilities.PrintAsTable([ + ("Name", target.Name), + ("Type", target.IsGame ? "Game" : "Mod"), + ("Engine", target.Engine), + ("Version", target.Version ?? "n/a"), + ("Location", target.Location.TargetPath), + ], 120); + Console.ResetColor(); + } + + public static void WriteBaselineInfo(VerificationBaseline baseline, string? filePath) + { + if (baseline.IsEmpty) + return; + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine("Using Baseline:"); + Console.ForegroundColor = ConsoleColor.DarkGray; + + IList<(string, object)> baselineData = + [ + ("Version", baseline.Version?.ToString(2) ?? "n/a"), + ("Is Default", filePath is null), + ("Minimum Severity", baseline.MinimumSeverity.ToString()), + ("Entries", baseline.Count.ToString()) + ]; + if (!string.IsNullOrEmpty(filePath)) + baselineData.Add(("File Path", filePath)); + + ConsoleUtilities.PrintAsTable(baselineData, 120); + + if (baseline.Target is not null) + { + Console.ForegroundColor = ConsoleColor.DarkMagenta; + Console.WriteLine("Baseline Target:"); + Console.ForegroundColor = ConsoleColor.DarkGray; + + IList<(string, object)> targetData = [ + ("Name", baseline.Target.Name), + ("Type", baseline.Target.IsGame ? "Game" : "Mod"), + ("Engine", baseline.Target.Engine), + ("Version", baseline.Target.Version ?? "n/a"), + ]; + + if (baseline.Target.Location is not null) + targetData.Add(("Location", baseline.Target.Location.TargetPath)); + + ConsoleUtilities.PrintAsTable(targetData, 120); + } + Console.ResetColor(); + } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Utilities/PathUtilities.cs b/src/ModVerify.CliApp/Utilities/PathUtilities.cs new file mode 100644 index 0000000..561630f --- /dev/null +++ b/src/ModVerify.CliApp/Utilities/PathUtilities.cs @@ -0,0 +1,39 @@ +using System; +using System.Runtime.InteropServices; +using AnakinRaW.CommonUtilities.FileSystem.Normalization; + +namespace AET.ModVerify.App.Utilities; + +internal static class PathUtilities +{ + private static readonly string HomeVariable; + private static readonly string HomePath; + private static readonly StringComparison StringComparer; + + static PathUtilities() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + HomeVariable = "%USERPROFILE%"; + StringComparer = StringComparison.OrdinalIgnoreCase; + } + else + { + HomeVariable = "$HOME"; + StringComparer = StringComparison.Ordinal; + } + + HomePath = PathNormalizer.Normalize( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + PathNormalizeOptions.EnsureTrailingSeparator); + } + + internal static string MaskUsername(string path) + { + if (string.IsNullOrWhiteSpace(path)) + return path; + + var index = path.IndexOf(HomePath, StringComparer); + return index >= 0 ? path.Remove(index, HomePath.Length).Insert(index, HomeVariable) : path; + } +} \ No newline at end of file diff --git a/src/ModVerify/GameVerificationException.cs b/src/ModVerify/GameVerificationException.cs index 1096179..dba312b 100644 --- a/src/ModVerify/GameVerificationException.cs +++ b/src/ModVerify/GameVerificationException.cs @@ -7,23 +7,21 @@ namespace AET.ModVerify; public sealed class GameVerificationException : Exception { - private readonly string? _errorMessage = null; - public IReadOnlyCollection Errors { get; } private string ErrorMessage { get { - if (_errorMessage != null) - return _errorMessage; + if (field != null) + return field; var stringBuilder = new StringBuilder(); foreach (var error in Errors) stringBuilder.AppendLine($"Verification error: {error.Id}: {error.Message};"); return stringBuilder.ToString().TrimEnd(';'); } - } + } = null; /// public override string Message => ErrorMessage; diff --git a/src/ModVerify/ModVerify.csproj b/src/ModVerify/ModVerify.csproj index 427458d..62b1a65 100644 --- a/src/ModVerify/ModVerify.csproj +++ b/src/ModVerify/ModVerify.csproj @@ -25,18 +25,18 @@ - + - + - - - - + + + + @@ -51,8 +51,4 @@ - - - - diff --git a/src/ModVerify/Pipeline/GameVerifierPipelineStep.cs b/src/ModVerify/Pipeline/GameVerifierPipelineStep.cs index 6405dbd..e83cb25 100644 --- a/src/ModVerify/Pipeline/GameVerifierPipelineStep.cs +++ b/src/ModVerify/Pipeline/GameVerifierPipelineStep.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using System; using System.Threading; +using System.Threading.Tasks; using AET.ModVerify.Pipeline.Progress; namespace AET.ModVerify.Pipeline; @@ -22,18 +23,19 @@ public sealed class GameVerifierPipelineStep( public long Size => 1; - protected override void RunCore(CancellationToken token) + protected override Task RunCoreAsync(CancellationToken token) { try { Logger?.LogDebug("Running verifier '{Name}'...", GameVerifier.FriendlyName); ReportProgress(new ProgressEventArgs(0.0, "Started")); - + GameVerifier.Progress += OnVerifyProgress; GameVerifier.Verify(token); Logger?.LogDebug("Finished verifier '{Name}'", GameVerifier.FriendlyName); ReportProgress(new ProgressEventArgs(1.0, "Finished")); + return Task.CompletedTask; } finally { diff --git a/src/ModVerify/Pipeline/GameVerifyPipeline.cs b/src/ModVerify/Pipeline/GameVerifyPipeline.cs index 810651a..493aa8c 100644 --- a/src/ModVerify/Pipeline/GameVerifyPipeline.cs +++ b/src/ModVerify/Pipeline/GameVerifyPipeline.cs @@ -1,7 +1,6 @@ -using AET.ModVerify.Reporting; -using AET.ModVerify.Reporting.Settings; +using AET.ModVerify.Pipeline.Progress; +using AET.ModVerify.Reporting; using AET.ModVerify.Settings; -using AET.ModVerify.Utilities; using AET.ModVerify.Verifiers; using AnakinRaW.CommonUtilities.SimplePipeline; using AnakinRaW.CommonUtilities.SimplePipeline.Runners; @@ -12,113 +11,127 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using AET.ModVerify.Pipeline.Progress; +using AET.ModVerify.Utilities; +using Microsoft.Extensions.DependencyInjection; namespace AET.ModVerify.Pipeline; -public sealed class GameVerifyPipeline : AnakinRaW.CommonUtilities.SimplePipeline.Pipeline +public sealed class GameVerifyPipeline : StepRunnerPipelineBase { - private readonly List _verifiers = new(); - private readonly List _verificationSteps = new(); - private readonly StepRunnerBase _verifyRunner; - - private readonly IStarWarsGameEngine _gameEngine; - private readonly IGameEngineErrorCollection _engineErrors; + private readonly List _verifiers = []; + private readonly List _verificationSteps = []; + private readonly ConcurrentGameEngineErrorReporter _engineErrorReporter = new(); + private readonly VerificationTarget _verificationTarget; private readonly VerifyPipelineSettings _pipelineSettings; - private readonly GlobalVerifyReportSettings _reportSettings; - private readonly IVerifyProgressReporter _progressReporter; - - protected override bool FailFast { get; } + private readonly IGameEngineInitializationReporter? _engineInitializationReporter; + private readonly IPetroglyphStarWarsGameEngineService _gameEngineService; + private readonly ILogger? _logger; + private AggregatedVerifyProgressReporter? _aggregatedVerifyProgressReporter; public IReadOnlyCollection FilteredErrors { get; private set; } = []; - + public VerificationBaseline Baseline { get; } + public SuppressionList Suppressions { get; } + public GameVerifyPipeline( - IStarWarsGameEngine gameEngine, - IGameEngineErrorCollection engineErrors, - VerifyPipelineSettings pipelineSettings, - GlobalVerifyReportSettings reportSettings, + VerificationTarget verificationTarget, + VerifyPipelineSettings pipelineSettings, IVerifyProgressReporter progressReporter, + IGameEngineInitializationReporter? engineInitializationReporter, + VerificationBaseline baseline, + SuppressionList suppressions, IServiceProvider serviceProvider) : base(serviceProvider) { - _gameEngine = gameEngine ?? throw new ArgumentNullException(nameof(gameEngine)); - _engineErrors = engineErrors ?? throw new ArgumentNullException(nameof(gameEngine)); + Baseline = baseline ?? throw new ArgumentNullException(nameof(baseline)); + Suppressions = suppressions ?? throw new ArgumentNullException(nameof(suppressions)); + _verificationTarget = verificationTarget ?? throw new ArgumentNullException(nameof(verificationTarget)); _pipelineSettings = pipelineSettings ?? throw new ArgumentNullException(nameof(pipelineSettings)); - _reportSettings = reportSettings ?? throw new ArgumentNullException(nameof(reportSettings)); _progressReporter = progressReporter ?? throw new ArgumentNullException(nameof(progressReporter)); + _engineInitializationReporter = engineInitializationReporter; + _gameEngineService = serviceProvider.GetRequiredService(); + _logger = serviceProvider.GetService()?.CreateLogger(GetType()); - if (pipelineSettings.ParallelVerifiers is < 0 or > 64) - throw new ArgumentException("_pipelineSettings has invalid parallel worker number.", nameof(pipelineSettings)); - - if (pipelineSettings.ParallelVerifiers == 1) - _verifyRunner = new SequentialStepRunner(serviceProvider); - else - _verifyRunner = new ParallelStepRunner(pipelineSettings.ParallelVerifiers, serviceProvider); + FailFast = pipelineSettings.FailFastSettings.IsFailFast; + } - FailFast = pipelineSettings.FailFast; + protected override AsyncStepRunner CreateRunner() + { + var requestedRunnerCount = _pipelineSettings.ParallelVerifiers; + return requestedRunnerCount switch + { + < 0 or > 64 => throw new InvalidOperationException( + $"Invalid parallel worker count ({requestedRunnerCount}) specified in verifier settings."), + 1 => new SequentialStepRunner(ServiceProvider), + _ => new AsyncStepRunner(requestedRunnerCount, ServiceProvider) + }; } - protected override Task PrepareCoreAsync() + protected override async Task PrepareCoreAsync(CancellationToken token) { _verifiers.Clear(); - AddStep(new GameEngineErrorCollector(_engineErrors, _gameEngine, _pipelineSettings.GameVerifySettings, ServiceProvider)); - - foreach (var gameVerificationStep in CreateVerificationSteps(_gameEngine)) - AddStep(gameVerificationStep); - - return Task.FromResult(true); - } - - protected override async Task RunCoreAsync(CancellationToken token) - { - var aggregatedVerifyProgressReporter = new AggregatedVerifyProgressReporter(_progressReporter, _verificationSteps); + IStarWarsGameEngine gameEngine; try { - Logger?.LogInformation("Running game verifiers..."); - _verifyRunner.Error += OnError; - await _verifyRunner.RunAsync(token); + gameEngine = await _gameEngineService.InitializeAsync( + _verificationTarget.Engine, + _verificationTarget.Location, + _engineErrorReporter, + _engineInitializationReporter, + false, + CancellationToken.None).ConfigureAwait(false); } - finally + catch (Exception e) { - aggregatedVerifyProgressReporter.Dispose(); - _verifyRunner.Error -= OnError; - Logger?.LogDebug("Game verifiers finished."); + _logger?.LogError(e, "Creating game engine failed: {Message}", e.Message); + throw; } - token.ThrowIfCancellationRequested(); - - var failedSteps = _verifyRunner.ExecutedSteps.Where(p => - p.Error != null && !p.Error.IsExceptionType()).ToList(); + AddStep(new GameEngineErrorCollector(_engineErrorReporter, gameEngine, _pipelineSettings.GameVerifySettings, ServiceProvider)); - if (failedSteps.Count != 0) - throw new StepFailureException(failedSteps); + foreach (var gameVerificationStep in CreateVerificationSteps(gameEngine)) + AddStep(gameVerificationStep); + } + protected override void OnExecuteStarted() + { + Logger?.LogInformation("Running game verifiers..."); + _aggregatedVerifyProgressReporter = new AggregatedVerifyProgressReporter(_progressReporter, _verificationSteps); + _progressReporter.Report(0.0, $"Verifying {_verificationTarget.Name}...", VerifyProgress.ProgressType, default); + } + + protected override void OnExecuteCompleted() + { + Logger?.LogInformation("Game verifiers finished."); FilteredErrors = GetReportableErrors(_verifiers.SelectMany(s => s.VerifyErrors)).ToList(); + _progressReporter.Report(1.0, $"Finished Verifying {_verificationTarget.Name}", VerifyProgress.ProgressType, default); } - protected override void OnError(object sender, StepRunnerErrorEventArgs e) + protected override void OnRunnerExecutionError(object sender, StepRunnerErrorEventArgs e) { - if (FailFast && e.Exception is GameVerificationException v) + if (FailFast && e.Exception is GameVerificationException verificationException) { - // TODO: Apply globalMinSeverity - if (v.Errors.All(error => _reportSettings.Baseline.Contains(error) || _reportSettings.Suppressions.Suppresses(error))) + var minSeverity = _pipelineSettings.FailFastSettings.MinumumSeverity; + var ignoreError = verificationException.Errors + .Where(error => error.Severity >= minSeverity) + .All(error => Baseline.Contains(error) || Suppressions.Suppresses(error)); + if (ignoreError) return; } - base.OnError(sender, e); + base.OnRunnerExecutionError(sender, e); } - private IEnumerable CreateVerificationSteps(IStarWarsGameEngine database) + protected override IEnumerable GetFailedSteps(IEnumerable steps) { - return _pipelineSettings.VerifiersProvider.GetVerifiers(database, _pipelineSettings.GameVerifySettings, ServiceProvider); + return base.GetFailedSteps(steps).Where(s => s.Error is not GameVerificationException); } private void AddStep(GameVerifier verifier) { var verificationStep = new GameVerifierPipelineStep(verifier, ServiceProvider); - _verifyRunner.AddStep(verificationStep); + StepRunner.AddStep(verificationStep); _verificationSteps.Add(verificationStep); _verifiers.Add(verifier); } @@ -128,7 +141,18 @@ private IEnumerable GetReportableErrors(IEnumerable CreateVerificationSteps(IStarWarsGameEngine engine) + { + return _pipelineSettings.VerifiersProvider + .GetVerifiers(engine, _pipelineSettings.GameVerifySettings, ServiceProvider); + } + + protected override void DisposeResources() + { + base.DisposeResources(); + _aggregatedVerifyProgressReporter?.Dispose(); } } \ No newline at end of file diff --git a/src/ModVerify/Pipeline/Progress/AggregatedVerifyProgressReporter.cs b/src/ModVerify/Pipeline/Progress/AggregatedVerifyProgressReporter.cs index 361febe..cfdae25 100644 --- a/src/ModVerify/Pipeline/Progress/AggregatedVerifyProgressReporter.cs +++ b/src/ModVerify/Pipeline/Progress/AggregatedVerifyProgressReporter.cs @@ -59,6 +59,7 @@ protected override ProgressEventArgs CalculateAggregatedProg var progressInfo = new VerifyProgressInfo { TotalVerifiers = TotalStepCount, + IsDetailed = true }; return new ProgressEventArgs(totalProgress, progress.ProgressText, progressInfo); } diff --git a/src/ModVerify/Pipeline/Progress/VerifyProgressInfo.cs b/src/ModVerify/Pipeline/Progress/VerifyProgressInfo.cs index 1409239..cadeb0d 100644 --- a/src/ModVerify/Pipeline/Progress/VerifyProgressInfo.cs +++ b/src/ModVerify/Pipeline/Progress/VerifyProgressInfo.cs @@ -4,5 +4,5 @@ public struct VerifyProgressInfo { public bool IsDetailed { get; init; } - public int TotalVerifiers { get; internal set; } + public int TotalVerifiers { get; internal init; } } \ No newline at end of file diff --git a/src/ModVerify/Reporting/BaselineVerificationTarget.cs b/src/ModVerify/Reporting/BaselineVerificationTarget.cs new file mode 100644 index 0000000..6e21ddd --- /dev/null +++ b/src/ModVerify/Reporting/BaselineVerificationTarget.cs @@ -0,0 +1,23 @@ +using System.Text; +using PG.StarWarsGame.Engine; + +namespace AET.ModVerify.Reporting; + +public sealed class BaselineVerificationTarget +{ + public required GameEngineType Engine { get; init; } + public required string Name { get; init; } + public GameLocations? Location { get; init; } // Optional compared to Verification Target + public string? Version { get; init; } + public bool IsGame { get; init; } + + public override string ToString() + { + var sb = new StringBuilder($"[Name={Name};EngineType={Engine};IsGame={IsGame};"); + if (!string.IsNullOrEmpty(Version)) sb.Append($"Version={Version};"); + if (Location is not null) + sb.Append($"Location={Location};"); + sb.Append(']'); + return sb.ToString(); + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonBaselineParser.cs b/src/ModVerify/Reporting/Json/JsonBaselineParser.cs index ef2f5d1..669ef53 100644 --- a/src/ModVerify/Reporting/Json/JsonBaselineParser.cs +++ b/src/ModVerify/Reporting/Json/JsonBaselineParser.cs @@ -1,11 +1,10 @@ using System; using System.IO; using System.Text.Json; -using System.Text.Json.Nodes; namespace AET.ModVerify.Reporting.Json; -public static class JsonBaselineParser +internal static class JsonBaselineParser { public static VerificationBaseline Parse(Stream dataStream) { @@ -13,8 +12,8 @@ public static VerificationBaseline Parse(Stream dataStream) throw new ArgumentNullException(nameof(dataStream)); try { - var jsonNode = JsonNode.Parse(dataStream); - var jsonBaseline = ParseCore(jsonNode); + var jsonNode = JsonDocument.Parse(dataStream); + var jsonBaseline = EvaluateAndDeserialize(jsonNode); if (jsonBaseline is null) throw new InvalidBaselineException($"Unable to parse input from stream to {nameof(VerificationBaseline)}. Unknown Error!"); @@ -27,12 +26,11 @@ public static VerificationBaseline Parse(Stream dataStream) } } - private static JsonVerificationBaseline? ParseCore(JsonNode? jsonData) + private static JsonVerificationBaseline? EvaluateAndDeserialize(JsonDocument? json) { - if (jsonData is null) + if (json is null) return null; - - JsonBaselineSchema.Evaluate(jsonData); - return jsonData.Deserialize(); + JsonBaselineSchema.Evaluate(json.RootElement); + return json.Deserialize(); } } \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonBaselineSchema.cs b/src/ModVerify/Reporting/Json/JsonBaselineSchema.cs index 7c8b02a..12e3705 100644 --- a/src/ModVerify/Reporting/Json/JsonBaselineSchema.cs +++ b/src/ModVerify/Reporting/Json/JsonBaselineSchema.cs @@ -3,8 +3,9 @@ using System.Diagnostics; using System.Linq; using System.Text; -using System.Text.Json.Nodes; +using System.Text.Json; using Json.Schema; +using Json.Schema.Keywords; namespace AET.ModVerify.Reporting.Json; @@ -12,18 +13,20 @@ public static class JsonBaselineSchema { private static readonly JsonSchema Schema; private static readonly EvaluationOptions EvaluationOptions; - + private static readonly BuildOptions BuildOptions; + static JsonBaselineSchema() { - var evalvOptions = new EvaluationOptions + BuildOptions = new BuildOptions { - EvaluateAs = SpecVersion.Draft202012, - OutputFormat = OutputFormat.Hierarchical, - AllowReferencesIntoUnknownKeywords = false + Dialect = Dialect.Draft202012 }; Schema = GetCurrentSchema(); - EvaluationOptions = evalvOptions; + EvaluationOptions = new EvaluationOptions + { + OutputFormat = OutputFormat.Hierarchical + }; } /// @@ -31,11 +34,8 @@ static JsonBaselineSchema() /// /// The JSON node to evaluate. /// is not valid against the baseline JSON schema. - /// is . - public static void Evaluate(JsonNode json) + public static void Evaluate(JsonElement json) { - if (json == null) - throw new ArgumentNullException(nameof(json)); var result = Schema.Evaluate(json, EvaluationOptions); ThrowOnValidationError(result); } @@ -58,13 +58,17 @@ private static void ThrowOnValidationError(EvaluationResults result) private static KeyValuePair? GetFirstError(EvaluationResults result) { - if (result.HasErrors) - return result.Errors!.First(); - foreach (var child in result.Details) + if (result.Errors is not null) + return result.Errors.First(); + + if (result.Details is not null) { - var error = GetFirstError(child); - if (error is not null) - return error; + foreach (var child in result.Details) + { + var error = GetFirstError(child); + if (error is not null) + return error; + } } return null; } @@ -75,10 +79,12 @@ private static JsonSchema GetCurrentSchema() .Assembly.GetManifestResourceStream($"AET.ModVerify.Resources.Schemas.{GetVersionedPath()}.baseline.json"); Debug.Assert(resourceStream is not null); - var schema = JsonSchema.FromStream(resourceStream!).GetAwaiter().GetResult(); + var json = JsonDocument.Parse(resourceStream!).RootElement; + var schema = JsonSchema.Build(json, BuildOptions); + - var id = schema.GetId(); - if (id is null || !UriContainsVersion(id, VerificationBaseline.LatestVersionString)) + if (schema.Root.Keywords.FirstOrDefault(x => x.Handler is IdKeyword)?.Value is not Uri id + || !UriContainsVersion(id, VerificationBaseline.LatestVersionString)) throw new InvalidOperationException("Internal error: The embedded schema version does not match the expected baseline version!"); return schema; diff --git a/src/ModVerify/Reporting/Json/JsonGameLocation.cs b/src/ModVerify/Reporting/Json/JsonGameLocation.cs new file mode 100644 index 0000000..bd51993 --- /dev/null +++ b/src/ModVerify/Reporting/Json/JsonGameLocation.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; +using PG.StarWarsGame.Engine; + +namespace AET.ModVerify.Reporting.Json; + +internal class JsonGameLocation +{ + [JsonPropertyName("modPaths")] + public IReadOnlyList ModPaths { get; } + + [JsonPropertyName("gamePath")] + public string GamePath { get; } + + [JsonPropertyName("fallbackPaths")] + public IReadOnlyList FallbackPaths { get; } + + [JsonConstructor] + private JsonGameLocation(IReadOnlyList modPaths, string gamePath, IReadOnlyList fallbackPaths) + { + ModPaths = modPaths; + GamePath = gamePath; + FallbackPaths = fallbackPaths; + } + + public JsonGameLocation(GameLocations location) + { + ModPaths = location.ModPaths.ToArray(); + GamePath = location.GamePath; + FallbackPaths = location.FallbackPaths.ToArray(); + } + + public static GameLocations? ToLocation(JsonGameLocation? jsonLocation) + { + return jsonLocation is null + ? null + : new GameLocations(jsonLocation.ModPaths, jsonLocation.GamePath, jsonLocation.FallbackPaths); + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonVerificationBaseline.cs b/src/ModVerify/Reporting/Json/JsonVerificationBaseline.cs index 4688c98..0d9a1b7 100644 --- a/src/ModVerify/Reporting/Json/JsonVerificationBaseline.cs +++ b/src/ModVerify/Reporting/Json/JsonVerificationBaseline.cs @@ -10,6 +10,10 @@ internal class JsonVerificationBaseline [JsonPropertyName("version")] public Version? Version { get; } + [JsonPropertyName("target")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public JsonVerificationTarget? Target { get; } + [JsonPropertyName("minSeverity")] [JsonConverter(typeof(JsonStringEnumConverter))] public VerificationSeverity MinimumSeverity { get; } @@ -22,11 +26,17 @@ public JsonVerificationBaseline(VerificationBaseline baseline) Errors = baseline.Select(x => new JsonVerificationError(x)); Version = baseline.Version; MinimumSeverity = baseline.MinimumSeverity; + Target = baseline.Target is not null ? new JsonVerificationTarget(baseline.Target) : null; } [JsonConstructor] - private JsonVerificationBaseline(Version version, VerificationSeverity minimumSeverity, IEnumerable errors) + private JsonVerificationBaseline( + JsonVerificationTarget target, + Version version, + VerificationSeverity minimumSeverity, + IEnumerable errors) { + Target = target; Errors = errors; Version = version; MinimumSeverity = minimumSeverity; diff --git a/src/ModVerify/Reporting/Json/JsonVerificationTarget.cs b/src/ModVerify/Reporting/Json/JsonVerificationTarget.cs new file mode 100644 index 0000000..9495456 --- /dev/null +++ b/src/ModVerify/Reporting/Json/JsonVerificationTarget.cs @@ -0,0 +1,63 @@ +using System.Text.Json.Serialization; +using PG.StarWarsGame.Engine; + +namespace AET.ModVerify.Reporting.Json; + +internal class JsonVerificationTarget +{ + [JsonPropertyName("name")] + public string Name { get; } + + [JsonPropertyName("engine")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public GameEngineType Engine { get; } + + [JsonPropertyName("isGame")] + public bool IsGame { get; } + + [JsonPropertyName("version")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Version{ get; } + + [JsonPropertyName("location")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public JsonGameLocation? Location { get; } + + [JsonConstructor] + private JsonVerificationTarget( + string name, + string? version, + JsonGameLocation? location, + GameEngineType engine, + bool isGame) + { + Name = name; + Version = version; + Engine = engine; + Location = location; + IsGame = isGame; + } + + public JsonVerificationTarget(BaselineVerificationTarget target) + { + Name = target.Name; + Version = target.Version; + Engine = target.Engine; + Location = target.Location is null ? null : new JsonGameLocation(target.Location); + IsGame = target.IsGame; + } + + public static BaselineVerificationTarget? ToTarget(JsonVerificationTarget? jsonTarget) + { + if (jsonTarget is null) + return null; + return new BaselineVerificationTarget + { + Engine = jsonTarget.Engine, + Name = jsonTarget.Name, + Location = JsonGameLocation.ToLocation(jsonTarget.Location), + Version = jsonTarget.Version, + IsGame = jsonTarget.IsGame + }; + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/ConsoleReporter.cs b/src/ModVerify/Reporting/Reporters/ConsoleReporter.cs index 7ee435d..dbee50d 100644 --- a/src/ModVerify/Reporting/Reporters/ConsoleReporter.cs +++ b/src/ModVerify/Reporting/Reporters/ConsoleReporter.cs @@ -7,10 +7,10 @@ namespace AET.ModVerify.Reporting.Reporters; internal class ConsoleReporter( - VerifyReportSettings settings, + ReporterSettings settings, bool summaryOnly, IServiceProvider serviceProvider) : - ReporterBase(settings, serviceProvider) + ReporterBase(settings, serviceProvider) { public override Task ReportAsync(IReadOnlyCollection errors) { diff --git a/src/ModVerify/Reporting/Reporters/Engine/EngineErrorReporterBase.cs b/src/ModVerify/Reporting/Reporters/Engine/EngineErrorReporterBase.cs index 5ecbb6f..455212d 100644 --- a/src/ModVerify/Reporting/Reporters/Engine/EngineErrorReporterBase.cs +++ b/src/ModVerify/Reporting/Reporters/Engine/EngineErrorReporterBase.cs @@ -38,12 +38,13 @@ public ErrorData(string identifier, string message, IEnumerable context, ThrowHelper.ThrowIfNullOrEmpty(message); Identifier = identifier; Message = message; - Context = context; + Context = context ?? throw new ArgumentNullException(nameof(context)); Asset = asset; Severity = severity; } - public ErrorData(string identifier, string message, string asset, VerificationSeverity severity) : this(identifier, message, [], asset, severity) + public ErrorData(string identifier, string message, string asset, VerificationSeverity severity) + : this(identifier, message, [], asset, severity) { } } diff --git a/src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs b/src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs index cb6b258..6c5fb17 100644 --- a/src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs +++ b/src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs @@ -14,18 +14,10 @@ internal sealed class GameAssertErrorReporter(IGameRepository gameRepository, IS protected override ErrorData CreateError(EngineAssert assert) { - // TODO: Why is context not used atm? var context = new List(); - - if (assert.Value is not null) - context.Add($"value='{assert.Value}'"); - if (assert.Context is not null) - context.Add($"context='{assert.Context}'"); - - // The location is the only identifiable thing of an assert. 'Value' might be null, thus we cannot use it. - var asset = GetLocation(assert); - - return new ErrorData(GetIdFromError(assert.Kind), assert.Message, asset, VerificationSeverity.Warning); + context.AddRange(assert.Context); + context.Add($"location='{GetLocation(assert)}'"); + return new ErrorData(GetIdFromError(assert.Kind), assert.Message, context, assert.Value, VerificationSeverity.Warning); } private static string GetLocation(EngineAssert assert) diff --git a/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs b/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs index 941a9d5..348fbb1 100644 --- a/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs +++ b/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs @@ -12,7 +12,6 @@ internal class JsonReporter(JsonReporterSettings settings, IServiceProvider serv { public const string FileName = "VerificationResult.json"; - public override async Task ReportAsync(IReadOnlyCollection errors) { var report = new JsonVerificationReport(errors.Select(x => new JsonVerificationError(x))); diff --git a/src/ModVerify/Reporting/Reporters/ReporterBase.cs b/src/ModVerify/Reporting/Reporters/ReporterBase.cs index fd86119..df444e3 100644 --- a/src/ModVerify/Reporting/Reporters/ReporterBase.cs +++ b/src/ModVerify/Reporting/Reporters/ReporterBase.cs @@ -6,7 +6,7 @@ namespace AET.ModVerify.Reporting.Reporters; -public abstract class ReporterBase(T settings, IServiceProvider serviceProvider) : IVerificationReporter where T : VerifyReportSettings +public abstract class ReporterBase(T settings, IServiceProvider serviceProvider) : IVerificationReporter where T : ReporterSettings { protected IServiceProvider ServiceProvider { get; } = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); diff --git a/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs b/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs index 7de81eb..b8906ac 100644 --- a/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs +++ b/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs @@ -11,7 +11,7 @@ public static class VerificationReportersExtensions { public IServiceCollection RegisterJsonReporter() { - return RegisterJsonReporter(serviceCollection, new JsonReporterSettings + return serviceCollection.RegisterJsonReporter(new JsonReporterSettings { OutputDirectory = "." }); @@ -19,7 +19,7 @@ public IServiceCollection RegisterJsonReporter() public IServiceCollection RegisterTextFileReporter() { - return RegisterTextFileReporter(serviceCollection, new TextFileReporterSettings + return serviceCollection.RegisterTextFileReporter(new TextFileReporterSettings { OutputDirectory = "." }); @@ -27,7 +27,7 @@ public IServiceCollection RegisterTextFileReporter() public IServiceCollection RegisterConsoleReporter(bool summaryOnly = false) { - return RegisterConsoleReporter(serviceCollection, new VerifyReportSettings + return serviceCollection.RegisterConsoleReporter(new ReporterSettings { MinimumReportSeverity = VerificationSeverity.Error }, summaryOnly); @@ -43,7 +43,7 @@ public IServiceCollection RegisterTextFileReporter(TextFileReporterSettings sett return serviceCollection.AddSingleton(sp => new TextFileReporter(settings, sp)); } - public IServiceCollection RegisterConsoleReporter(VerifyReportSettings settings, + public IServiceCollection RegisterConsoleReporter(ReporterSettings settings, bool summaryOnly = false) { return serviceCollection.AddSingleton(sp => new ConsoleReporter(settings, summaryOnly, sp)); diff --git a/src/ModVerify/Reporting/Settings/FileBasedReporterSettings.cs b/src/ModVerify/Reporting/Settings/FileBasedReporterSettings.cs index c6233a1..759a6ab 100644 --- a/src/ModVerify/Reporting/Settings/FileBasedReporterSettings.cs +++ b/src/ModVerify/Reporting/Settings/FileBasedReporterSettings.cs @@ -2,13 +2,11 @@ namespace AET.ModVerify.Reporting.Settings; -public record FileBasedReporterSettings : VerifyReportSettings +public record FileBasedReporterSettings : ReporterSettings { - private readonly string _outputDirectory = Environment.CurrentDirectory; - public string OutputDirectory { - get => _outputDirectory; - init => _outputDirectory = string.IsNullOrEmpty(value) ? Environment.CurrentDirectory : value; - } + get; + init => field = string.IsNullOrEmpty(value) ? Environment.CurrentDirectory : value; + } = Environment.CurrentDirectory; } \ No newline at end of file diff --git a/src/ModVerify/Reporting/Settings/GlobalVerifyReportSettings.cs b/src/ModVerify/Reporting/Settings/GlobalVerifyReportSettings.cs index b376fe3..5a51436 100644 --- a/src/ModVerify/Reporting/Settings/GlobalVerifyReportSettings.cs +++ b/src/ModVerify/Reporting/Settings/GlobalVerifyReportSettings.cs @@ -1,8 +1,10 @@ namespace AET.ModVerify.Reporting.Settings; -public record GlobalVerifyReportSettings : VerifyReportSettings -{ - public VerificationBaseline Baseline { get; init; } = VerificationBaseline.Empty; +//public record GlobalVerifyReportSettings +//{ +// //public VerificationSeverity MinimumReportSeverity { get; init; } = VerificationSeverity.Information; - public SuppressionList Suppressions { get; init; } = SuppressionList.Empty; -} \ No newline at end of file +// public VerificationBaseline Baseline { get; init; } = VerificationBaseline.Empty; + +// public SuppressionList Suppressions { get; init; } = SuppressionList.Empty; +//} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Settings/VerifyReportSettings.cs b/src/ModVerify/Reporting/Settings/ReporterSettings.cs similarity index 81% rename from src/ModVerify/Reporting/Settings/VerifyReportSettings.cs rename to src/ModVerify/Reporting/Settings/ReporterSettings.cs index 1289822..ef33857 100644 --- a/src/ModVerify/Reporting/Settings/VerifyReportSettings.cs +++ b/src/ModVerify/Reporting/Settings/ReporterSettings.cs @@ -1,6 +1,6 @@ namespace AET.ModVerify.Reporting.Settings; -public record VerifyReportSettings +public record ReporterSettings { public VerificationSeverity MinimumReportSeverity { get; init; } = VerificationSeverity.Information; } \ No newline at end of file diff --git a/src/ModVerify/Reporting/VerificationBaseline.cs b/src/ModVerify/Reporting/VerificationBaseline.cs index c37539b..3f9c274 100644 --- a/src/ModVerify/Reporting/VerificationBaseline.cs +++ b/src/ModVerify/Reporting/VerificationBaseline.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using System.Text.Json; using System.Threading.Tasks; using AET.ModVerify.Reporting.Json; @@ -11,13 +12,15 @@ namespace AET.ModVerify.Reporting; public sealed class VerificationBaseline : IReadOnlyCollection { - public static readonly Version LatestVersion = new(2, 0); + public static readonly Version LatestVersion = new(2, 1); public static readonly string LatestVersionString = LatestVersion.ToString(2); - public static readonly VerificationBaseline Empty = new(VerificationSeverity.Information, []); + public static readonly VerificationBaseline Empty = new(VerificationSeverity.Information, [], null); private readonly HashSet _errors; + public BaselineVerificationTarget? Target { get; } + public Version? Version { get; } public VerificationSeverity MinimumSeverity { get; } @@ -25,18 +28,22 @@ public sealed class VerificationBaseline : IReadOnlyCollection public int Count => _errors.Count; + public bool IsEmpty => Count == 0; + internal VerificationBaseline(JsonVerificationBaseline baseline) { _errors = [..baseline.Errors.Select(x => new VerificationError(x))]; Version = baseline.Version; MinimumSeverity = baseline.MinimumSeverity; + Target = JsonVerificationTarget.ToTarget(baseline.Target); } - public VerificationBaseline(VerificationSeverity minimumSeverity, IEnumerable errors) + public VerificationBaseline(VerificationSeverity minimumSeverity, IEnumerable errors, BaselineVerificationTarget? target) { _errors = [..errors]; Version = LatestVersion; MinimumSeverity = minimumSeverity; + Target = target; } public bool Contains(VerificationError error) @@ -77,6 +84,10 @@ IEnumerator IEnumerable.GetEnumerator() public override string ToString() { - return $"Baseline [Version={Version}, MinSeverity={MinimumSeverity}, NumErrors={Count}]"; + var sb = new StringBuilder($"Baseline [Version={Version}, MinSeverity={MinimumSeverity}, NumErrors={Count}"); + if (Target is not null) + sb.Append($", Target={Target}"); + sb.Append(']'); + return sb.ToString(); } } \ No newline at end of file diff --git a/src/ModVerify/Reporting/VerificationError.cs b/src/ModVerify/Reporting/VerificationError.cs index 4174f32..538a8fd 100644 --- a/src/ModVerify/Reporting/VerificationError.cs +++ b/src/ModVerify/Reporting/VerificationError.cs @@ -117,6 +117,7 @@ public override int GetHashCode() public override string ToString() { - return $"[{Severity}] [{string.Join(" --> ", VerifierChain)}] {Id}: Message={Message}; Asset='{Asset}'; Context=[{string.Join(",", ContextEntries)}];"; + return $"[{Severity}] [{string.Join(" --> ", VerifierChain)}] " + + $"{Id}: Message={Message}; Asset='{Asset}'; Context=[{string.Join(",", ContextEntries)}];"; } } \ No newline at end of file diff --git a/src/ModVerify/Resources/Schemas/2.0/baseline.json b/src/ModVerify/Resources/Schemas/2.1/baseline.json similarity index 55% rename from src/ModVerify/Resources/Schemas/2.0/baseline.json rename to src/ModVerify/Resources/Schemas/2.1/baseline.json index 2520a58..da37c4d 100644 --- a/src/ModVerify/Resources/Schemas/2.0/baseline.json +++ b/src/ModVerify/Resources/Schemas/2.1/baseline.json @@ -1,9 +1,60 @@ { - "$id": "https://AlamoEngine-Tools.github.io/schemas/mod-verify/2.0/baseline", + "$id": "https://AlamoEngine-Tools.github.io/schemas/mod-verify/2.1/baseline", "$schema": "https://json-schema.org/draft/2020-12/schema", "description": "Represents a baseline for AET ModVerify", "type": "object", "$defs": { + "location": { + "type": "object", + "properties": { + "modPaths": { + "type": "array", + "items": { + "type": "string" + } + }, + "gamePath": { + "type": "string" + }, + "fallbackPaths": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "modPaths", + "gamePath", + "fallbackPaths" + ], + "additionalProperties": false + }, + "target": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "engine": { + "enum": [ "Eaw", "Foc" ] + }, + "location": { + "$ref": "#/$defs/location" + }, + "isGame": { + "type": "boolean" + } + }, + "required": [ + "name", + "engine" + ], + "additionalProperties": false + }, "severity": { "enum": [ "Information", "Warning", "Error", "Critical" ] }, @@ -48,7 +99,7 @@ }, "properties": { "version": { - "const": "2.0" + "const": "2.1" }, "minSeverity": { "$ref": "#/$defs/severity" @@ -59,6 +110,9 @@ "$ref": "#/$defs/error" }, "additionalItems": false + }, + "target": { + "$ref": "#/$defs/target" } }, "required": [ diff --git a/src/ModVerify/Settings/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/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 3b77732..f57b605 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameLocations.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameLocations.cs @@ -1,7 +1,8 @@ -using System; +using AnakinRaW.CommonUtilities; +using System; using System.Collections.Generic; using System.Linq; -using AnakinRaW.CommonUtilities; +using System.Text; namespace PG.StarWarsGame.Engine; @@ -27,13 +28,13 @@ public GameLocations(string modPath, string gamePath, string fallbackGamePath) : ThrowHelper.ThrowIfNullOrEmpty(modPath); } - public GameLocations(IList modPaths, string gamePath, string fallbackGamePath) : this(modPaths, - gamePath, [fallbackGamePath]) + public GameLocations(IReadOnlyList modPaths, string gamePath, string fallbackGamePath) + : this(modPaths, gamePath, [fallbackGamePath]) { ThrowHelper.ThrowIfNullOrEmpty(fallbackGamePath); } - public GameLocations(IList modPaths, string gamePath, IList fallbackPaths) + public GameLocations(IReadOnlyList modPaths, string gamePath, IReadOnlyList fallbackPaths) { if (modPaths == null) throw new ArgumentNullException(nameof(modPaths)); @@ -48,4 +49,19 @@ public GameLocations(IList modPaths, string gamePath, IList fall ? ModPaths[0] : GamePath; } + + public override string ToString() + { + var sb = new StringBuilder(); + + sb.AppendLine("GameLocation=["); + if (ModPaths.Count > 0) + sb.AppendLine($"Mods=[{string.Join(";", ModPaths)}];"); + sb.AppendLine($"Game=[{GamePath}];"); + if (FallbackPaths.Count > 0) + sb.AppendLine($"Fallbacks=[{string.Join(";", FallbackPaths)}];"); + sb.AppendLine("]"); + + return sb.ToString(); + } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameManagerBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameManagerBase.cs index 130fb50..607121d 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameManagerBase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameManagerBase.cs @@ -6,7 +6,6 @@ using AnakinRaW.CommonUtilities.Collections; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using PG.Commons.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.ErrorReporting; using PG.StarWarsGame.Engine.IO.Repositories; @@ -16,13 +15,13 @@ namespace PG.StarWarsGame.Engine; internal abstract class GameManagerBase(GameRepository repository, GameEngineErrorReporterWrapper errorReporter, IServiceProvider serviceProvider) : GameManagerBase(repository, errorReporter, serviceProvider), IGameManager { - protected readonly ValueListDictionary NamedEntries = new(); + protected readonly FrugalValueListDictionary NamedEntries = new(); public ICollection Entries => NamedEntries.Values; public ICollection EntryKeys => NamedEntries.Keys; - public ReadOnlyFrugalList GetEntries(Crc32 key) + public ImmutableFrugalList GetEntries(Crc32 key) { return NamedEntries.GetValues(key); } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/XmlComponentTextureData.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/XmlComponentTextureData.cs index 2ed95ce..f102457 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/XmlComponentTextureData.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/XmlComponentTextureData.cs @@ -1,14 +1,14 @@ using System; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.StarWarsGame.Engine.Xml; using PG.StarWarsGame.Files.XML; namespace PG.StarWarsGame.Engine.GuiDialog.Xml; -public class XmlComponentTextureData(string componentId, IReadOnlyValueListDictionary textures, XmlLocationInfo location) +public class XmlComponentTextureData(string componentId, IReadOnlyFrugalValueListDictionary textures, XmlLocationInfo location) : XmlObject(location) { public string Component { get; } = componentId ?? throw new ArgumentNullException(componentId); - public IReadOnlyValueListDictionary Textures { get; } = textures ?? throw new ArgumentNullException(nameof(textures)); + public IReadOnlyFrugalValueListDictionary Textures { get; } = textures ?? throw new ArgumentNullException(nameof(textures)); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IGameEngineInitializationReporter.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IGameEngineInitializationReporter.cs new file mode 100644 index 0000000..2ff43bd --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IGameEngineInitializationReporter.cs @@ -0,0 +1,10 @@ +namespace PG.StarWarsGame.Engine; + +public interface IGameEngineInitializationReporter +{ + void ReportProgress(string message); + + void ReportStarted(); + + void ReportFinished(); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IGameManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IGameManager.cs index b9f60a7..750f00c 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IGameManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IGameManager.cs @@ -10,5 +10,5 @@ public interface IGameManager ICollection EntryKeys { get; } - ReadOnlyFrugalList GetEntries(Crc32 key); + ImmutableFrugalList GetEntries(Crc32 key); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs index e952db8..7abd1b9 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs @@ -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 d1451b0..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; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IPetroglyphStarWarsGameEngineService.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IPetroglyphStarWarsGameEngineService.cs index 314ce06..80aaa61 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IPetroglyphStarWarsGameEngineService.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IPetroglyphStarWarsGameEngineService.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading; +using System.Threading; using System.Threading.Tasks; using PG.StarWarsGame.Engine.ErrorReporting; @@ -11,7 +10,7 @@ public Task InitializeAsync( GameEngineType engineType, GameLocations gameLocations, IGameEngineErrorReporter? errorReporter = null, - IProgress? initProgress = null, + IGameEngineInitializationReporter? initReporter = null, bool cancelOnInitializationError = false, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj index e54e6f0..47b3a7f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj @@ -23,10 +23,10 @@ - - - - + + + + @@ -39,7 +39,4 @@ - - - \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs index 8ce1a5a..02a8d9f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs @@ -27,7 +27,7 @@ public async Task InitializeAsync( GameEngineType engineType, GameLocations gameLocations, IGameEngineErrorReporter? errorReporter = null, - IProgress? progress = null, + IGameEngineInitializationReporter? initReporter = null, bool cancelOnInitializationError = false, CancellationToken cancellationToken = default) @@ -39,7 +39,7 @@ public async Task InitializeAsync( try { - return await InitializeEngine(engineType, gameLocations, errorListenerWrapper, progress, cts.Token) + return await InitializeEngineAsync(engineType, gameLocations, errorListenerWrapper, initReporter, cts.Token) .ConfigureAwait(false); } finally @@ -56,16 +56,17 @@ void OnInitializationError(object sender, InitializationError e) } } - private async Task InitializeEngine( + private async Task InitializeEngineAsync( GameEngineType engineType, GameLocations gameLocations, GameEngineErrorReporterWrapper errorReporter, - IProgress? progress, + IGameEngineInitializationReporter? initReporter, CancellationToken token) { try { _logger?.LogInformation("Initializing game engine for type '{GameEngineType}'.", engineType); + initReporter?.ReportStarted(); var repoFactory = _serviceProvider.GetRequiredService(); var repository = repoFactory.Create(engineType, gameLocations, errorReporter); @@ -73,7 +74,7 @@ private async Task InitializeEngine( var pgRender = new PGRender(repository, errorReporter, serviceProvider); var gameConstants = new GameConstants.GameConstants(repository, errorReporter, serviceProvider); - progress?.Report("Initializing GameConstants"); + initReporter?.ReportProgress("Initializing GameConstants"); await gameConstants.InitializeAsync(token); // AudioConstants @@ -81,23 +82,23 @@ private async Task InitializeEngine( // MousePointer var fontManger = new FontManager(repository, errorReporter, serviceProvider); - progress?.Report("Initializing FontManager"); + initReporter?.ReportProgress("Initializing FontManager"); await fontManger.InitializeAsync(token); var guiDialogs = new GuiDialogGameManager(repository, errorReporter, serviceProvider); - progress?.Report("Initializing GUIDialogManager"); + initReporter?.ReportProgress("Initializing GUIDialogManager"); await guiDialogs.InitializeAsync(token); var sfxGameManager = new SfxEventGameManager(repository, errorReporter, serviceProvider); - progress?.Report("Initializing SFXManager"); + initReporter?.ReportProgress("Initializing SFXManager"); await sfxGameManager.InitializeAsync(token); var commandBarManager = new CommandBarGameManager(repository, pgRender, gameConstants, fontManger, errorReporter, serviceProvider); - progress?.Report("Initializing CommandBar"); + initReporter?.ReportProgress("Initializing CommandBar"); await commandBarManager.InitializeAsync(token); var gameObjetTypeManager = new GameObjectTypeGameManager(repository, errorReporter, serviceProvider); - progress?.Report("Initializing GameObjectTypeManager"); + initReporter?.ReportProgress("Initializing GameObjectTypeManager"); await gameObjetTypeManager.InitializeAsync(token); token.ThrowIfCancellationRequested(); @@ -120,6 +121,7 @@ private async Task InitializeEngine( } finally { + initReporter?.ReportFinished(); _logger?.LogDebug("Finished initializing game database."); } } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/AnimationCollection.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/AnimationCollection.cs index 03916fe..dfd43aa 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/AnimationCollection.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/AnimationCollection.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using AnakinRaW.CommonUtilities; using AnakinRaW.CommonUtilities.Collections; -using PG.Commons.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Files.ALO.Files.Animations; @@ -13,10 +12,10 @@ public sealed class AnimationCollection : DisposableObject, IEnumerable _animations = new(); - private readonly ValueListDictionary _animationCrc = new(); + private readonly FrugalValueListDictionary _animations = new(); + private readonly FrugalValueListDictionary _animationCrc = new(); - public int Cout => _animations.Count; + public int Cout => _animations.ValueCount; public Crc32 GetAnimationCrc(ModelAnimationType type, int subIndex) { @@ -28,12 +27,12 @@ public Crc32 GetAnimationCrc(ModelAnimationType type, int subIndex) return checksumsForType[subIndex]; } - public ReadOnlyFrugalList GetAnimations(ModelAnimationType type) + public ImmutableFrugalList GetAnimations(ModelAnimationType type) { return _animations.GetValues(type); } - public bool TryGetAnimations(ModelAnimationType type, out ReadOnlyFrugalList animations) + public bool TryGetAnimations(ModelAnimationType type, out ImmutableFrugalList animations) { return _animations.TryGetValues(type, out animations); } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/WindowsFontManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/WindowsFontManager.cs index f11374e..fe8f546 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/WindowsFontManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/WindowsFontManager.cs @@ -28,8 +28,8 @@ public IEnumerable GetFontFamilies() return fonts; } - static IEnumerable<(Gdi32.ENUMLOGFONTEXDV lpelfe, Gdi32.ENUMTEXTMETRIC lpntme, Gdi32.FontType FontType)> GetFonts(Gdi32.SafeHDC hdc) + static IEnumerable<(Gdi32.ENUMLOGFONTEXDV lpelfe, Gdi32.ENUMTEXTMETRIC _, Gdi32.FontType __)> GetFonts(Gdi32.SafeHDC hdc) { - return Gdi32.EnumFontFamiliesEx(hdc, CharacterSet.DEFAULT_CHARSET); + return Gdi32.EnumFontFamiliesEx(hdc, lfCharSet: CharacterSet.DEFAULT_CHARSET); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/CommandBarComponentParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/CommandBarComponentParser.cs index 1e20a8b..297b474 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/CommandBarComponentParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/CommandBarComponentParser.cs @@ -1,7 +1,7 @@ using System; using System.Collections.ObjectModel; using System.Xml.Linq; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.CommandBar.Xml; using PG.StarWarsGame.Engine.Xml.Tags; @@ -12,7 +12,7 @@ namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; public sealed class CommandBarComponentParser( - IReadOnlyValueListDictionary parsedElements, + IReadOnlyFrugalValueListDictionary parsedElements, IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : XmlObjectParser(parsedElements, serviceProvider, errorReporter) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs index 4f445a7..5cac3e2 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs @@ -1,6 +1,6 @@ using System; using System.Xml.Linq; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.GameObjects; using PG.StarWarsGame.Files.XML; @@ -26,7 +26,7 @@ public static class GameObjectXmlTags } public sealed class GameObjectParser( - IReadOnlyValueListDictionary parsedElements, + IReadOnlyFrugalValueListDictionary parsedElements, IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : XmlObjectParser(parsedElements, serviceProvider, errorReporter) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs index 4e3a5cb..bc3439f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs @@ -1,7 +1,7 @@ using System; using System.Collections.ObjectModel; using System.Xml.Linq; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.Audio.Sfx; using PG.StarWarsGame.Engine.Xml.Tags; @@ -12,7 +12,7 @@ namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; public sealed class SfxEventParser( - IReadOnlyValueListDictionary parsedElements, + IReadOnlyFrugalValueListDictionary parsedElements, IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : XmlObjectParser(parsedElements, serviceProvider, errorReporter) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/CommandBarComponentFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/CommandBarComponentFileParser.cs index a32ff09..763d244 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/CommandBarComponentFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/CommandBarComponentFileParser.cs @@ -1,6 +1,6 @@ using System; using System.Xml.Linq; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.CommandBar.Xml; using PG.StarWarsGame.Engine.Xml.Parsers.Data; @@ -12,7 +12,7 @@ namespace PG.StarWarsGame.Engine.Xml.Parsers.File; internal class CommandBarComponentFileParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : PetroglyphXmlFileContainerParser(serviceProvider, errorReporter) { - protected override void Parse(XElement element, IValueListDictionary parsedElements, string fileName) + protected override void Parse(XElement element, IFrugalValueListDictionary parsedElements, string fileName) { var parser = new CommandBarComponentParser(parsedElements, ServiceProvider, ErrorReporter); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileParser.cs index d2abfed..ffeafff 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileParser.cs @@ -1,6 +1,6 @@ using System; using System.Xml.Linq; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.GameObjects; using PG.StarWarsGame.Engine.Xml.Parsers.Data; @@ -12,7 +12,7 @@ namespace PG.StarWarsGame.Engine.Xml.Parsers.File; internal class GameObjectFileParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : PetroglyphXmlFileContainerParser(serviceProvider, errorReporter) { - protected override void Parse(XElement element, IValueListDictionary parsedElements, string fileName) + protected override void Parse(XElement element, IFrugalValueListDictionary parsedElements, string fileName) { var parser = new GameObjectParser(parsedElements, ServiceProvider, ErrorReporter); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GuiDialogParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GuiDialogParser.cs index 7c23dd6..851aa9f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GuiDialogParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GuiDialogParser.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Xml.Linq; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.StarWarsGame.Engine.GuiDialog.Xml; using PG.StarWarsGame.Files.XML; using PG.StarWarsGame.Files.XML.ErrorHandling; @@ -49,7 +49,7 @@ private GuiDialogsXmlTextureData ParseTextures(XElement? element, string fileNam private XmlComponentTextureData ParseTexture(XElement texture) { var componentId = GetTagName(texture); - var textures = new ValueListDictionary(); + var textures = new FrugalValueListDictionary(); foreach (var entry in texture.Elements()) textures.Add(entry.Name.ToString(), PetroglyphXmlStringParser.Instance.Parse(entry)); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs index f40dd1a..841d805 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs @@ -1,6 +1,6 @@ using System; using System.Xml.Linq; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.Audio.Sfx; using PG.StarWarsGame.Engine.Xml.Parsers.Data; @@ -12,7 +12,7 @@ namespace PG.StarWarsGame.Engine.Xml.Parsers.File; internal class SfxEventFileParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : PetroglyphXmlFileContainerParser(serviceProvider, errorReporter) { - protected override void Parse(XElement element, IValueListDictionary parsedElements, string fileName) + protected override void Parse(XElement element, IFrugalValueListDictionary parsedElements, string fileName) { var parser = new SfxEventParser(parsedElements, ServiceProvider, ErrorReporter); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs index 0c911e2..00f4f61 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs @@ -1,9 +1,9 @@ using System; using System.Linq; using System.Xml; +using AnakinRaW.CommonUtilities.Collections; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using PG.Commons.Collections; using PG.Commons.Hashing; using PG.Commons.Services; using PG.StarWarsGame.Engine.IO; @@ -34,7 +34,7 @@ public void ParseEntriesFromFileListXml( string xmlFile, IGameRepository gameRepository, string lookupPath, - ValueListDictionary entries, + FrugalValueListDictionary entries, Action? onFileParseAction = null) where T : notnull { Logger.LogDebug("Parsing container data '{XmlFile}'", xmlFile); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs index fead02d..1565e9e 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs @@ -1,7 +1,7 @@ using System; using System.Xml.Linq; +using AnakinRaW.CommonUtilities.Collections; using Microsoft.Extensions.DependencyInjection; -using PG.Commons.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Files.XML.ErrorHandling; using PG.StarWarsGame.Files.XML.Parsers; @@ -9,7 +9,7 @@ namespace PG.StarWarsGame.Engine.Xml.Parsers; public abstract class XmlObjectParser( - IReadOnlyValueListDictionary parsedElements, + IReadOnlyFrugalValueListDictionary parsedElements, IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : XmlObjectParser(parsedElements, serviceProvider, errorReporter) where TObject : XmlObject @@ -34,12 +34,12 @@ public readonly struct EmptyParseState public abstract class XmlObjectParser( - IReadOnlyValueListDictionary parsedElements, + IReadOnlyFrugalValueListDictionary parsedElements, IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : PetroglyphXmlElementParser(errorReporter) where TObject : XmlObject { - protected IReadOnlyValueListDictionary ParsedElements { get; } = + protected IReadOnlyFrugalValueListDictionary ParsedElements { get; } = parsedElements ?? throw new ArgumentNullException(nameof(parsedElements)); protected ICrc32HashingService HashingService { get; } = serviceProvider.GetRequiredService(); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/PG.StarWarsGame.Files.ALO.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/PG.StarWarsGame.Files.ALO.csproj index c653074..052190c 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/PG.StarWarsGame.Files.ALO.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/PG.StarWarsGame.Files.ALO.csproj @@ -24,7 +24,4 @@ - - - \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj index 36221be..5328887 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj @@ -17,9 +17,6 @@ preview - - - - + \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/PG.StarWarsGame.Files.XML.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/PG.StarWarsGame.Files.XML.csproj index fbb055c..5feadba 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/PG.StarWarsGame.Files.XML.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/PG.StarWarsGame.Files.XML.csproj @@ -18,8 +18,7 @@ preview - - + diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlFileContainerParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlFileContainerParser.cs index f42a9a9..cd21e00 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlFileContainerParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlFileContainerParser.cs @@ -1,10 +1,10 @@ using System.IO; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; namespace PG.StarWarsGame.Files.XML.Parsers; public interface IPetroglyphXmlFileContainerParser : IPetroglyphXmlParser where T : notnull { - void ParseFile(Stream xmlStream, IValueListDictionary parsedEntries); + void ParseFile(Stream xmlStream, IFrugalValueListDictionary parsedEntries); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileContainerParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileContainerParser.cs index 4e371e2..d2b862a 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileContainerParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileContainerParser.cs @@ -1,7 +1,7 @@ using System; using System.IO; using System.Xml.Linq; -using PG.Commons.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Files.XML.ErrorHandling; @@ -10,12 +10,12 @@ namespace PG.StarWarsGame.Files.XML.Parsers; public abstract class PetroglyphXmlFileContainerParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? listener = null) : PetroglyphXmlFileParserBase(serviceProvider, listener), IPetroglyphXmlFileContainerParser where T : notnull { - public void ParseFile(Stream xmlStream, IValueListDictionary parsedEntries) + public void ParseFile(Stream xmlStream, IFrugalValueListDictionary parsedEntries) { var root = GetRootElement(xmlStream, out var fileName); if (root is not null) Parse(root, parsedEntries, fileName); } - protected abstract void Parse(XElement element, IValueListDictionary parsedElements, string fileName); + protected abstract void Parse(XElement element, IFrugalValueListDictionary parsedElements, string fileName); } \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/CommonTestBase.cs b/test/ModVerify.CliApp.Test/CommonTestBase.cs index 7d6dc18..8a93507 100644 --- a/test/ModVerify.CliApp.Test/CommonTestBase.cs +++ b/test/ModVerify.CliApp.Test/CommonTestBase.cs @@ -1,29 +1,22 @@ -using System; -using System.IO.Abstractions; +using AET.SteamAbstraction; using AnakinRaW.CommonUtilities.Hashing; +using AnakinRaW.CommonUtilities.Testing; using Microsoft.Extensions.DependencyInjection; using PG.Commons; -using Testably.Abstractions.Testing; +using PG.StarWarsGame.Infrastructure; +using PG.StarWarsGame.Infrastructure.Clients.Steam; namespace ModVerify.CliApp.Test; -public abstract class CommonTestBase +public abstract class CommonTestBase : TestBaseWithFileSystem { - protected readonly MockFileSystem FileSystem = new(); - protected readonly IServiceProvider ServiceProvider; - - protected CommonTestBase() - { - var sc = new ServiceCollection(); - sc.AddSingleton(sp => new HashingService(sp)); - sc.AddSingleton(FileSystem); - PetroglyphCommons.ContributeServices(sc); - // ReSharper disable once VirtualMemberCallInConstructor - SetupServices(sc); - ServiceProvider = sc.BuildServiceProvider(); - } - - protected virtual void SetupServices(ServiceCollection serviceCollection) + protected override void SetupServices(IServiceCollection serviceCollection) { + base.SetupServices(serviceCollection); + serviceCollection.AddSingleton(sp => new HashingService(sp)); + PetroglyphCommons.ContributeServices(serviceCollection); + PetroglyphGameInfrastructure.InitializeServices(serviceCollection); + SteamAbstractionLayer.InitializeServices(serviceCollection); + SteamPetroglyphStarWarsGameClients.InitializeServices(serviceCollection); } } \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/EmbeddedBaselineTest.cs b/test/ModVerify.CliApp.Test/EmbeddedBaselineTest.cs index cde1dad..ecf3253 100644 --- a/test/ModVerify.CliApp.Test/EmbeddedBaselineTest.cs +++ b/test/ModVerify.CliApp.Test/EmbeddedBaselineTest.cs @@ -1,31 +1,18 @@ using AET.ModVerify.App; using AET.ModVerify.App.Reporting; -using AET.ModVerify.App.Settings; -using AET.ModVerify.Settings; using AnakinRaW.ApplicationBase.Environment; using Microsoft.Extensions.DependencyInjection; using PG.StarWarsGame.Engine; using System; using System.IO.Abstractions; -using ModVerify.CliApp.Test.TestData; using Testably.Abstractions; +using Xunit; namespace ModVerify.CliApp.Test; public class BaselineSelectorTest { private static readonly IFileSystem FileSystem = new RealFileSystem(); - private static readonly ModVerifyAppSettings TestSettings = new() - { - ReportSettings = new(), - GameInstallationsSettings = new (), - VerifyPipelineSettings = new() - { - GameVerifySettings = new GameVerifySettings(), - VerifiersProvider = new NoVerifierProvider() - } - }; - private readonly IServiceProvider _serviceProvider; public BaselineSelectorTest() @@ -42,6 +29,6 @@ public BaselineSelectorTest() public void LoadEmbeddedBaseline(GameEngineType engineType) { // Ensure this operation does not crash, meaning the embedded baseline is at least compatible. - new BaselineSelector(TestSettings, _serviceProvider).LoadEmbeddedBaseline(engineType); + BaselineSelector.LoadEmbeddedBaseline(engineType); } } \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj b/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj index 84d1bba..bca9845 100644 --- a/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj +++ b/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj @@ -3,31 +3,49 @@ net10.0 $(TargetFrameworks);net481 - false preview + + false + true + Exe + + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + + - - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - - + + true + true + + + + Windows + + + Linux + + diff --git a/test/ModVerify.CliApp.Test/ModVerifyOptionsParserTest.cs b/test/ModVerify.CliApp.Test/ModVerifyOptionsParserTest.cs index 294baf7..c5caf92 100644 --- a/test/ModVerify.CliApp.Test/ModVerifyOptionsParserTest.cs +++ b/test/ModVerify.CliApp.Test/ModVerifyOptionsParserTest.cs @@ -4,7 +4,11 @@ using System.IO.Abstractions; using ModVerify.CliApp.Test.TestData; using Testably.Abstractions; +using Xunit; +#if NETFRAMEWORK using ModVerify.CliApp.Test.Utilities; +#endif + namespace ModVerify.CliApp.Test; diff --git a/test/ModVerify.CliApp.Test/TargetSelectors/AutomaticSelectorTest.cs b/test/ModVerify.CliApp.Test/TargetSelectors/AutomaticSelectorTest.cs new file mode 100644 index 0000000..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 From 520e832f4b87ce958d5d94cea5b2f0b5925a27bf Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Wed, 11 Feb 2026 10:57:51 +0100 Subject: [PATCH 6/7] update documentation --- README.md | 64 ++++++++++++++++++++++++++++++--------------- docs/ModVerify.png | Bin 0 -> 101833 bytes 2 files changed, 43 insertions(+), 21 deletions(-) create mode 100644 docs/ModVerify.png 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/docs/ModVerify.png b/docs/ModVerify.png new file mode 100644 index 0000000000000000000000000000000000000000..babd53c2186c5d2387ed4b009f37574f6fc18d97 GIT binary patch literal 101833 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 literal 0 HcmV?d00001 From 5609e1116956f167854ba1bf6ccafcf7f8facb5b Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Wed, 11 Feb 2026 12:57:13 +0100 Subject: [PATCH 7/7] update deps --- modules/ModdingToolBase | 2 +- src/ModVerify.CliApp/ModVerify.CliApp.csproj | 16 ++++++++++------ src/ModVerify/ModVerify.csproj | 6 +++++- .../PG.StarWarsGame.Engine.csproj | 3 +++ .../PG.StarWarsGame.Files.ALO.csproj | 3 +++ .../PG.StarWarsGame.Files.ChunkFiles.csproj | 3 +++ .../PG.StarWarsGame.Files.XML.csproj | 3 +++ .../ModVerify.CliApp.Test.csproj | 6 +++++- 8 files changed, 33 insertions(+), 9 deletions(-) diff --git a/modules/ModdingToolBase b/modules/ModdingToolBase index 0657db4..5103bad 160000 --- a/modules/ModdingToolBase +++ b/modules/ModdingToolBase @@ -1 +1 @@ -Subproject commit 0657db489e65d288bd3cc27b44d15bbb55fcac05 +Subproject commit 5103bad6f09ba88061ccbc36ee285ee9300744cc diff --git a/src/ModVerify.CliApp/ModVerify.CliApp.csproj b/src/ModVerify.CliApp/ModVerify.CliApp.csproj index ce20eed..ec2fd46 100644 --- a/src/ModVerify.CliApp/ModVerify.CliApp.csproj +++ b/src/ModVerify.CliApp/ModVerify.CliApp.csproj @@ -41,11 +41,11 @@ - - - - - + + + + + @@ -74,7 +74,7 @@ compile runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -86,6 +86,10 @@ + + + + diff --git a/src/ModVerify/ModVerify.csproj b/src/ModVerify/ModVerify.csproj index 62b1a65..f1415bc 100644 --- a/src/ModVerify/ModVerify.csproj +++ b/src/ModVerify/ModVerify.csproj @@ -35,7 +35,7 @@ - + @@ -51,4 +51,8 @@ + + + + diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj index 47b3a7f..326fd1d 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj @@ -39,4 +39,7 @@ + + + \ No newline at end of file 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 052190c..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 @@ -24,4 +24,7 @@ + + + \ 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 5328887..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 @@ -19,4 +19,7 @@ + + + \ 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 5feadba..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 @@ -26,4 +26,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj b/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj index bca9845..ef3c9ef 100644 --- a/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj +++ b/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj @@ -17,7 +17,7 @@ - + @@ -35,6 +35,10 @@ + + + + true true