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 01/14] 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 02/14] 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 03/14] 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 04/14] 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 05/14] 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 06/14] 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 07/14] 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 From 5c2de7c96601eae992e098221c8a4ff75cf0cea9 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sat, 14 Feb 2026 12:36:18 +0100 Subject: [PATCH 08/14] Refactor Reporting (#33) * introduce VerifierService and VerificationResult and report metadata information. * udpate deps * fix project files and missing pacakge icon * move code and namepsaces * update module * minor project changes * keep real verifier chain * ReferencedModelsVerifier captures the context of the verified model * support error aggregation in JSON verification reporter * move to files --- Directory.Build.props | 7 +- aet.png | Bin 0 -> 3613 bytes modules/ModdingToolBase | 2 +- .../App/CreateBaselineAction.cs | 12 +- .../App/ModVerifyApplicationAction.cs | 59 +++++---- src/ModVerify.CliApp/App/VerifyAction.cs | 53 ++++++-- src/ModVerify.CliApp/ModVerify.CliApp.csproj | 14 +- src/ModVerify.CliApp/Program.cs | 40 +----- .../Reporting/BaselineFactory.cs | 13 +- .../Reporting/BaselineSelector.cs | 2 +- .../Reporting/IBaselineFactory.cs | 1 + .../VerifyConsoleProgressReporter.cs | 2 +- .../ModVerify.CliApp/Resources/aet.ico | Bin .../Settings/ModVerifyAppSettings.cs | 2 +- .../Settings/SettingsBuilder.cs | 5 +- .../VerificationTargetSelectorBase.cs | 1 - .../Utilities/ExtensionMethods.cs | 22 +++- .../Utilities/ModVerifyConsoleUtilities.cs | 2 +- .../DefaultGameVerifiersProvider.cs | 2 +- src/ModVerify/GameVerificationException.cs | 12 +- .../GameVerifierPipelineStep.cs | 14 +- src/ModVerify/GameVerifierService.cs | 66 ++++++++++ .../{Pipeline => }/GameVerifyPipeline.cs | 120 ++++++++++-------- src/ModVerify/IGameVerifierService.cs | 22 ++++ .../{Pipeline => }/IGameVerifiersProvider.cs | 2 +- src/ModVerify/ModVerify.csproj | 17 +-- src/ModVerify/ModVerify.csproj.DotSettings | 5 + src/ModVerify/ModVerifyServiceExtensions.cs | 12 +- .../AggregatedVerifyProgressReporter.cs | 6 +- .../Progress/IVerifyProgressReporter.cs | 2 +- .../{Pipeline => }/Progress/VerifyProgress.cs | 2 +- .../Progress/VerifyProgressInfo.cs | 2 +- .../Baseline/BaselineVerificationTarget.cs | 12 ++ .../Baseline/InvalidBaselineException.cs | 14 ++ .../{ => Baseline}/Json/JsonBaselineParser.cs | 2 +- .../{ => Baseline}/Json/JsonBaselineSchema.cs | 4 +- .../Json/JsonVerificationBaseline.cs | 5 +- .../{ => Baseline}/VerificationBaseline.cs | 3 +- .../Reporting/BaselineVerificationTarget.cs | 23 ---- .../Engine/EngineErrorReporterBase.cs | 14 +- .../Engine/GameAssertErrorReporter.cs | 4 +- .../GameEngineErrorCollection.cs} | 28 +++- .../IGameEngineErrorCollection.cs | 2 +- .../Engine/InitializationErrorReporter.cs | 4 +- .../Engine/XmlParseErrorReporter.cs | 4 +- .../Reporting/IVerificationReporter.cs | 9 -- .../Reporting/InvalidBaselineException.cs | 14 -- .../Json/JsonAggregatedVerificationError.cs | 30 +++++ .../Reporting/Json/JsonVerificationError.cs | 42 ++---- .../Json/JsonVerificationErrorBase.cs | 49 +++++++ .../Reporting/Json/JsonVerificationReport.cs | 7 +- .../Json/JsonVerificationReportMetadata.cs | 24 ++++ .../Reporting/Json/JsonVerificationTarget.cs | 10 ++ .../{ => Console}/ConsoleReporter.cs | 28 ++-- .../Console/ConsoleReporterSettings.cs | 6 + .../Reporting/Reporters/ExtensionMethods.cs | 44 +++++++ .../Reporting/Reporters/FileBasedReporter.cs | 1 - .../FileBasedReporterSettings.cs | 2 +- .../Reporters/IVerificationReporter.cs | 8 ++ .../Reporting/Reporters/JSON/JsonReporter.cs | 79 +++++++++++- .../Reporters/JSON/JsonReporterSettings.cs | 9 +- .../Reporting/Reporters/ReporterBase.cs | 3 +- .../ReporterSettings.cs | 2 +- .../Reporters/Text/TextFileReporter.cs | 63 ++++++--- .../Text/TextFileReporterSettings.cs | 6 +- .../Reporters/VerificationReportBroker.cs | 36 ++++++ .../VerificationReportersExtensions.cs | 52 -------- .../Reporting/RestoredVerifierInfo.cs | 10 ++ .../Settings/GlobalVerifyReportSettings.cs | 10 -- .../Json/JsonSuppressionFilter.cs | 2 +- .../Json/JsonSuppressionList.cs | 2 +- .../{ => Suppressions}/SuppressionFilter.cs | 6 +- .../{ => Suppressions}/SuppressionList.cs | 3 +- .../Reporting/VerificationCompletionStatus.cs | 8 ++ src/ModVerify/Reporting/VerificationError.cs | 34 +++-- .../Reporting/VerificationReportBroker.cs | 29 ----- src/ModVerify/Reporting/VerificationResult.cs | 44 +++++++ .../VerifierChainEqualityComparer.cs | 40 ++++++ ...Settings.cs => VerifierServiceSettings.cs} | 6 +- .../Utilities/VerificationErrorExtensions.cs | 2 + src/ModVerify/VerificationTarget.cs | 13 +- .../Verifiers/GameEngineErrorCollector.cs | 5 +- src/ModVerify/Verifiers/GameVerifierBase.cs | 2 +- .../Verifiers/NameBasedEqualityComparer.cs | 34 +++++ .../Verifiers/ReferencedModelsVerifier.cs | 31 +++-- .../ErrorReporting/GameEngineErrorReporter.cs | 16 --- .../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 | 4 - .../TestData/NoVerifierProvider.cs | 2 +- 92 files changed, 944 insertions(+), 535 deletions(-) create mode 100644 aet.png rename aet.ico => src/ModVerify.CliApp/Resources/aet.ico (100%) rename src/ModVerify/{Pipeline => }/DefaultGameVerifiersProvider.cs (96%) rename src/ModVerify/{Pipeline => }/GameVerifierPipelineStep.cs (91%) create mode 100644 src/ModVerify/GameVerifierService.cs rename src/ModVerify/{Pipeline => }/GameVerifyPipeline.cs (56%) create mode 100644 src/ModVerify/IGameVerifierService.cs rename src/ModVerify/{Pipeline => }/IGameVerifiersProvider.cs (91%) rename src/ModVerify/{Pipeline => }/Progress/AggregatedVerifyProgressReporter.cs (95%) rename src/ModVerify/{Pipeline => }/Progress/IVerifyProgressReporter.cs (76%) rename src/ModVerify/{Pipeline => }/Progress/VerifyProgress.cs (84%) rename src/ModVerify/{Pipeline => }/Progress/VerifyProgressInfo.cs (74%) create mode 100644 src/ModVerify/Reporting/Baseline/BaselineVerificationTarget.cs create mode 100644 src/ModVerify/Reporting/Baseline/InvalidBaselineException.cs rename src/ModVerify/Reporting/{ => Baseline}/Json/JsonBaselineParser.cs (95%) rename src/ModVerify/Reporting/{ => Baseline}/Json/JsonBaselineSchema.cs (97%) rename src/ModVerify/Reporting/{ => Baseline}/Json/JsonVerificationBaseline.cs (92%) rename src/ModVerify/Reporting/{ => Baseline}/VerificationBaseline.cs (96%) delete mode 100644 src/ModVerify/Reporting/BaselineVerificationTarget.cs rename src/ModVerify/Reporting/{Reporters => }/Engine/EngineErrorReporterBase.cs (83%) rename src/ModVerify/Reporting/{Reporters => }/Engine/GameAssertErrorReporter.cs (93%) rename src/ModVerify/Reporting/{ConcurrentGameEngineErrorReporter.cs => Engine/GameEngineErrorCollection.cs} (50%) rename src/ModVerify/Reporting/{ => Engine}/IGameEngineErrorCollection.cs (87%) rename src/ModVerify/Reporting/{Reporters => }/Engine/InitializationErrorReporter.cs (84%) rename src/ModVerify/Reporting/{Reporters => }/Engine/XmlParseErrorReporter.cs (97%) delete mode 100644 src/ModVerify/Reporting/IVerificationReporter.cs delete mode 100644 src/ModVerify/Reporting/InvalidBaselineException.cs create mode 100644 src/ModVerify/Reporting/Json/JsonAggregatedVerificationError.cs create mode 100644 src/ModVerify/Reporting/Json/JsonVerificationErrorBase.cs create mode 100644 src/ModVerify/Reporting/Json/JsonVerificationReportMetadata.cs rename src/ModVerify/Reporting/Reporters/{ => Console}/ConsoleReporter.cs (62%) create mode 100644 src/ModVerify/Reporting/Reporters/Console/ConsoleReporterSettings.cs create mode 100644 src/ModVerify/Reporting/Reporters/ExtensionMethods.cs rename src/ModVerify/Reporting/{Settings => Reporters}/FileBasedReporterSettings.cs (85%) create mode 100644 src/ModVerify/Reporting/Reporters/IVerificationReporter.cs rename src/ModVerify/Reporting/{Settings => Reporters}/ReporterSettings.cs (74%) create mode 100644 src/ModVerify/Reporting/Reporters/VerificationReportBroker.cs delete mode 100644 src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs create mode 100644 src/ModVerify/Reporting/RestoredVerifierInfo.cs delete mode 100644 src/ModVerify/Reporting/Settings/GlobalVerifyReportSettings.cs rename src/ModVerify/Reporting/{ => Suppressions}/Json/JsonSuppressionFilter.cs (91%) rename src/ModVerify/Reporting/{ => Suppressions}/Json/JsonSuppressionList.cs (88%) rename src/ModVerify/Reporting/{ => Suppressions}/SuppressionFilter.cs (93%) rename src/ModVerify/Reporting/{ => Suppressions}/SuppressionList.cs (96%) create mode 100644 src/ModVerify/Reporting/VerificationCompletionStatus.cs delete mode 100644 src/ModVerify/Reporting/VerificationReportBroker.cs create mode 100644 src/ModVerify/Reporting/VerificationResult.cs create mode 100644 src/ModVerify/Reporting/VerifierChainEqualityComparer.cs rename src/ModVerify/Settings/{VerifyPipelineSettings.cs => VerifierServiceSettings.cs} (72%) create mode 100644 src/ModVerify/Verifiers/NameBasedEqualityComparer.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/GameEngineErrorReporter.cs diff --git a/Directory.Build.props b/Directory.Build.props index 5110cdb..723456e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -33,20 +33,17 @@ - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - + all 3.9.50 - + \ No newline at end of file diff --git a/aet.png b/aet.png new file mode 100644 index 0000000000000000000000000000000000000000..ac479433817de947441ad136ccdc8962baeca251 GIT binary patch literal 3613 zcmV+&4&w2NP)_l zV8H^{I-UryXU`tg)YJskuCSp-$~ip048ZLG$^h)nbVGLS+Lg3Udt^a_0K5p`bpQ(i zd_mk(1FEZ0zX-tRGTcVb@H7J=gs%ZuY?SQuWp z1^{RTFgJtkc0`nCfNEPm06!tGKaDI#v70LZ9A-TmfA7uXLb9mt?0k)~47A@9qDlbO zSuazgQ7q#w04Iw3e*;j>^Di5-KndmHJpVb5y~X_oQJ!@~l>nDe;7Y`e>o<5T1~7v} zyBxp@0LKH^4`2fc_mFsQEP!nxx8F0ON`P$?rV>v9C=l0jMhqdpUkBi9mem8e3cw!) zn|d6;Gak8F~^4j`S;NfGOFr z(^79y2<;67Q6j)R06zpU62LOHEr)97Y)k#U)R3CR2v_x|Tk1Y4zEu%yDuC@|O%H|q zU9N}<3sRjjP+Wfrz$>(T&vSTYD=l0l8y*qw`I)6|B~{UVe4qU8%N(A`2%hYb8d*f6y9sZ<&)sY5Uy}19bxPLy`RExM@=itf+1hH5wY2QX=1{|aZ(}u$?&nav9 zRjQy$0CzI(mr%GXIk5%6Jpdj9@BlTc?mNNlh%b===?QUJ2SN3x78SeAW>) z8IUhJUgI^F`hPX9KcH;@I^oXZ>^X%tfa%7tL9wYG0Nx@YLq*gG&_ZoyAk#`VxM)WT zHyPJmDAy9a-UZ+nmgn;5A?*&}3?7x5dI38>mu`_K1N=5CqDBBv;Ldcp-<7_fdjZ^L z+*4Z5N{-$HwWLlYOeb2_-N=dxleFXOM-u_MF_wE0J5alhbB$18p6~(Xv*QZbVW+Z# zPf7B>uH#?2I6r`QBt_T+=tvfDYdcVRO>_}PTxbZpQywOpXm6bl4$jc+hA=xuv5HY4 zzCRPN6-g9UYAWXlf< zL^fNhP&4Q^M%fOd^DBI6NbQmZ&7^VTgg;Y0$hdD;I@(r?vH}`LJ!#SQ;4@`3qH>sk zRV|*m*!W*2Q78NS78^?o6hRYUfDu}hQ2cxfjhktfw31s40(7Jw$ZVssmVd{NUm?oR zpw=bPB+hR#cz>K}F1gGfvZ0$pd0+^V1)XM$Mu`vj@sE)&+uRGLTj3ypx5afUpI0-W zztQ<+Tr*&XF11W42JozL?-jP`YG%tNENya|$$=n90{qlLXsHjq2*7$#wuV}cL{G~H zOM&?<0OKjJCDg3jPsz72u^7tsYQ8{WzY zmK$urcGGit63f@|`bL)yZvgN<*8z+YS#nb?b)8lmUQO8`>)J9h+>k>xye<}tJs);Z z{x|>&j8T3Fz}*y-Zdhe0>up@`2Jj*Cgz^}uIg7%vke%Mh5x&n7rtI!85S$igDAqX3 zuqAp@SbISkpVyh)$x~j^oB{KV(Xzvb7qQ48r1(Xn-s>#yb*cz^`b8YOJF6=ZaJb(34t^#xiD}ZTI<}6_&Do#C7+Ox4 zV38-_Y}(}Ec_mA}P21o+2bV-8qi?2q`!-Wy)_ZAaDg^ie8`aDX8Rn9slML%P<;^x# ziEC*fJpkZ(m-ppT=uUFDpG+~3SW9gw*DwihRDsC*=&ELih@*n2kQY zoo;y5A}E`w`GA?v@}80%PgCgi<@BFO?W5L3O*L~#z2IDauTG1}qnazR+>9+hf1fzWgk)I28D7qF`OXa%t?hpZIx(x7`u3GVKm~yp z9m+2O@DH)&K)P6*VtjWB+2U(d1T=n0A-px6MtLQ`02)|*LfCcWJ&sNrnuV2}F1#dV z*>D4o#iDFFQ{fxZZIo96tf4{Dn(jZepiQj#)u^TazlerWi3mGZ&{DN&P;u(sW*p#S zhx*A-!sSgG2|5L<0vZ&a2~ZcrKXZZEId5=k>yK>UEP0prs;Lc`cE^pvRk1^quS`)> z8zEHCRvLqWt}~8Gx`^o)YT}eXkAk>NEJ@O6p_@XZR%k0t276eRLYuk?V=7LUw)^&1 zDIh8Y*lfraoe8&vg1AOF=H>~(EVuEJv$RAkZB?E3hZNd0h4GC9?T`5>ydx>1LI6_^ zbSDe?$f$QRv+NdgCB59vpkAE!7NJa~D(>;wo^lbJm>5>)BSHj#v3Nr3){-@ir2C^Q z_k-AESjqBSZuGGvg|1WIk8=jrQ&`uh^Ra#j(9BM-1<2!U;1Lp|jSV~`8hwDo)Ad^T@jCxsb;?)7I|tJ3 zt1*L#xwB~%CzxP0fcm=Bj&7h3ZlUdP3Ol%1bb7kjMpsG7EGrcu{2)L4exqD3xXZU} znvp8H7y>bm@!kpK{c+1)aT*;0TCC@Uda9~v$%|A7&`RG(LTD^u!YLq=Y?*18NRSKI z(rSDm9U(l-B^&x^Mk@ziDlU%yLplVAGcfXa5DjT3h%+!1*uK+2sG;LNBSg>xZcdOd z4ayD}7q-_h%ZO9^3$*hQuq$Fb11ZA;dS5gBKlRMIJ4|n=KI)~cM_9Lono)w^6&L5= zbw_$qdsz0Sc7l$E%?s$eK82utK_hweLxei68{1B==Pq{q;gFI5!2DA$1ZZUnVrUz` z22FrC1KInyA)Xg>FH91~sqIzizKmyJ4F7;|_jJ>Q)-o{*?r>p0BYfrc2ZT%pWX5|S zLRE?>i}QOB&1{;w-CNq{z5%vCORx`(I{UFWbB{%G+_9T`V9LXy*yEaHAx z{774n7-lJ(>Jnkg2(lFuRwM4a4)@AJ__;wPU%p|KJ>$bz3=P=~%0DH3`NG8bfcNSd zWP~eUh{}F;+0KfXLY89&^H1=w*MwK(7~0ipE;3JbWoMwsHUYrx&1_3uM=?VoU0lu8 zj$o?FZALCpRuKgPfT7L%4I*@AmU6n#T2%|xP`|0_p&}{-0Ds@*9wW=(PLXJv3OdpC ztP===T-_N6qC^02zo7fXOvtAzBUQw>TIShfzoh3K&rwL~HC}^Uen~GG`f(kylggWQ zM3DdrzZSoIU6jIhlNxb)KQoqW>laZa0O*guo*~OYV&R4#REE9#dAf))0l=k6mvI5# z=oFq0E2bP6a{jwxM-pt_ycs)p+J2-oW5}V4q9&V-@Qc#YQk0jwZ*}2`0LKs>qRxVj jdK@D_mhqne009607wx>LkmG_g00000NkvXXu0mjfZf2dG literal 0 HcmV?d00001 diff --git a/modules/ModdingToolBase b/modules/ModdingToolBase index 5103bad..e12f6ce 160000 --- a/modules/ModdingToolBase +++ b/modules/ModdingToolBase @@ -1 +1 @@ -Subproject commit 5103bad6f09ba88061ccbc36ee285ee9300744cc +Subproject commit e12f6ceedb83fe9e3372dd89c68d508f8479cf92 diff --git a/src/ModVerify.CliApp/App/CreateBaselineAction.cs b/src/ModVerify.CliApp/App/CreateBaselineAction.cs index b0eb880..b776e09 100644 --- a/src/ModVerify.CliApp/App/CreateBaselineAction.cs +++ b/src/ModVerify.CliApp/App/CreateBaselineAction.cs @@ -2,10 +2,10 @@ using AET.ModVerify.App.Settings; using AET.ModVerify.App.Utilities; using AET.ModVerify.Reporting; +using AET.ModVerify.Reporting.Baseline; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; using System.IO.Abstractions; using System.Threading.Tasks; @@ -26,16 +26,14 @@ protected override void PrintAction(VerificationTarget target) Console.WriteLine(); } - protected override async Task ProcessVerifyFindings( - VerificationTarget verificationTarget, - IReadOnlyCollection allErrors) + protected override async Task ProcessResult(VerificationResult result) { var baselineFactory = ServiceProvider.GetRequiredService(); - var baseline = baselineFactory.CreateBaseline(verificationTarget, Settings, allErrors); + var baseline = baselineFactory.CreateBaseline(result.Target, Settings, result.Errors); var fullPath = _fileSystem.Path.GetFullPath(Settings.NewBaselinePath); Logger?.LogInformation(ModVerifyConstants.ConsoleEventId, - "Writing Baseline to '{FullPath}' with {Number} findings", fullPath, allErrors.Count); + "Writing Baseline to '{FullPath}' with {Number} findings", fullPath, result.Errors.Count); await baselineFactory.WriteBaselineAsync(baseline, Settings.NewBaselinePath); @@ -43,7 +41,7 @@ protected override async Task ProcessVerifyFindings( Console.WriteLine(); Console.ForegroundColor = ConsoleColor.DarkGreen; - Console.WriteLine($"Baseline for {verificationTarget.Name} created."); + Console.WriteLine($"Baseline for {result.Target.Name} created."); Console.ResetColor(); return ModVerifyConstants.Success; diff --git a/src/ModVerify.CliApp/App/ModVerifyApplicationAction.cs b/src/ModVerify.CliApp/App/ModVerifyApplicationAction.cs index b3f12f7..113e633 100644 --- a/src/ModVerify.CliApp/App/ModVerifyApplicationAction.cs +++ b/src/ModVerify.CliApp/App/ModVerifyApplicationAction.cs @@ -1,13 +1,13 @@ 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 AET.ModVerify.Reporting.Baseline; +using AET.ModVerify.Reporting.Suppressions; using AnakinRaW.ApplicationBase; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -71,43 +71,55 @@ public async Task ExecuteAsync() PrintAction(verificationTarget); - var allErrors = await VerifyTargetAsync(verificationTarget) + var verificationResult = await VerifyTargetAsync(verificationTarget) .ConfigureAwait(false); - return await ProcessVerifyFindings(verificationTarget, allErrors); + return await ProcessResult(verificationResult); } - protected abstract Task ProcessVerifyFindings( - VerificationTarget verificationTarget, - IReadOnlyCollection allErrors); + protected abstract Task ProcessResult(VerificationResult result); protected abstract VerificationBaseline GetBaseline(VerificationTarget verificationTarget); - private async Task> VerifyTargetAsync(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 { + var verifierService = ServiceProvider.GetRequiredService(); + Logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Verifying '{Target}'...", verificationTarget.Name); - await verifyPipeline.RunAsync().ConfigureAwait(false); + + var verificationResult = await verifierService.VerifyAsync( + verificationTarget, + Settings.VerifierServiceSettings, + baseline, + suppressions, + progressReporter, + new EngineInitializeProgressReporter(verificationTarget.Engine)); + progressReporter.Report(string.Empty, 1.0); - } - catch (OperationCanceledException) - { - Logger?.LogWarning(ModVerifyConstants.ConsoleEventId, "Verification stopped due to enabled failFast setting."); + + switch (verificationResult.Status) + { + case VerificationCompletionStatus.CompletedFailFast: + Logger?.LogWarning(ModVerifyConstants.ConsoleEventId, "Verification stopped due to enabled failFast setting."); + break; + case VerificationCompletionStatus.Cancelled: + Logger?.LogWarning(ModVerifyConstants.ConsoleEventId, "Verification was cancelled."); + break; + case VerificationCompletionStatus.Completed: + default: + Logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Verification completed successfully."); + break; + } + + return verificationResult; + } catch (Exception e) { @@ -119,9 +131,6 @@ private async Task> VerifyTargetAsync(Ver { progressReporter.Dispose(); } - - Logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Finished verification"); - return verifyPipeline.FilteredErrors; } private SuppressionList GetSuppressions() diff --git a/src/ModVerify.CliApp/App/VerifyAction.cs b/src/ModVerify.CliApp/App/VerifyAction.cs index 0cfea0e..f30305c 100644 --- a/src/ModVerify.CliApp/App/VerifyAction.cs +++ b/src/ModVerify.CliApp/App/VerifyAction.cs @@ -1,12 +1,14 @@ using AET.ModVerify.App.Reporting; using AET.ModVerify.App.Settings; +using AET.ModVerify.App.Utilities; 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; +using AET.ModVerify.Reporting.Reporters; +using AET.ModVerify.Reporting.Baseline; namespace AET.ModVerify.App; @@ -23,20 +25,27 @@ protected override void PrintAction(VerificationTarget target) Console.WriteLine(); } - protected override async Task ProcessVerifyFindings( - VerificationTarget verificationTarget, - IReadOnlyCollection allErrors) + protected override async Task ProcessResult(VerificationResult result) { Logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Reporting Errors..."); - var reportBroker = new VerificationReportBroker(ServiceProvider); - await reportBroker.ReportAsync(allErrors); + var reportBroker = new VerificationReportBroker(CreateReporters(), ServiceProvider); + + result = result with + { + Target = result.Target with + { + Location = result.Target.Location.MaskUsername() + } + }; + + await reportBroker.ReportAsync(result); if (Settings.AppFailsOnMinimumSeverity.HasValue && - allErrors.Any(x => x.Severity >= Settings.AppFailsOnMinimumSeverity)) + result.Errors.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); + result.Target.Name, Settings.AppFailsOnMinimumSeverity); return ModVerifyConstants.CompletedWithFindings; } @@ -57,4 +66,32 @@ protected override VerificationBaseline GetBaseline(VerificationTarget verificat } return baseline; } + + private IReadOnlyCollection CreateReporters() + { + var reporters = new List(); + + reporters.Add(IVerificationReporter.CreateConsole(new ConsoleReporterSettings + { + MinimumReportSeverity = Settings.VerifierServiceSettings.FailFastSettings.IsFailFast + ? VerificationSeverity.Information + : VerificationSeverity.Error + }, ServiceProvider)); + + var outputDirectory = Settings.ReportDirectory; + reporters.Add(IVerificationReporter.CreateJson(new JsonReporterSettings + { + OutputDirectory = outputDirectory, + MinimumReportSeverity = Settings.ReportSettings.MinimumReportSeverity, + AggregateResults = true + }, ServiceProvider)); + + reporters.Add(IVerificationReporter.CreateText(new TextFileReporterSettings + { + OutputDirectory = outputDirectory!, + MinimumReportSeverity = Settings.ReportSettings.MinimumReportSeverity + }, ServiceProvider)); + + return reporters; + } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModVerify.CliApp.csproj b/src/ModVerify.CliApp/ModVerify.CliApp.csproj index ec2fd46..7dcf361 100644 --- a/src/ModVerify.CliApp/ModVerify.CliApp.csproj +++ b/src/ModVerify.CliApp/ModVerify.CliApp.csproj @@ -5,14 +5,14 @@ Exe AET.ModVerify.App ModVerify - $(RepoRootPath)aet.ico + Resources/aet.ico AlamoEngineTools.ModVerify.CliApp ModVerify Console Application AET.ModVerify - Console application that analyzes game modifications for Empire at War / Forces of Corruption for common errors. + An application that analyzes game modifications for Empire at War / Forces of Corruption for common errors. alamo,petroglyph,glyphx @@ -25,10 +25,6 @@ true - - - - @@ -70,7 +66,7 @@ compile runtime; build; native; contentfiles; analyzers; buildtransitive - + compile runtime; build; native; contentfiles; analyzers; buildtransitive @@ -86,10 +82,6 @@ - - - - diff --git a/src/ModVerify.CliApp/Program.cs b/src/ModVerify.CliApp/Program.cs index 2d23b3b..68056a4 100644 --- a/src/ModVerify.CliApp/Program.cs +++ b/src/ModVerify.CliApp/Program.cs @@ -2,11 +2,6 @@ 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.SteamAbstraction; using AnakinRaW.ApplicationBase; using AnakinRaW.ApplicationBase.Environment; @@ -36,7 +31,6 @@ 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; @@ -157,11 +151,10 @@ protected override void CreateAppServices(IServiceCollection services, IReadOnly PetroglyphCommons.ContributeServices(services); PetroglyphEngineServiceContribution.ContributeServices(services); + services.AddModVerify(); services.RegisterVerifierCache(); services.AddSingleton(sp => new BaselineFactory(sp)); - - SetupVerifyReporting(services); if (_offlineMode) { @@ -200,37 +193,6 @@ protected override async Task RunAppAsync(string[] args, IServiceProvider a return await new ModVerifyApplication(_modVerifyAppSettings, appServiceProvider).RunAsync().ConfigureAwait(false); } - private void SetupVerifyReporting(IServiceCollection serviceCollection) - { - Debug.Assert(_modVerifyAppSettings is not null); - - var verifySettings = _modVerifyAppSettings as AppVerifySettings; - - // Console should be in minimal summary mode if we are in a different mode than verify. - serviceCollection.RegisterConsoleReporter(new ReporterSettings - { - MinimumReportSeverity = verifySettings?.VerifyPipelineSettings.FailFastSettings.IsFailFast is true - ? VerificationSeverity.Information - : VerificationSeverity.Error - }, summaryOnly: verifySettings is null); - - if (verifySettings == null) - return; - - var outputDirectory = verifySettings.ReportDirectory; - serviceCollection.RegisterJsonReporter(new JsonReporterSettings - { - OutputDirectory = outputDirectory!, - MinimumReportSeverity = _modVerifyAppSettings.ReportSettings.MinimumReportSeverity - }); - - serviceCollection.RegisterTextFileReporter(new TextFileReporterSettings - { - OutputDirectory = outputDirectory!, - MinimumReportSeverity = _modVerifyAppSettings.ReportSettings.MinimumReportSeverity - }); - } - private void ConfigureLogging(ILoggingBuilder loggingBuilder) { loggingBuilder.ClearProviders(); diff --git a/src/ModVerify.CliApp/Reporting/BaselineFactory.cs b/src/ModVerify.CliApp/Reporting/BaselineFactory.cs index a83cea9..b621343 100644 --- a/src/ModVerify.CliApp/Reporting/BaselineFactory.cs +++ b/src/ModVerify.CliApp/Reporting/BaselineFactory.cs @@ -7,10 +7,9 @@ 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; +using AET.ModVerify.Reporting.Baseline; namespace AET.ModVerify.App.Reporting; @@ -89,21 +88,13 @@ public VerificationBaseline CreateBaseline( Engine = target.Engine, Name = target.Name, Version = target.Version, - Location = settings.WriteLocations ? MaskUsername(target.Location) : null, + Location = settings.WriteLocations ? target.Location.MaskUsername() : 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 diff --git a/src/ModVerify.CliApp/Reporting/BaselineSelector.cs b/src/ModVerify.CliApp/Reporting/BaselineSelector.cs index 791eaae..efcaae5 100644 --- a/src/ModVerify.CliApp/Reporting/BaselineSelector.cs +++ b/src/ModVerify.CliApp/Reporting/BaselineSelector.cs @@ -1,6 +1,6 @@ using AET.ModVerify.App.Resources.Baselines; using AET.ModVerify.App.Settings; -using AET.ModVerify.Reporting; +using AET.ModVerify.Reporting.Baseline; using AnakinRaW.ApplicationBase; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/src/ModVerify.CliApp/Reporting/IBaselineFactory.cs b/src/ModVerify.CliApp/Reporting/IBaselineFactory.cs index 721a7ce..a534cdb 100644 --- a/src/ModVerify.CliApp/Reporting/IBaselineFactory.cs +++ b/src/ModVerify.CliApp/Reporting/IBaselineFactory.cs @@ -1,5 +1,6 @@ using AET.ModVerify.App.Settings; using AET.ModVerify.Reporting; +using AET.ModVerify.Reporting.Baseline; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; diff --git a/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs b/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs index b2ce170..66587ae 100644 --- a/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs +++ b/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs @@ -1,7 +1,7 @@ using System; using System.Threading; using AET.ModVerify.App.Settings; -using AET.ModVerify.Pipeline.Progress; +using AET.ModVerify.Progress; using AnakinRaW.CommonUtilities; using AnakinRaW.CommonUtilities.SimplePipeline.Progress; using ShellProgressBar; diff --git a/aet.ico b/src/ModVerify.CliApp/Resources/aet.ico similarity index 100% rename from aet.ico rename to src/ModVerify.CliApp/Resources/aet.ico diff --git a/src/ModVerify.CliApp/Settings/ModVerifyAppSettings.cs b/src/ModVerify.CliApp/Settings/ModVerifyAppSettings.cs index e4488a0..add39a5 100644 --- a/src/ModVerify.CliApp/Settings/ModVerifyAppSettings.cs +++ b/src/ModVerify.CliApp/Settings/ModVerifyAppSettings.cs @@ -29,7 +29,7 @@ public required VerificationTargetSettings VerificationTargetSettings init => field = value ?? throw new ArgumentNullException(nameof(value)); } - public required VerifyPipelineSettings VerifyPipelineSettings + public required VerifierServiceSettings VerifierServiceSettings { get; init => field = value ?? throw new ArgumentNullException(nameof(value)); diff --git a/src/ModVerify.CliApp/Settings/SettingsBuilder.cs b/src/ModVerify.CliApp/Settings/SettingsBuilder.cs index 23a89ff..62bca7a 100644 --- a/src/ModVerify.CliApp/Settings/SettingsBuilder.cs +++ b/src/ModVerify.CliApp/Settings/SettingsBuilder.cs @@ -1,5 +1,4 @@ using AET.ModVerify.App.Settings.CommandLine; -using AET.ModVerify.Pipeline; using AET.ModVerify.Settings; using Microsoft.Extensions.DependencyInjection; using System; @@ -31,7 +30,7 @@ private AppVerifySettings BuildFromVerifyVerb(VerifyVerbOption verifyOptions) return new AppVerifySettings(BuildReportSettings()) { ReportDirectory = GetReportDirectory(), - VerifyPipelineSettings = new VerifyPipelineSettings + VerifierServiceSettings = new VerifierServiceSettings { ParallelVerifiers = verifyOptions.Parallel ? 4 : 1, VerifiersProvider = new DefaultGameVerifiersProvider(), @@ -97,7 +96,7 @@ private AppBaselineSettings BuildFromCreateBaselineVerb(CreateBaselineVerbOption { return new AppBaselineSettings(BuildReportSettings()) { - VerifyPipelineSettings = new VerifyPipelineSettings + VerifierServiceSettings = new VerifierServiceSettings { ParallelVerifiers = baselineVerb.Parallel ? 4 : 1, VerifiersProvider = new DefaultGameVerifiersProvider(), diff --git a/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorBase.cs b/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorBase.cs index 68f0f39..0c45986 100644 --- a/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorBase.cs +++ b/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorBase.cs @@ -2,7 +2,6 @@ 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; diff --git a/src/ModVerify.CliApp/Utilities/ExtensionMethods.cs b/src/ModVerify.CliApp/Utilities/ExtensionMethods.cs index fc07469..5871311 100644 --- a/src/ModVerify.CliApp/Utilities/ExtensionMethods.cs +++ b/src/ModVerify.CliApp/Utilities/ExtensionMethods.cs @@ -1,8 +1,9 @@ -using System.Diagnostics.CodeAnalysis; -using AET.ModVerify.App.Settings.CommandLine; +using AET.ModVerify.App.Settings.CommandLine; using AnakinRaW.ApplicationBase.Environment; using PG.StarWarsGame.Engine; using PG.StarWarsGame.Infrastructure.Games; +using System.Diagnostics.CodeAnalysis; +using System.Linq; namespace AET.ModVerify.App.Utilities; @@ -21,10 +22,6 @@ public GameEngineType Opposite() } } - public static GameEngineType ToEngineType(this GameType type) - { - return (GameEngineType)(int)type; - } extension(ApplicationEnvironment modVerifyEnvironment) { public bool IsUpdatable() @@ -39,6 +36,19 @@ public bool IsUpdatable([NotNullWhen(true)] out UpdatableApplicationEnvironment? } } + public static GameEngineType ToEngineType(this GameType type) + { + return (GameEngineType)(int)type; + } + + public static GameLocations MaskUsername(this GameLocations targetLocation) + { + return new GameLocations( + targetLocation.ModPaths.Select(PathUtilities.MaskUsername).ToList(), + PathUtilities.MaskUsername(targetLocation.GamePath), + targetLocation.FallbackPaths.Select(PathUtilities.MaskUsername).ToList()); + } + public static bool LaunchedWithoutArguments(this BaseModVerifyOptions options) { if (options is VerifyVerbOption verifyOptions) diff --git a/src/ModVerify.CliApp/Utilities/ModVerifyConsoleUtilities.cs b/src/ModVerify.CliApp/Utilities/ModVerifyConsoleUtilities.cs index b9ebff0..9f9962b 100644 --- a/src/ModVerify.CliApp/Utilities/ModVerifyConsoleUtilities.cs +++ b/src/ModVerify.CliApp/Utilities/ModVerifyConsoleUtilities.cs @@ -2,7 +2,7 @@ using Figgle; using System; using System.Collections.Generic; -using AET.ModVerify.Reporting; +using AET.ModVerify.Reporting.Baseline; namespace AET.ModVerify.App.Utilities; diff --git a/src/ModVerify/Pipeline/DefaultGameVerifiersProvider.cs b/src/ModVerify/DefaultGameVerifiersProvider.cs similarity index 96% rename from src/ModVerify/Pipeline/DefaultGameVerifiersProvider.cs rename to src/ModVerify/DefaultGameVerifiersProvider.cs index 26b3893..c9982b7 100644 --- a/src/ModVerify/Pipeline/DefaultGameVerifiersProvider.cs +++ b/src/ModVerify/DefaultGameVerifiersProvider.cs @@ -5,7 +5,7 @@ using AET.ModVerify.Verifiers.GuiDialogs; using PG.StarWarsGame.Engine; -namespace AET.ModVerify.Pipeline; +namespace AET.ModVerify; public sealed class DefaultGameVerifiersProvider : IGameVerifiersProvider { diff --git a/src/ModVerify/GameVerificationException.cs b/src/ModVerify/GameVerificationException.cs index dba312b..a6ada92 100644 --- a/src/ModVerify/GameVerificationException.cs +++ b/src/ModVerify/GameVerificationException.cs @@ -7,8 +7,11 @@ namespace AET.ModVerify; public sealed class GameVerificationException : Exception { + /// + public override string Message => ErrorMessage; + public IReadOnlyCollection Errors { get; } - + private string ErrorMessage { get @@ -23,14 +26,11 @@ private string ErrorMessage } } = null; - /// - public override string Message => ErrorMessage; - - public GameVerificationException(VerificationError error) : this([error]) + internal GameVerificationException(VerificationError error) : this([error]) { } - public GameVerificationException(IEnumerable errors) + internal GameVerificationException(IEnumerable errors) { if (errors is null) throw new ArgumentNullException(nameof(errors)); diff --git a/src/ModVerify/Pipeline/GameVerifierPipelineStep.cs b/src/ModVerify/GameVerifierPipelineStep.cs similarity index 91% rename from src/ModVerify/Pipeline/GameVerifierPipelineStep.cs rename to src/ModVerify/GameVerifierPipelineStep.cs index e83cb25..b1f45b8 100644 --- a/src/ModVerify/Pipeline/GameVerifierPipelineStep.cs +++ b/src/ModVerify/GameVerifierPipelineStep.cs @@ -1,16 +1,16 @@ -using AET.ModVerify.Verifiers; +using System; +using System.Threading; +using System.Threading.Tasks; +using AET.ModVerify.Progress; +using AET.ModVerify.Verifiers; using AnakinRaW.CommonUtilities.SimplePipeline; using AnakinRaW.CommonUtilities.SimplePipeline.Progress; using AnakinRaW.CommonUtilities.SimplePipeline.Steps; using Microsoft.Extensions.Logging; -using System; -using System.Threading; -using System.Threading.Tasks; -using AET.ModVerify.Pipeline.Progress; -namespace AET.ModVerify.Pipeline; +namespace AET.ModVerify; -public sealed class GameVerifierPipelineStep( +internal sealed class GameVerifierPipelineStep( GameVerifier verifier, IServiceProvider serviceProvider) : PipelineStep(serviceProvider), IProgressStep diff --git a/src/ModVerify/GameVerifierService.cs b/src/ModVerify/GameVerifierService.cs new file mode 100644 index 0000000..f6d8f07 --- /dev/null +++ b/src/ModVerify/GameVerifierService.cs @@ -0,0 +1,66 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using AET.ModVerify.Progress; +using AET.ModVerify.Reporting; +using AET.ModVerify.Reporting.Baseline; +using AET.ModVerify.Reporting.Suppressions; +using AET.ModVerify.Settings; +using PG.StarWarsGame.Engine; + +namespace AET.ModVerify; + +internal sealed class GameVerifierService(IServiceProvider serviceProvider) : IGameVerifierService +{ + public async Task VerifyAsync( + VerificationTarget verificationTarget, + VerifierServiceSettings settings, + VerificationBaseline baseline, + SuppressionList suppressions, + IVerifyProgressReporter? progressReporter, + IGameEngineInitializationReporter? engineInitializationReporter, + CancellationToken token = default) + { + if (verificationTarget == null) + throw new ArgumentNullException(nameof(verificationTarget)); + if (settings == null) + throw new ArgumentNullException(nameof(settings)); + + using var pipeline = new GameVerifyPipeline( + verificationTarget, + settings, + serviceProvider, + baseline, + suppressions, + progressReporter, + engineInitializationReporter); + + VerificationCompletionStatus completionStatus; + var start = DateTime.UtcNow; + + try + { + await pipeline.RunAsync(token).ConfigureAwait(false); + completionStatus = VerificationCompletionStatus.Completed; + } + catch (OperationCanceledException) + { + completionStatus = settings.FailFastSettings.IsFailFast + ? VerificationCompletionStatus.CompletedFailFast + : VerificationCompletionStatus.Cancelled; + } + + var duration = DateTime.UtcNow - start; + + return new VerificationResult + { + Duration = duration, + Errors = pipeline.Errors, + Status = completionStatus, + Target = verificationTarget, + UsedBaseline = baseline, + UsedSuppressions = suppressions, + Verifiers = pipeline.Verifiers + }; + } +} \ No newline at end of file diff --git a/src/ModVerify/Pipeline/GameVerifyPipeline.cs b/src/ModVerify/GameVerifyPipeline.cs similarity index 56% rename from src/ModVerify/Pipeline/GameVerifyPipeline.cs rename to src/ModVerify/GameVerifyPipeline.cs index 493aa8c..4ed351e 100644 --- a/src/ModVerify/Pipeline/GameVerifyPipeline.cs +++ b/src/ModVerify/GameVerifyPipeline.cs @@ -1,63 +1,67 @@ -using AET.ModVerify.Pipeline.Progress; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AET.ModVerify.Progress; using AET.ModVerify.Reporting; +using AET.ModVerify.Reporting.Baseline; +using AET.ModVerify.Reporting.Engine; +using AET.ModVerify.Reporting.Suppressions; using AET.ModVerify.Settings; +using AET.ModVerify.Utilities; using AET.ModVerify.Verifiers; using AnakinRaW.CommonUtilities.SimplePipeline; using AnakinRaW.CommonUtilities.SimplePipeline.Runners; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using PG.StarWarsGame.Engine; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using AET.ModVerify.Utilities; -using Microsoft.Extensions.DependencyInjection; -namespace AET.ModVerify.Pipeline; +namespace AET.ModVerify; -public sealed class GameVerifyPipeline : StepRunnerPipelineBase +internal sealed class GameVerifyPipeline : StepRunnerPipelineBase { private readonly List _verifiers = []; + private readonly List _errors = []; private readonly List _verificationSteps = []; - private readonly ConcurrentGameEngineErrorReporter _engineErrorReporter = new(); - + + private readonly GameEngineErrorCollection _engineErrorReporter = new(); private readonly VerificationTarget _verificationTarget; - private readonly VerifyPipelineSettings _pipelineSettings; - private readonly IVerifyProgressReporter _progressReporter; + private readonly VerifierServiceSettings _serviceSettings; + private readonly IVerifyProgressReporter? _progressReporter; private readonly IGameEngineInitializationReporter? _engineInitializationReporter; - private readonly IPetroglyphStarWarsGameEngineService _gameEngineService; - private readonly ILogger? _logger; - private AggregatedVerifyProgressReporter? _aggregatedVerifyProgressReporter; + private readonly VerificationBaseline _baseline; + private readonly SuppressionList _suppressions; + + internal IReadOnlyCollection Errors => [.._errors]; + + internal IReadOnlyCollection Verifiers => [.. _verifiers]; - public IReadOnlyCollection FilteredErrors { get; private set; } = []; - public VerificationBaseline Baseline { get; } - public SuppressionList Suppressions { get; } + private AggregatedVerifyProgressReporter? _aggregatedVerifyProgressReporter; public GameVerifyPipeline( VerificationTarget verificationTarget, - VerifyPipelineSettings pipelineSettings, - IVerifyProgressReporter progressReporter, - IGameEngineInitializationReporter? engineInitializationReporter, + VerifierServiceSettings serviceSettings, + IServiceProvider serviceProvider, VerificationBaseline baseline, SuppressionList suppressions, - IServiceProvider serviceProvider) : base(serviceProvider) + IVerifyProgressReporter? progressReporter = null, + IGameEngineInitializationReporter? engineInitializationReporter = null) + : base(serviceProvider) { - 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)); - _progressReporter = progressReporter ?? throw new ArgumentNullException(nameof(progressReporter)); + _serviceSettings = serviceSettings ?? throw new ArgumentNullException(nameof(serviceSettings)); + _baseline = baseline ?? throw new ArgumentNullException(nameof(baseline)); + _suppressions = suppressions ?? throw new ArgumentNullException(nameof(suppressions)); + _progressReporter = progressReporter; _engineInitializationReporter = engineInitializationReporter; - _gameEngineService = serviceProvider.GetRequiredService(); - _logger = serviceProvider.GetService()?.CreateLogger(GetType()); - FailFast = pipelineSettings.FailFastSettings.IsFailFast; + FailFast = serviceSettings.FailFastSettings.IsFailFast; } - + protected override AsyncStepRunner CreateRunner() { - var requestedRunnerCount = _pipelineSettings.ParallelVerifiers; + var requestedRunnerCount = _serviceSettings.ParallelVerifiers; return requestedRunnerCount switch { < 0 or > 64 => throw new InvalidOperationException( @@ -70,12 +74,14 @@ protected override AsyncStepRunner CreateRunner() protected override async Task PrepareCoreAsync(CancellationToken token) { _verifiers.Clear(); + _errors.Clear(); IStarWarsGameEngine gameEngine; try { - gameEngine = await _gameEngineService.InitializeAsync( + var engineService = ServiceProvider.GetRequiredService(); + gameEngine = await engineService.InitializeAsync( _verificationTarget.Engine, _verificationTarget.Location, _engineErrorReporter, @@ -85,38 +91,43 @@ protected override async Task PrepareCoreAsync(CancellationToken token) } catch (Exception e) { - _logger?.LogError(e, "Creating game engine failed: {Message}", e.Message); + Logger?.LogError(e, "Creating game engine failed: {Message}", e.Message); throw; } - AddStep(new GameEngineErrorCollector(_engineErrorReporter, gameEngine, _pipelineSettings.GameVerifySettings, ServiceProvider)); + AddStep(new GameEngineErrorCollector(_engineErrorReporter, gameEngine, _serviceSettings.GameVerifySettings, ServiceProvider)); - foreach (var gameVerificationStep in CreateVerificationSteps(gameEngine)) + foreach (var gameVerificationStep in CreateVerifiers(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); + if (_progressReporter is not null) + { + _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); + _errors.AddRange(GetReportableErrors(_verifiers.SelectMany(s => s.VerifyErrors))); + _progressReporter?.Report(1.0, $"Finished Verifying {_verificationTarget.Name}", + VerifyProgress.ProgressType, default); } protected override void OnRunnerExecutionError(object sender, StepRunnerErrorEventArgs e) { if (FailFast && e.Exception is GameVerificationException verificationException) { - var minSeverity = _pipelineSettings.FailFastSettings.MinumumSeverity; + var minSeverity = _serviceSettings.FailFastSettings.MinumumSeverity; var ignoreError = verificationException.Errors .Where(error => error.Severity >= minSeverity) - .All(error => Baseline.Contains(error) || Suppressions.Suppresses(error)); + .All(error => _baseline.Contains(error) || _suppressions.Suppresses(error)); if (ignoreError) return; } @@ -128,6 +139,14 @@ protected override IEnumerable GetFailedSteps(IEnumerable steps) return base.GetFailedSteps(steps).Where(s => s.Error is not GameVerificationException); } + protected override void DisposeResources() + { + base.DisposeResources(); + _engineErrorReporter.Clear(); + _aggregatedVerifyProgressReporter?.Dispose(); + _aggregatedVerifyProgressReporter = null; + } + private void AddStep(GameVerifier verifier) { var verificationStep = new GameVerifierPipelineStep(verifier, ServiceProvider); @@ -141,18 +160,13 @@ private IEnumerable GetReportableErrors(IEnumerable CreateVerificationSteps(IStarWarsGameEngine engine) - { - return _pipelineSettings.VerifiersProvider - .GetVerifiers(engine, _pipelineSettings.GameVerifySettings, ServiceProvider); + return errors.ApplyBaseline(_baseline).ApplySuppressions(_suppressions); } - protected override void DisposeResources() + private IEnumerable CreateVerifiers(IStarWarsGameEngine engine) { - base.DisposeResources(); - _aggregatedVerifyProgressReporter?.Dispose(); + return _serviceSettings.VerifiersProvider + .GetVerifiers(engine, _serviceSettings.GameVerifySettings, ServiceProvider) + .Distinct(NameBasedEqualityComparer.Instance); } } \ No newline at end of file diff --git a/src/ModVerify/IGameVerifierService.cs b/src/ModVerify/IGameVerifierService.cs new file mode 100644 index 0000000..c814779 --- /dev/null +++ b/src/ModVerify/IGameVerifierService.cs @@ -0,0 +1,22 @@ +using System.Threading; +using System.Threading.Tasks; +using AET.ModVerify.Progress; +using AET.ModVerify.Reporting; +using AET.ModVerify.Reporting.Baseline; +using AET.ModVerify.Reporting.Suppressions; +using AET.ModVerify.Settings; +using PG.StarWarsGame.Engine; + +namespace AET.ModVerify; + +public interface IGameVerifierService +{ + Task VerifyAsync( + VerificationTarget verificationTarget, + VerifierServiceSettings settings, + VerificationBaseline baseline, + SuppressionList suppressions, + IVerifyProgressReporter? progressReporter, + IGameEngineInitializationReporter? engineInitializationReporter, + CancellationToken token = default); +} \ No newline at end of file diff --git a/src/ModVerify/Pipeline/IGameVerifiersProvider.cs b/src/ModVerify/IGameVerifiersProvider.cs similarity index 91% rename from src/ModVerify/Pipeline/IGameVerifiersProvider.cs rename to src/ModVerify/IGameVerifiersProvider.cs index 930afa6..8cde9dc 100644 --- a/src/ModVerify/Pipeline/IGameVerifiersProvider.cs +++ b/src/ModVerify/IGameVerifiersProvider.cs @@ -4,7 +4,7 @@ using AET.ModVerify.Verifiers; using PG.StarWarsGame.Engine; -namespace AET.ModVerify.Pipeline; +namespace AET.ModVerify; public interface IGameVerifiersProvider { diff --git a/src/ModVerify/ModVerify.csproj b/src/ModVerify/ModVerify.csproj index f1415bc..aecf306 100644 --- a/src/ModVerify/ModVerify.csproj +++ b/src/ModVerify/ModVerify.csproj @@ -10,33 +10,25 @@ ModVerify Core AET.ModVerify - Provides interfaces and classes to verify Empire at War / Forces of Corruption game modifications. + A library that contains classes and methods to verify Empire at War / Forces of Corruption game modifications. alamo,petroglyph,glyphx true true - - - true snupkg - - - - - - + @@ -50,9 +42,4 @@ - - - - - diff --git a/src/ModVerify/ModVerify.csproj.DotSettings b/src/ModVerify/ModVerify.csproj.DotSettings index fcd6f14..3859842 100644 --- a/src/ModVerify/ModVerify.csproj.DotSettings +++ b/src/ModVerify/ModVerify.csproj.DotSettings @@ -1,4 +1,9 @@  + True + True + True + True + False True True True \ No newline at end of file diff --git a/src/ModVerify/ModVerifyServiceExtensions.cs b/src/ModVerify/ModVerifyServiceExtensions.cs index 8b46960..41fbeaa 100644 --- a/src/ModVerify/ModVerifyServiceExtensions.cs +++ b/src/ModVerify/ModVerifyServiceExtensions.cs @@ -5,8 +5,16 @@ namespace AET.ModVerify; public static class ModVerifyServiceExtensions { - public static IServiceCollection RegisterVerifierCache(this IServiceCollection serviceCollection) + extension(IServiceCollection serviceCollection) { - return serviceCollection.AddSingleton(sp => new AlreadyVerifiedCache(sp)); + public IServiceCollection AddModVerify() + { + return serviceCollection.AddSingleton(sp => new GameVerifierService(sp)); + } + + public IServiceCollection RegisterVerifierCache() + { + return serviceCollection.AddSingleton(sp => new AlreadyVerifiedCache(sp)); + } } } \ No newline at end of file diff --git a/src/ModVerify/Pipeline/Progress/AggregatedVerifyProgressReporter.cs b/src/ModVerify/Progress/AggregatedVerifyProgressReporter.cs similarity index 95% rename from src/ModVerify/Pipeline/Progress/AggregatedVerifyProgressReporter.cs rename to src/ModVerify/Progress/AggregatedVerifyProgressReporter.cs index cfdae25..1dbf538 100644 --- a/src/ModVerify/Pipeline/Progress/AggregatedVerifyProgressReporter.cs +++ b/src/ModVerify/Progress/AggregatedVerifyProgressReporter.cs @@ -1,8 +1,8 @@ -using AnakinRaW.CommonUtilities.SimplePipeline.Progress; -using System; +using System; using System.Collections.Generic; +using AnakinRaW.CommonUtilities.SimplePipeline.Progress; -namespace AET.ModVerify.Pipeline.Progress; +namespace AET.ModVerify.Progress; internal class AggregatedVerifyProgressReporter( IVerifyProgressReporter progressReporter, diff --git a/src/ModVerify/Pipeline/Progress/IVerifyProgressReporter.cs b/src/ModVerify/Progress/IVerifyProgressReporter.cs similarity index 76% rename from src/ModVerify/Pipeline/Progress/IVerifyProgressReporter.cs rename to src/ModVerify/Progress/IVerifyProgressReporter.cs index e63e0f9..99c2141 100644 --- a/src/ModVerify/Pipeline/Progress/IVerifyProgressReporter.cs +++ b/src/ModVerify/Progress/IVerifyProgressReporter.cs @@ -1,5 +1,5 @@ using AnakinRaW.CommonUtilities.SimplePipeline.Progress; -namespace AET.ModVerify.Pipeline.Progress; +namespace AET.ModVerify.Progress; public interface IVerifyProgressReporter : IProgressReporter; \ No newline at end of file diff --git a/src/ModVerify/Pipeline/Progress/VerifyProgress.cs b/src/ModVerify/Progress/VerifyProgress.cs similarity index 84% rename from src/ModVerify/Pipeline/Progress/VerifyProgress.cs rename to src/ModVerify/Progress/VerifyProgress.cs index a18f3d3..aa5f2f2 100644 --- a/src/ModVerify/Pipeline/Progress/VerifyProgress.cs +++ b/src/ModVerify/Progress/VerifyProgress.cs @@ -1,6 +1,6 @@ using AnakinRaW.CommonUtilities.SimplePipeline.Progress; -namespace AET.ModVerify.Pipeline.Progress; +namespace AET.ModVerify.Progress; public static class VerifyProgress { diff --git a/src/ModVerify/Pipeline/Progress/VerifyProgressInfo.cs b/src/ModVerify/Progress/VerifyProgressInfo.cs similarity index 74% rename from src/ModVerify/Pipeline/Progress/VerifyProgressInfo.cs rename to src/ModVerify/Progress/VerifyProgressInfo.cs index cadeb0d..b7c8b3d 100644 --- a/src/ModVerify/Pipeline/Progress/VerifyProgressInfo.cs +++ b/src/ModVerify/Progress/VerifyProgressInfo.cs @@ -1,4 +1,4 @@ -namespace AET.ModVerify.Pipeline.Progress; +namespace AET.ModVerify.Progress; public struct VerifyProgressInfo { diff --git a/src/ModVerify/Reporting/Baseline/BaselineVerificationTarget.cs b/src/ModVerify/Reporting/Baseline/BaselineVerificationTarget.cs new file mode 100644 index 0000000..ca77fa8 --- /dev/null +++ b/src/ModVerify/Reporting/Baseline/BaselineVerificationTarget.cs @@ -0,0 +1,12 @@ +using PG.StarWarsGame.Engine; + +namespace AET.ModVerify.Reporting.Baseline; + +public sealed record 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; } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Baseline/InvalidBaselineException.cs b/src/ModVerify/Reporting/Baseline/InvalidBaselineException.cs new file mode 100644 index 0000000..58e180e --- /dev/null +++ b/src/ModVerify/Reporting/Baseline/InvalidBaselineException.cs @@ -0,0 +1,14 @@ +using System; + +namespace AET.ModVerify.Reporting.Baseline; + +public sealed class InvalidBaselineException : Exception +{ + internal InvalidBaselineException(string message) : base(message) + { + } + + internal 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/Baseline/Json/JsonBaselineParser.cs similarity index 95% rename from src/ModVerify/Reporting/Json/JsonBaselineParser.cs rename to src/ModVerify/Reporting/Baseline/Json/JsonBaselineParser.cs index 669ef53..a8ba53c 100644 --- a/src/ModVerify/Reporting/Json/JsonBaselineParser.cs +++ b/src/ModVerify/Reporting/Baseline/Json/JsonBaselineParser.cs @@ -2,7 +2,7 @@ using System.IO; using System.Text.Json; -namespace AET.ModVerify.Reporting.Json; +namespace AET.ModVerify.Reporting.Baseline.Json; internal static class JsonBaselineParser { diff --git a/src/ModVerify/Reporting/Json/JsonBaselineSchema.cs b/src/ModVerify/Reporting/Baseline/Json/JsonBaselineSchema.cs similarity index 97% rename from src/ModVerify/Reporting/Json/JsonBaselineSchema.cs rename to src/ModVerify/Reporting/Baseline/Json/JsonBaselineSchema.cs index 12e3705..cf4513a 100644 --- a/src/ModVerify/Reporting/Json/JsonBaselineSchema.cs +++ b/src/ModVerify/Reporting/Baseline/Json/JsonBaselineSchema.cs @@ -7,9 +7,9 @@ using Json.Schema; using Json.Schema.Keywords; -namespace AET.ModVerify.Reporting.Json; +namespace AET.ModVerify.Reporting.Baseline.Json; -public static class JsonBaselineSchema +internal static class JsonBaselineSchema { private static readonly JsonSchema Schema; private static readonly EvaluationOptions EvaluationOptions; diff --git a/src/ModVerify/Reporting/Json/JsonVerificationBaseline.cs b/src/ModVerify/Reporting/Baseline/Json/JsonVerificationBaseline.cs similarity index 92% rename from src/ModVerify/Reporting/Json/JsonVerificationBaseline.cs rename to src/ModVerify/Reporting/Baseline/Json/JsonVerificationBaseline.cs index 0d9a1b7..3e25aed 100644 --- a/src/ModVerify/Reporting/Json/JsonVerificationBaseline.cs +++ b/src/ModVerify/Reporting/Baseline/Json/JsonVerificationBaseline.cs @@ -1,9 +1,10 @@ -using System; +using AET.ModVerify.Reporting.Json; +using System; using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; -namespace AET.ModVerify.Reporting.Json; +namespace AET.ModVerify.Reporting.Baseline.Json; internal class JsonVerificationBaseline { diff --git a/src/ModVerify/Reporting/VerificationBaseline.cs b/src/ModVerify/Reporting/Baseline/VerificationBaseline.cs similarity index 96% rename from src/ModVerify/Reporting/VerificationBaseline.cs rename to src/ModVerify/Reporting/Baseline/VerificationBaseline.cs index 3f9c274..d734e6e 100644 --- a/src/ModVerify/Reporting/VerificationBaseline.cs +++ b/src/ModVerify/Reporting/Baseline/VerificationBaseline.cs @@ -6,9 +6,10 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; +using AET.ModVerify.Reporting.Baseline.Json; using AET.ModVerify.Reporting.Json; -namespace AET.ModVerify.Reporting; +namespace AET.ModVerify.Reporting.Baseline; public sealed class VerificationBaseline : IReadOnlyCollection { diff --git a/src/ModVerify/Reporting/BaselineVerificationTarget.cs b/src/ModVerify/Reporting/BaselineVerificationTarget.cs deleted file mode 100644 index 6e21ddd..0000000 --- a/src/ModVerify/Reporting/BaselineVerificationTarget.cs +++ /dev/null @@ -1,23 +0,0 @@ -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/Reporters/Engine/EngineErrorReporterBase.cs b/src/ModVerify/Reporting/Engine/EngineErrorReporterBase.cs similarity index 83% rename from src/ModVerify/Reporting/Reporters/Engine/EngineErrorReporterBase.cs rename to src/ModVerify/Reporting/Engine/EngineErrorReporterBase.cs index 455212d..7200f23 100644 --- a/src/ModVerify/Reporting/Reporters/Engine/EngineErrorReporterBase.cs +++ b/src/ModVerify/Reporting/Engine/EngineErrorReporterBase.cs @@ -1,16 +1,22 @@ using System; using System.Collections.Generic; +using AET.ModVerify.Verifiers; using AnakinRaW.CommonUtilities; using PG.StarWarsGame.Engine.IO; -namespace AET.ModVerify.Reporting.Reporters.Engine; +namespace AET.ModVerify.Reporting.Engine; -internal abstract class EngineErrorReporterBase(IGameRepository gameRepository, IServiceProvider serviceProvider) +internal abstract class EngineErrorReporterBase(IGameRepository gameRepository, IServiceProvider serviceProvider) + : IGameVerifierInfo { protected readonly IGameRepository GameRepository = gameRepository ?? throw new ArgumentNullException(nameof(gameRepository)); protected readonly IServiceProvider ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - public abstract string Name { get; } + public IGameVerifierInfo? Parent => null; + + public string Name => GetType().FullName; + + public abstract string FriendlyName { get; } public IEnumerable GetErrors(IEnumerable errors) { @@ -18,7 +24,7 @@ public IEnumerable GetErrors(IEnumerable errors) { var errorData = CreateError(error); yield return new VerificationError( - errorData.Identifier, errorData.Message, [Name], errorData.Context, errorData.Asset, errorData.Severity); + errorData.Identifier, errorData.Message, [this], errorData.Context, errorData.Asset, errorData.Severity); } } diff --git a/src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs b/src/ModVerify/Reporting/Engine/GameAssertErrorReporter.cs similarity index 93% rename from src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs rename to src/ModVerify/Reporting/Engine/GameAssertErrorReporter.cs index 6c5fb17..f75334c 100644 --- a/src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs +++ b/src/ModVerify/Reporting/Engine/GameAssertErrorReporter.cs @@ -5,12 +5,12 @@ using PG.StarWarsGame.Engine.ErrorReporting; using PG.StarWarsGame.Engine.IO; -namespace AET.ModVerify.Reporting.Reporters.Engine; +namespace AET.ModVerify.Reporting.Engine; internal sealed class GameAssertErrorReporter(IGameRepository gameRepository, IServiceProvider serviceProvider) : EngineErrorReporterBase(gameRepository, serviceProvider) { - public override string Name => "GameAsserts"; + public override string FriendlyName => "Game Engine Asserts"; protected override ErrorData CreateError(EngineAssert assert) { diff --git a/src/ModVerify/Reporting/ConcurrentGameEngineErrorReporter.cs b/src/ModVerify/Reporting/Engine/GameEngineErrorCollection.cs similarity index 50% rename from src/ModVerify/Reporting/ConcurrentGameEngineErrorReporter.cs rename to src/ModVerify/Reporting/Engine/GameEngineErrorCollection.cs index 3130547..ba0abf5 100644 --- a/src/ModVerify/Reporting/ConcurrentGameEngineErrorReporter.cs +++ b/src/ModVerify/Reporting/Engine/GameEngineErrorCollection.cs @@ -3,9 +3,9 @@ using System.Linq; using PG.StarWarsGame.Engine.ErrorReporting; -namespace AET.ModVerify.Reporting; +namespace AET.ModVerify.Reporting.Engine; -public sealed class ConcurrentGameEngineErrorReporter : GameEngineErrorReporter, IGameEngineErrorCollection +public sealed class GameEngineErrorCollection : IGameEngineErrorCollection, IGameEngineErrorReporter { private readonly ConcurrentBag _xmlErrors = new(); private readonly ConcurrentBag _initializationErrors = new(); @@ -17,18 +17,36 @@ public sealed class ConcurrentGameEngineErrorReporter : GameEngineErrorReporter, public IEnumerable Asserts => _asserts.ToList(); - public override void Report(XmlError error) + void IGameEngineErrorReporter.Report(XmlError error) { _xmlErrors.Add(error); } - public override void Report(InitializationError error) + void IGameEngineErrorReporter.Report(InitializationError error) { _initializationErrors.Add(error); } - public override void Assert(EngineAssert assert) + void IGameEngineErrorReporter.Assert(EngineAssert assert) { _asserts.Add(assert); } + + internal void Clear() + { +#if !NETFRAMEWORK && !NETSTANDARD2_0 + _xmlErrors.Clear(); + _initializationErrors.Clear(); + _asserts.Clear(); +#else + ClearUnsafe(_xmlErrors); + ClearUnsafe(_initializationErrors); + ClearUnsafe(_asserts); + + static void ClearUnsafe(ConcurrentBag bag) + { + while (bag.TryTake(out _)) ; + } +#endif + } } \ No newline at end of file diff --git a/src/ModVerify/Reporting/IGameEngineErrorCollection.cs b/src/ModVerify/Reporting/Engine/IGameEngineErrorCollection.cs similarity index 87% rename from src/ModVerify/Reporting/IGameEngineErrorCollection.cs rename to src/ModVerify/Reporting/Engine/IGameEngineErrorCollection.cs index 14d59d9..adf734d 100644 --- a/src/ModVerify/Reporting/IGameEngineErrorCollection.cs +++ b/src/ModVerify/Reporting/Engine/IGameEngineErrorCollection.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using PG.StarWarsGame.Engine.ErrorReporting; -namespace AET.ModVerify.Reporting; +namespace AET.ModVerify.Reporting.Engine; public interface IGameEngineErrorCollection { diff --git a/src/ModVerify/Reporting/Reporters/Engine/InitializationErrorReporter.cs b/src/ModVerify/Reporting/Engine/InitializationErrorReporter.cs similarity index 84% rename from src/ModVerify/Reporting/Reporters/Engine/InitializationErrorReporter.cs rename to src/ModVerify/Reporting/Engine/InitializationErrorReporter.cs index fc44c66..e46db1a 100644 --- a/src/ModVerify/Reporting/Reporters/Engine/InitializationErrorReporter.cs +++ b/src/ModVerify/Reporting/Engine/InitializationErrorReporter.cs @@ -3,12 +3,12 @@ using PG.StarWarsGame.Engine.ErrorReporting; using PG.StarWarsGame.Engine.IO; -namespace AET.ModVerify.Reporting.Reporters.Engine; +namespace AET.ModVerify.Reporting.Engine; internal sealed class InitializationErrorReporter(IGameRepository gameRepository, IServiceProvider serviceProvider) : EngineErrorReporterBase(gameRepository, serviceProvider) { - public override string Name => "InitializationErrors"; + public override string FriendlyName => "Initialization Errors"; protected override ErrorData CreateError(InitializationError error) { diff --git a/src/ModVerify/Reporting/Reporters/Engine/XmlParseErrorReporter.cs b/src/ModVerify/Reporting/Engine/XmlParseErrorReporter.cs similarity index 97% rename from src/ModVerify/Reporting/Reporters/Engine/XmlParseErrorReporter.cs rename to src/ModVerify/Reporting/Engine/XmlParseErrorReporter.cs index 094a701..2629f26 100644 --- a/src/ModVerify/Reporting/Reporters/Engine/XmlParseErrorReporter.cs +++ b/src/ModVerify/Reporting/Engine/XmlParseErrorReporter.cs @@ -8,14 +8,14 @@ using PG.StarWarsGame.Engine.IO; using PG.StarWarsGame.Files.XML.ErrorHandling; -namespace AET.ModVerify.Reporting.Reporters.Engine; +namespace AET.ModVerify.Reporting.Engine; internal sealed class XmlParseErrorReporter(IGameRepository gameRepository, IServiceProvider serviceProvider) : EngineErrorReporterBase(gameRepository, serviceProvider) { private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); - public override string Name => "XMLError"; + public override string FriendlyName => "XML Errors"; protected override ErrorData CreateError(XmlError error) { diff --git a/src/ModVerify/Reporting/IVerificationReporter.cs b/src/ModVerify/Reporting/IVerificationReporter.cs deleted file mode 100644 index 8b8664e..0000000 --- a/src/ModVerify/Reporting/IVerificationReporter.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace AET.ModVerify.Reporting; - -public interface IVerificationReporter -{ - public Task ReportAsync(IReadOnlyCollection errors); -} \ No newline at end of file diff --git a/src/ModVerify/Reporting/InvalidBaselineException.cs b/src/ModVerify/Reporting/InvalidBaselineException.cs deleted file mode 100644 index 37ab9c8..0000000 --- a/src/ModVerify/Reporting/InvalidBaselineException.cs +++ /dev/null @@ -1,14 +0,0 @@ -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/JsonAggregatedVerificationError.cs b/src/ModVerify/Reporting/Json/JsonAggregatedVerificationError.cs new file mode 100644 index 0000000..5a9cbb3 --- /dev/null +++ b/src/ModVerify/Reporting/Json/JsonAggregatedVerificationError.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AET.ModVerify.Reporting.Json; + +internal class JsonAggregatedVerificationError : JsonVerificationErrorBase +{ + [JsonPropertyName("contexts")] + [JsonPropertyOrder(99)] + public IEnumerable> Contexts { get; } + + [JsonConstructor] + public JsonAggregatedVerificationError( + string id, + IReadOnlyList? verifierChain, + string message, + VerificationSeverity severity, + IEnumerable>? contexts, + string? asset) : base(id, verifierChain, message, severity, asset) + { + Contexts = contexts ?? []; + } + + public JsonAggregatedVerificationError( + VerificationError error, + IEnumerable> contexts) : base(error) + { + Contexts = contexts; + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonVerificationError.cs b/src/ModVerify/Reporting/Json/JsonVerificationError.cs index 55f7b92..0e23283 100644 --- a/src/ModVerify/Reporting/Json/JsonVerificationError.cs +++ b/src/ModVerify/Reporting/Json/JsonVerificationError.cs @@ -1,53 +1,29 @@ using System.Collections.Generic; +using System.Linq; using System.Text.Json.Serialization; namespace AET.ModVerify.Reporting.Json; -internal class JsonVerificationError +internal class JsonVerificationError : JsonVerificationErrorBase { - [JsonPropertyName("id")] - public string Id { get; } - - [JsonPropertyName("verifiers")] - public IReadOnlyList VerifierChain { get; } - - [JsonPropertyName("message")] - public string Message { get; } - - [JsonPropertyName("severity")] - [JsonConverter(typeof(JsonStringEnumConverter))] - public VerificationSeverity Severity { get; } - [JsonPropertyName("context")] - public IEnumerable ContextEntries { get; } - - [JsonPropertyName("asset")] - public string Asset { get; } + [JsonPropertyOrder(99)] + public IEnumerable? ContextEntries { get; } [JsonConstructor] - private JsonVerificationError( + public JsonVerificationError( string id, IReadOnlyList? verifierChain, string message, VerificationSeverity severity, IEnumerable? contextEntries, - string? asset) + string? asset) : base(id, verifierChain, message, severity, asset) { - Id = id; - VerifierChain = verifierChain ?? []; - Message = message; - Severity = severity; - ContextEntries = contextEntries ?? []; - Asset = asset ?? string.Empty; + ContextEntries = contextEntries; } - public JsonVerificationError(VerificationError error) + public JsonVerificationError(VerificationError error) : base(error) { - Id = error.Id; - VerifierChain = error.VerifierChain; - Message = error.Message; - Severity = error.Severity; - ContextEntries = error.ContextEntries; - Asset = error.Asset; + ContextEntries = error.ContextEntries.Any() ? error.ContextEntries : null; } } \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonVerificationErrorBase.cs b/src/ModVerify/Reporting/Json/JsonVerificationErrorBase.cs new file mode 100644 index 0000000..06af012 --- /dev/null +++ b/src/ModVerify/Reporting/Json/JsonVerificationErrorBase.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace AET.ModVerify.Reporting.Json; + +[JsonDerivedType(typeof(JsonVerificationError))] +[JsonDerivedType(typeof(JsonAggregatedVerificationError))] +internal abstract class JsonVerificationErrorBase +{ + [JsonPropertyName("id")] + public string Id { get; } + + [JsonPropertyName("verifiers")] + public IReadOnlyList VerifierChain { get; } + + [JsonPropertyName("message")] + public string Message { get; } + + [JsonPropertyName("severity")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public VerificationSeverity Severity { get; } + + [JsonPropertyName("asset")] + public string Asset { get; } + + protected JsonVerificationErrorBase( + string id, + IReadOnlyList? verifierChain, + string message, + VerificationSeverity severity, + string? asset) + { + Id = id; + VerifierChain = verifierChain ?? []; + Message = message; + Severity = severity; + Asset = asset ?? string.Empty; + } + + protected JsonVerificationErrorBase(VerificationError error) + { + Id = error.Id; + VerifierChain = error.VerifierChain.Select(x => x.Name).ToList(); + Message = error.Message; + Severity = error.Severity; + Asset = error.Asset; + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonVerificationReport.cs b/src/ModVerify/Reporting/Json/JsonVerificationReport.cs index 6ebb926..d10d18f 100644 --- a/src/ModVerify/Reporting/Json/JsonVerificationReport.cs +++ b/src/ModVerify/Reporting/Json/JsonVerificationReport.cs @@ -3,8 +3,11 @@ namespace AET.ModVerify.Reporting.Json; -internal class JsonVerificationReport(IEnumerable errors) +internal class JsonVerificationReport { + [JsonPropertyName("metadata")] + public required JsonVerificationReportMetadata Metadata { get; init; } + [JsonPropertyName("errors")] - public IEnumerable Errors { get; } = errors; + public required IEnumerable Errors { get; init; } } \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonVerificationReportMetadata.cs b/src/ModVerify/Reporting/Json/JsonVerificationReportMetadata.cs new file mode 100644 index 0000000..7e3b90e --- /dev/null +++ b/src/ModVerify/Reporting/Json/JsonVerificationReportMetadata.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AET.ModVerify.Reporting.Json; + +internal class JsonVerificationReportMetadata +{ + [JsonPropertyName("status")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public VerificationCompletionStatus Status { get; init; } + + [JsonPropertyName("target")] + public JsonVerificationTarget Target { get; init; } + + [JsonPropertyName("time")] + public string Date { get; } = DateTime.Now.ToString("s"); + + [JsonPropertyName("duration")] + public string Duration { get; init; } + + [JsonPropertyName("verifiers")] + public IReadOnlyCollection Verifiers { get; init; } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonVerificationTarget.cs b/src/ModVerify/Reporting/Json/JsonVerificationTarget.cs index 9495456..fe7e33d 100644 --- a/src/ModVerify/Reporting/Json/JsonVerificationTarget.cs +++ b/src/ModVerify/Reporting/Json/JsonVerificationTarget.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using AET.ModVerify.Reporting.Baseline; using PG.StarWarsGame.Engine; namespace AET.ModVerify.Reporting.Json; @@ -38,6 +39,15 @@ private JsonVerificationTarget( IsGame = isGame; } + public JsonVerificationTarget(VerificationTarget target) + { + Name = target.Name; + Version = target.Version; + Engine = target.Engine; + Location = new JsonGameLocation(target.Location); + IsGame = target.IsGame; + } + public JsonVerificationTarget(BaselineVerificationTarget target) { Name = target.Name; diff --git a/src/ModVerify/Reporting/Reporters/ConsoleReporter.cs b/src/ModVerify/Reporting/Reporters/Console/ConsoleReporter.cs similarity index 62% rename from src/ModVerify/Reporting/Reporters/ConsoleReporter.cs rename to src/ModVerify/Reporting/Reporters/Console/ConsoleReporter.cs index dbee50d..5266550 100644 --- a/src/ModVerify/Reporting/Reporters/ConsoleReporter.cs +++ b/src/ModVerify/Reporting/Reporters/Console/ConsoleReporter.cs @@ -2,25 +2,21 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using AET.ModVerify.Reporting.Settings; namespace AET.ModVerify.Reporting.Reporters; -internal class ConsoleReporter( - ReporterSettings settings, - bool summaryOnly, - IServiceProvider serviceProvider) : - ReporterBase(settings, serviceProvider) +internal class ConsoleReporter(ConsoleReporterSettings settings, IServiceProvider serviceProvider) : + ReporterBase(settings, serviceProvider) { - public override Task ReportAsync(IReadOnlyCollection errors) + public override Task ReportAsync(VerificationResult verificationResult) { - var filteredErrors = FilteredErrors(errors).OrderByDescending(x => x.Severity).ToList(); - PrintErrorStats(errors, filteredErrors); + var filteredErrors = FilteredErrors(verificationResult.Errors).OrderByDescending(x => x.Severity).ToList(); + PrintErrorStats(verificationResult, filteredErrors); Console.WriteLine(); return Task.CompletedTask; } - private void PrintErrorStats(IReadOnlyCollection errors, List filteredErrors) + private void PrintErrorStats(VerificationResult verificationResult, List filteredErrors) { Console.WriteLine(); Console.WriteLine(); @@ -28,9 +24,9 @@ private void PrintErrorStats(IReadOnlyCollection errors, List Console.WriteLine(" Error Report "); Console.WriteLine("***********************"); Console.WriteLine(); - if (errors.Count == 0) + if (verificationResult.Errors.Count == 0) { - if (summaryOnly) + if (Settings.SummaryOnly) { Console.WriteLine("No errors found."); } @@ -44,21 +40,21 @@ private void PrintErrorStats(IReadOnlyCollection errors, List return; } - Console.WriteLine($"TOTAL Verification Errors: {errors.Count}"); + Console.WriteLine($"TOTAL Verification Errors: {verificationResult.Errors.Count}"); - var groupedBySeverity = errors.GroupBy(x => x.Severity); + var groupedBySeverity = verificationResult.Errors.GroupBy(x => x.Severity); foreach (var group in groupedBySeverity) Console.WriteLine($" Severity {group.Key}: {group.Count()}"); Console.WriteLine(); if (filteredErrors.Count == 0) { - if (errors.Count != 0) + if (verificationResult.Errors.Count != 0) Console.WriteLine("Some errors are not displayed to the console. Please check the created output files."); return; } - if (summaryOnly) + if (Settings.SummaryOnly) return; Console.WriteLine($"Below the list of errors with severity '{Settings.MinimumReportSeverity}' or higher:"); diff --git a/src/ModVerify/Reporting/Reporters/Console/ConsoleReporterSettings.cs b/src/ModVerify/Reporting/Reporters/Console/ConsoleReporterSettings.cs new file mode 100644 index 0000000..4dc2a77 --- /dev/null +++ b/src/ModVerify/Reporting/Reporters/Console/ConsoleReporterSettings.cs @@ -0,0 +1,6 @@ +namespace AET.ModVerify.Reporting.Reporters; + +public sealed record ConsoleReporterSettings : ReporterSettings +{ + public bool SummaryOnly { get; init; } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/ExtensionMethods.cs b/src/ModVerify/Reporting/Reporters/ExtensionMethods.cs new file mode 100644 index 0000000..480e956 --- /dev/null +++ b/src/ModVerify/Reporting/Reporters/ExtensionMethods.cs @@ -0,0 +1,44 @@ +using System; + +namespace AET.ModVerify.Reporting.Reporters; + +public static class ExtensionMethods +{ + extension(IVerificationReporter) + { + public static IVerificationReporter CreateJson(IServiceProvider serviceProvider) + { + return IVerificationReporter.CreateJson(new JsonReporterSettings(), serviceProvider); + } + + public static IVerificationReporter CreateJson(JsonReporterSettings settings, IServiceProvider serviceProvider) + { + return new JsonReporter(settings, serviceProvider); + } + + public static IVerificationReporter CreateText(IServiceProvider serviceProvider) + { + return IVerificationReporter.CreateText(new TextFileReporterSettings(), serviceProvider); + } + + public static IVerificationReporter CreateText(TextFileReporterSettings settings, IServiceProvider serviceProvider) + { + return new TextFileReporter(settings, serviceProvider); + } + + public static IVerificationReporter CreateConsole(IServiceProvider serviceProvider, bool summaryOnly = false) + { + var settings = new ConsoleReporterSettings + { + MinimumReportSeverity = VerificationSeverity.Error, + SummaryOnly = summaryOnly + }; + return IVerificationReporter.CreateConsole(settings, serviceProvider); + } + + public static IVerificationReporter CreateConsole(ConsoleReporterSettings settings, IServiceProvider serviceProvider) + { + return new ConsoleReporter(settings, serviceProvider); + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/FileBasedReporter.cs b/src/ModVerify/Reporting/Reporters/FileBasedReporter.cs index 1057bfa..4c60121 100644 --- a/src/ModVerify/Reporting/Reporters/FileBasedReporter.cs +++ b/src/ModVerify/Reporting/Reporters/FileBasedReporter.cs @@ -1,7 +1,6 @@ using System; using System.IO; using System.IO.Abstractions; -using AET.ModVerify.Reporting.Settings; using Microsoft.Extensions.DependencyInjection; namespace AET.ModVerify.Reporting.Reporters; diff --git a/src/ModVerify/Reporting/Settings/FileBasedReporterSettings.cs b/src/ModVerify/Reporting/Reporters/FileBasedReporterSettings.cs similarity index 85% rename from src/ModVerify/Reporting/Settings/FileBasedReporterSettings.cs rename to src/ModVerify/Reporting/Reporters/FileBasedReporterSettings.cs index 759a6ab..aa468fc 100644 --- a/src/ModVerify/Reporting/Settings/FileBasedReporterSettings.cs +++ b/src/ModVerify/Reporting/Reporters/FileBasedReporterSettings.cs @@ -1,6 +1,6 @@ using System; -namespace AET.ModVerify.Reporting.Settings; +namespace AET.ModVerify.Reporting.Reporters; public record FileBasedReporterSettings : ReporterSettings { diff --git a/src/ModVerify/Reporting/Reporters/IVerificationReporter.cs b/src/ModVerify/Reporting/Reporters/IVerificationReporter.cs new file mode 100644 index 0000000..5ce7a65 --- /dev/null +++ b/src/ModVerify/Reporting/Reporters/IVerificationReporter.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace AET.ModVerify.Reporting.Reporters; + +public interface IVerificationReporter +{ + public Task ReportAsync(VerificationResult verificationResult); +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs b/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs index 348fbb1..1feb655 100644 --- a/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs +++ b/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs @@ -4,21 +4,88 @@ using System.Text.Json; using System.Threading.Tasks; using AET.ModVerify.Reporting.Json; +using AET.ModVerify.Verifiers; +using AnakinRaW.CommonUtilities.FileSystem.Validation; -namespace AET.ModVerify.Reporting.Reporters.JSON; +namespace AET.ModVerify.Reporting.Reporters; internal class JsonReporter(JsonReporterSettings settings, IServiceProvider serviceProvider) : FileBasedReporter(settings, serviceProvider) { - public const string FileName = "VerificationResult.json"; - - public override async Task ReportAsync(IReadOnlyCollection errors) + public override async Task ReportAsync(VerificationResult verificationResult) { - var report = new JsonVerificationReport(errors.Select(x => new JsonVerificationError(x))); + var report = CreateJsonReport(verificationResult); + var fileName = CreateFileName(verificationResult); + #if NET || NETSTANDARD2_1 await #endif - using var fs = CreateFile(FileName); + using var fs = CreateFile(fileName); await JsonSerializer.SerializeAsync(fs, report, ModVerifyJsonSettings.JsonSettings); } + + private JsonVerificationReport CreateJsonReport(VerificationResult result) + { + IEnumerable errors; + if (Settings.AggregateResults) + { + errors = result.Errors + .GroupBy(x => new GroupKey(x.Asset, x.Id, x.VerifierChain)) + .Select, JsonVerificationErrorBase>(g => + { + var first = g.First(); + var contexts = g.Select(x => x.ContextEntries).ToList(); + + if (contexts.Count == 1) + return new JsonVerificationError(first); + return new JsonAggregatedVerificationError(first, contexts); + }); + } + else + { + errors = result.Errors.Select(x => new JsonVerificationError(x)); + } + + return new JsonVerificationReport + { + Metadata = new JsonVerificationReportMetadata + { + Target = new JsonVerificationTarget(result.Target), + Duration = result.Duration.ToString("g"), + Status = result.Status, + Verifiers = result.Verifiers.Select(x => x.Name).ToList() + }, + Errors = errors + }; + } + + private static string CreateFileName(VerificationResult result) + { + var fileName = $"VerificationResult_{result.Target.Name}.json"; + if (CurrentSystemFileNameValidator.Instance.IsValidFileName(fileName) is FileNameValidationResult.Valid) + return fileName; + // I don't think there is a safe/secure way to re-encode the file name, if it's not valid using the plain target name. + // Thus, we simply use the current date and accept the fact that files may get overwritten for different targets. + return $"VerificationResult_{DateTime.Now:yyyy_mm_dd}.json"; + + } + + private readonly record struct GroupKey(string Asset, string Id, IReadOnlyList VerifierChain) + { + public bool Equals(GroupKey other) + { + return Asset == other.Asset + && Id == other.Id + && VerifierChainEqualityComparer.Instance.Equals(VerifierChain, other.VerifierChain); + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + hashCode.Add(Asset); + hashCode.Add(Id); + hashCode.Add(VerifierChain, VerifierChainEqualityComparer.Instance); + return hashCode.ToHashCode(); + } + } } \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/JSON/JsonReporterSettings.cs b/src/ModVerify/Reporting/Reporters/JSON/JsonReporterSettings.cs index 4207b36..7ccbba9 100644 --- a/src/ModVerify/Reporting/Reporters/JSON/JsonReporterSettings.cs +++ b/src/ModVerify/Reporting/Reporters/JSON/JsonReporterSettings.cs @@ -1,5 +1,6 @@ -using AET.ModVerify.Reporting.Settings; +namespace AET.ModVerify.Reporting.Reporters; -namespace AET.ModVerify.Reporting.Reporters.JSON; - -public record JsonReporterSettings : FileBasedReporterSettings; \ No newline at end of file +public record JsonReporterSettings : FileBasedReporterSettings +{ + public bool AggregateResults { get; init; } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/ReporterBase.cs b/src/ModVerify/Reporting/Reporters/ReporterBase.cs index df444e3..47cafc5 100644 --- a/src/ModVerify/Reporting/Reporters/ReporterBase.cs +++ b/src/ModVerify/Reporting/Reporters/ReporterBase.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using AET.ModVerify.Reporting.Settings; namespace AET.ModVerify.Reporting.Reporters; @@ -12,7 +11,7 @@ public abstract class ReporterBase(T settings, IServiceProvider serviceProvid protected T Settings { get; } = settings ?? throw new ArgumentNullException(nameof(settings)); - public abstract Task ReportAsync(IReadOnlyCollection errors); + public abstract Task ReportAsync(VerificationResult verificationResult); protected IEnumerable FilteredErrors(IReadOnlyCollection errors) { diff --git a/src/ModVerify/Reporting/Settings/ReporterSettings.cs b/src/ModVerify/Reporting/Reporters/ReporterSettings.cs similarity index 74% rename from src/ModVerify/Reporting/Settings/ReporterSettings.cs rename to src/ModVerify/Reporting/Reporters/ReporterSettings.cs index ef33857..73332eb 100644 --- a/src/ModVerify/Reporting/Settings/ReporterSettings.cs +++ b/src/ModVerify/Reporting/Reporters/ReporterSettings.cs @@ -1,4 +1,4 @@ -namespace AET.ModVerify.Reporting.Settings; +namespace AET.ModVerify.Reporting.Reporters; public record ReporterSettings { diff --git a/src/ModVerify/Reporting/Reporters/Text/TextFileReporter.cs b/src/ModVerify/Reporting/Reporters/Text/TextFileReporter.cs index cfc5c91..f3e8126 100644 --- a/src/ModVerify/Reporting/Reporters/Text/TextFileReporter.cs +++ b/src/ModVerify/Reporting/Reporters/Text/TextFileReporter.cs @@ -1,49 +1,56 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using System.Threading.Tasks; -namespace AET.ModVerify.Reporting.Reporters.Text; +namespace AET.ModVerify.Reporting.Reporters; -internal class TextFileReporter(TextFileReporterSettings settings, IServiceProvider serviceProvider) : FileBasedReporter(settings, serviceProvider) +internal sealed class TextFileReporter(TextFileReporterSettings settings, IServiceProvider serviceProvider) + : FileBasedReporter(settings, serviceProvider) { internal const string SingleReportFileName = "VerificationResult.txt"; - public override async Task ReportAsync(IReadOnlyCollection errors) + public override async Task ReportAsync(VerificationResult verificationResult) { if (Settings.SplitIntoFiles) - await ReportByVerifier(errors); + await ReportByVerifier(verificationResult); else - await ReportWhole(errors); + await ReportWhole(verificationResult); } - private async Task ReportWhole(IReadOnlyCollection errors) + private async Task ReportWhole(VerificationResult result) { #if NET || NETSTANDARD2_1 await #endif using var streamWriter = new StreamWriter(CreateFile(SingleReportFileName)); - foreach (var error in errors.OrderBy(x => x.Id)) + await WriteHeader(result.Target, DateTime.Now, null, streamWriter); + + foreach (var error in result.Errors.OrderBy(x => x.Id)) await WriteError(error, streamWriter); } - private async Task ReportByVerifier(IReadOnlyCollection errors) + private async Task ReportByVerifier(VerificationResult result) { - var grouped = errors.GroupBy(x => x.VerifierChain.Last()); + var time = DateTime.Now; + var grouped = result.Errors.GroupBy(x => x.VerifierChain.Last().Name); foreach (var group in grouped) - await ReportToSingleFile(group); + await ReportToSingleFile(group, result.Target, time); } - private async Task ReportToSingleFile(IGrouping group) + private async Task ReportToSingleFile(IGrouping group, VerificationTarget target, DateTime time) { - var fileName = $"{GetVerifierName(group.Key)}Results.txt"; + var fileName = $"{GetVerifierName(group.Key)}_Results.txt"; #if NET || NETSTANDARD2_1 await #endif using var streamWriter = new StreamWriter(CreateFile(fileName)); + + await WriteHeader(target, time, group.Key, streamWriter); + foreach (var error in group.OrderBy(x => x.Id)) await WriteError(error, streamWriter); } @@ -52,23 +59,41 @@ private static string GetVerifierName(string verifierTypeName) { var typeNameSpan = verifierTypeName.AsSpan(); var nameIndex = typeNameSpan.LastIndexOf('.'); + var isSupType = typeNameSpan.IndexOf('/') > -1; - if (nameIndex == -1) + if (nameIndex == -1 && !isSupType) return verifierTypeName; - // They type name must not be empty - if (typeNameSpan.Length == nameIndex) - throw new InvalidOperationException(); - var name = typeNameSpan.Slice(nameIndex + 1); // Normalize subtypes (such as C/M) to avoid creating directories - if (name.IndexOf('/') != -1) + if (isSupType) return name.ToString().Replace('/', '.'); return name.ToString(); } + private static async Task WriteHeader( + VerificationTarget target, + DateTime time, + string? verifier, + StreamWriter writer) + { + var header = CreateHeader(target, time, verifier); + await writer.WriteLineAsync(header); + await writer.WriteLineAsync(); + } + + private static string CreateHeader(VerificationTarget target, DateTime time, string? verifier) + { + var sb = new StringBuilder(); + sb.Append($"# Target '{target.Name}'"); + if (!string.IsNullOrEmpty(verifier)) + sb.Append($"; Verifier: {verifier}"); + sb.Append($"; Time: {time:s}"); + return sb.ToString(); + } + private static async Task WriteError(VerificationError error, StreamWriter writer) { await writer.WriteLineAsync($"[{error.Id}] {error.Message}"); diff --git a/src/ModVerify/Reporting/Reporters/Text/TextFileReporterSettings.cs b/src/ModVerify/Reporting/Reporters/Text/TextFileReporterSettings.cs index 8fb833b..2d0e348 100644 --- a/src/ModVerify/Reporting/Reporters/Text/TextFileReporterSettings.cs +++ b/src/ModVerify/Reporting/Reporters/Text/TextFileReporterSettings.cs @@ -1,8 +1,6 @@ -using AET.ModVerify.Reporting.Settings; +namespace AET.ModVerify.Reporting.Reporters; -namespace AET.ModVerify.Reporting.Reporters.Text; - -public record TextFileReporterSettings : FileBasedReporterSettings +public sealed record TextFileReporterSettings : FileBasedReporterSettings { public bool SplitIntoFiles { get; init; } = true; } \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/VerificationReportBroker.cs b/src/ModVerify/Reporting/Reporters/VerificationReportBroker.cs new file mode 100644 index 0000000..0ea6630 --- /dev/null +++ b/src/ModVerify/Reporting/Reporters/VerificationReportBroker.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace AET.ModVerify.Reporting.Reporters; + +public sealed class VerificationReportBroker : IVerificationReporter +{ + private readonly ILogger? _logger; + private readonly IReadOnlyCollection _reporters; + + public VerificationReportBroker( + IReadOnlyCollection reporters, + IServiceProvider serviceProvider) + { + _reporters = reporters ?? throw new ArgumentNullException(nameof(reporters)); + _logger = serviceProvider.GetService()?.CreateLogger(typeof(VerificationReportBroker)); + } + + public async Task ReportAsync(VerificationResult result) + { + foreach (var reporter in _reporters) + { + try + { + await reporter.ReportAsync(result); + } + catch (Exception e) + { + _logger?.LogError(e, "Exception while reporting verification error"); + } + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs b/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs deleted file mode 100644 index b8906ac..0000000 --- a/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs +++ /dev/null @@ -1,52 +0,0 @@ -using AET.ModVerify.Reporting.Reporters.JSON; -using AET.ModVerify.Reporting.Reporters.Text; -using AET.ModVerify.Reporting.Settings; -using Microsoft.Extensions.DependencyInjection; - -namespace AET.ModVerify.Reporting.Reporters; - -public static class VerificationReportersExtensions -{ - extension(IServiceCollection serviceCollection) - { - public IServiceCollection RegisterJsonReporter() - { - return serviceCollection.RegisterJsonReporter(new JsonReporterSettings - { - OutputDirectory = "." - }); - } - - public IServiceCollection RegisterTextFileReporter() - { - return serviceCollection.RegisterTextFileReporter(new TextFileReporterSettings - { - OutputDirectory = "." - }); - } - - public IServiceCollection RegisterConsoleReporter(bool summaryOnly = false) - { - return serviceCollection.RegisterConsoleReporter(new ReporterSettings - { - MinimumReportSeverity = VerificationSeverity.Error - }, summaryOnly); - } - - public IServiceCollection RegisterJsonReporter(JsonReporterSettings settings) - { - return serviceCollection.AddSingleton(sp => new JsonReporter(settings, sp)); - } - - public IServiceCollection RegisterTextFileReporter(TextFileReporterSettings settings) - { - return serviceCollection.AddSingleton(sp => new TextFileReporter(settings, sp)); - } - - public IServiceCollection RegisterConsoleReporter(ReporterSettings settings, - bool summaryOnly = false) - { - return serviceCollection.AddSingleton(sp => new ConsoleReporter(settings, summaryOnly, sp)); - } - } -} \ No newline at end of file diff --git a/src/ModVerify/Reporting/RestoredVerifierInfo.cs b/src/ModVerify/Reporting/RestoredVerifierInfo.cs new file mode 100644 index 0000000..0c29488 --- /dev/null +++ b/src/ModVerify/Reporting/RestoredVerifierInfo.cs @@ -0,0 +1,10 @@ +using AET.ModVerify.Verifiers; + +namespace AET.ModVerify.Reporting; + +internal sealed class RestoredVerifierInfo : IGameVerifierInfo +{ + public IGameVerifierInfo? Parent { get; init; } + public required string Name { get; init; } + public string FriendlyName => Name; +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Settings/GlobalVerifyReportSettings.cs b/src/ModVerify/Reporting/Settings/GlobalVerifyReportSettings.cs deleted file mode 100644 index 5a51436..0000000 --- a/src/ModVerify/Reporting/Settings/GlobalVerifyReportSettings.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace AET.ModVerify.Reporting.Settings; - -//public record GlobalVerifyReportSettings -//{ -// //public VerificationSeverity MinimumReportSeverity { get; init; } = VerificationSeverity.Information; - -// 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/Json/JsonSuppressionFilter.cs b/src/ModVerify/Reporting/Suppressions/Json/JsonSuppressionFilter.cs similarity index 91% rename from src/ModVerify/Reporting/Json/JsonSuppressionFilter.cs rename to src/ModVerify/Reporting/Suppressions/Json/JsonSuppressionFilter.cs index 6d232c7..13f8335 100644 --- a/src/ModVerify/Reporting/Json/JsonSuppressionFilter.cs +++ b/src/ModVerify/Reporting/Suppressions/Json/JsonSuppressionFilter.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace AET.ModVerify.Reporting.Json; +namespace AET.ModVerify.Reporting.Suppressions.Json; internal class JsonSuppressionFilter(SuppressionFilter filter) { diff --git a/src/ModVerify/Reporting/Json/JsonSuppressionList.cs b/src/ModVerify/Reporting/Suppressions/Json/JsonSuppressionList.cs similarity index 88% rename from src/ModVerify/Reporting/Json/JsonSuppressionList.cs rename to src/ModVerify/Reporting/Suppressions/Json/JsonSuppressionList.cs index 1a8cb4a..54569eb 100644 --- a/src/ModVerify/Reporting/Json/JsonSuppressionList.cs +++ b/src/ModVerify/Reporting/Suppressions/Json/JsonSuppressionList.cs @@ -2,7 +2,7 @@ using System.Linq; using System.Text.Json.Serialization; -namespace AET.ModVerify.Reporting.Json; +namespace AET.ModVerify.Reporting.Suppressions.Json; internal class JsonSuppressionList { diff --git a/src/ModVerify/Reporting/SuppressionFilter.cs b/src/ModVerify/Reporting/Suppressions/SuppressionFilter.cs similarity index 93% rename from src/ModVerify/Reporting/SuppressionFilter.cs rename to src/ModVerify/Reporting/Suppressions/SuppressionFilter.cs index efac588..35a21d7 100644 --- a/src/ModVerify/Reporting/SuppressionFilter.cs +++ b/src/ModVerify/Reporting/Suppressions/SuppressionFilter.cs @@ -1,8 +1,8 @@ using System; using System.Linq; -using AET.ModVerify.Reporting.Json; +using AET.ModVerify.Reporting.Suppressions.Json; -namespace AET.ModVerify.Reporting; +namespace AET.ModVerify.Reporting.Suppressions; public sealed class SuppressionFilter : IEquatable { @@ -42,7 +42,7 @@ public bool Suppresses(VerificationError error) if (Verifier is not null) { - if (error.VerifierChain.Contains(Verifier)) + if (error.VerifierChain.Any(x => x.Name.Equals(Verifier))) suppresses = true; else return false; diff --git a/src/ModVerify/Reporting/SuppressionList.cs b/src/ModVerify/Reporting/Suppressions/SuppressionList.cs similarity index 96% rename from src/ModVerify/Reporting/SuppressionList.cs rename to src/ModVerify/Reporting/Suppressions/SuppressionList.cs index 12ebab4..a5fc8f9 100644 --- a/src/ModVerify/Reporting/SuppressionList.cs +++ b/src/ModVerify/Reporting/Suppressions/SuppressionList.cs @@ -5,8 +5,9 @@ using System.IO; using System.Linq; using System.Text.Json; +using AET.ModVerify.Reporting.Suppressions.Json; -namespace AET.ModVerify.Reporting; +namespace AET.ModVerify.Reporting.Suppressions; public sealed class SuppressionList : IReadOnlyCollection { diff --git a/src/ModVerify/Reporting/VerificationCompletionStatus.cs b/src/ModVerify/Reporting/VerificationCompletionStatus.cs new file mode 100644 index 0000000..94fde18 --- /dev/null +++ b/src/ModVerify/Reporting/VerificationCompletionStatus.cs @@ -0,0 +1,8 @@ +namespace AET.ModVerify.Reporting; + +public enum VerificationCompletionStatus +{ + Completed, + CompletedFailFast, + Cancelled, +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/VerificationError.cs b/src/ModVerify/Reporting/VerificationError.cs index 538a8fd..d55f71b 100644 --- a/src/ModVerify/Reporting/VerificationError.cs +++ b/src/ModVerify/Reporting/VerificationError.cs @@ -17,7 +17,7 @@ public sealed class VerificationError : IEquatable public string Message { get; } - public IReadOnlyList VerifierChain { get; } + public IReadOnlyList VerifierChain { get; } public IReadOnlyCollection ContextEntries { get; } @@ -28,7 +28,7 @@ public sealed class VerificationError : IEquatable public VerificationError( string id, string message, - IReadOnlyList verifiers, + IReadOnlyList verifiers, IEnumerable contextEntries, string asset, VerificationSeverity severity) @@ -41,10 +41,9 @@ public VerificationError( Id = id; Message = message ?? throw new ArgumentNullException(nameof(message)); - VerifierChain = verifiers; + VerifierChain = [.. verifiers]; Severity = severity; - _contextEntries = [.. contextEntries]; - ContextEntries = _contextEntries.ToList(); + ContextEntries = _contextEntries = [.. contextEntries]; Asset = asset; } @@ -52,7 +51,7 @@ internal VerificationError(JsonVerificationError error) { Id = error.Id; Message = error.Message; - VerifierChain = error.VerifierChain; + VerifierChain = RestoreVerifierChain(error.VerifierChain); _contextEntries = [..error.ContextEntries]; ContextEntries = _contextEntries.ToList(); Asset = error.Asset; @@ -66,7 +65,7 @@ public static VerificationError Create( IEnumerable context, string asset) { - return new VerificationError(id, message, verifiers.Select(x => x.Name).ToList(), context, asset, severity); + return new VerificationError(id, message, verifiers, context, asset, severity); } public static VerificationError Create( @@ -79,7 +78,7 @@ public static VerificationError Create( return new VerificationError( id, message, - verifiers.Select(x => x.Name).ToList(), + verifiers, [], asset, severity); @@ -120,4 +119,23 @@ public override string ToString() return $"[{Severity}] [{string.Join(" --> ", VerifierChain)}] " + $"{Id}: Message={Message}; Asset='{Asset}'; Context=[{string.Join(",", ContextEntries)}];"; } + + private static IReadOnlyList RestoreVerifierChain(IReadOnlyList errorVerifierChain) + { + var verifierChain = new List(); + IGameVerifierInfo? previousVerifier = null; + + foreach (var name in errorVerifierChain) + { + var verifier = new RestoredVerifierInfo + { + Name = name, + Parent = previousVerifier + }; + verifierChain.Add(verifier); + previousVerifier = verifier; + } + + return verifierChain; + } } \ No newline at end of file diff --git a/src/ModVerify/Reporting/VerificationReportBroker.cs b/src/ModVerify/Reporting/VerificationReportBroker.cs deleted file mode 100644 index 5c1e8a3..0000000 --- a/src/ModVerify/Reporting/VerificationReportBroker.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace AET.ModVerify.Reporting; - -public sealed class VerificationReportBroker(IServiceProvider serviceProvider) -{ - private readonly ILogger? _logger = serviceProvider.GetService()?.CreateLogger(typeof(VerificationReportBroker)); - - public async Task ReportAsync(IReadOnlyCollection errors) - { - var reporters = serviceProvider.GetServices(); - - foreach (var reporter in reporters) - { - try - { - await reporter.ReportAsync(errors); - } - catch (Exception e) - { - _logger?.LogError(e, "Exception while reporting verification error"); - } - } - } -} \ No newline at end of file diff --git a/src/ModVerify/Reporting/VerificationResult.cs b/src/ModVerify/Reporting/VerificationResult.cs new file mode 100644 index 0000000..a4dbe1c --- /dev/null +++ b/src/ModVerify/Reporting/VerificationResult.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using AET.ModVerify.Reporting.Baseline; +using AET.ModVerify.Reporting.Suppressions; +using AET.ModVerify.Verifiers; + +namespace AET.ModVerify.Reporting; + +public sealed record VerificationResult +{ + public required VerificationCompletionStatus Status { get; init; } + + public required IReadOnlyCollection Errors + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } + + public required VerificationBaseline UsedBaseline + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } + + public required SuppressionList UsedSuppressions + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } + + public required IReadOnlyCollection Verifiers + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } + + public required VerificationTarget Target + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } + + public required TimeSpan Duration { get; init; } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/VerifierChainEqualityComparer.cs b/src/ModVerify/Reporting/VerifierChainEqualityComparer.cs new file mode 100644 index 0000000..abb01ef --- /dev/null +++ b/src/ModVerify/Reporting/VerifierChainEqualityComparer.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using AET.ModVerify.Verifiers; + +namespace AET.ModVerify.Reporting; + +internal sealed class VerifierChainEqualityComparer : IEqualityComparer> +{ + public static readonly VerifierChainEqualityComparer Instance = new(); + + private VerifierChainEqualityComparer() + { + } + + public bool Equals(IReadOnlyList? x, IReadOnlyList? y) + { + if (ReferenceEquals(x, y)) + return true; + if (x is null || y is null) + return false; + if (x.Count != y.Count) + return false; + for (var i = 0; i < x.Count; i++) + { + if (!NameBasedEqualityComparer.Instance.Equals(x[i], y[i])) + return false; + } + return true; + } + + public int GetHashCode(IReadOnlyList? obj) + { + if (obj == null) + return 0; + var hashCode = new HashCode(); + foreach (var verifier in obj) + hashCode.Add(verifier.Name); + return hashCode.ToHashCode(); + } +} diff --git a/src/ModVerify/Settings/VerifyPipelineSettings.cs b/src/ModVerify/Settings/VerifierServiceSettings.cs similarity index 72% rename from src/ModVerify/Settings/VerifyPipelineSettings.cs rename to src/ModVerify/Settings/VerifierServiceSettings.cs index 46fc997..762806d 100644 --- a/src/ModVerify/Settings/VerifyPipelineSettings.cs +++ b/src/ModVerify/Settings/VerifierServiceSettings.cs @@ -1,8 +1,6 @@ -using AET.ModVerify.Pipeline; +namespace AET.ModVerify.Settings; -namespace AET.ModVerify.Settings; - -public sealed class VerifyPipelineSettings +public sealed class VerifierServiceSettings { public required GameVerifySettings GameVerifySettings { get; init; } diff --git a/src/ModVerify/Utilities/VerificationErrorExtensions.cs b/src/ModVerify/Utilities/VerificationErrorExtensions.cs index e5aa9fe..f7eefbc 100644 --- a/src/ModVerify/Utilities/VerificationErrorExtensions.cs +++ b/src/ModVerify/Utilities/VerificationErrorExtensions.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using AET.ModVerify.Reporting; +using AET.ModVerify.Reporting.Baseline; +using AET.ModVerify.Reporting.Suppressions; namespace AET.ModVerify.Utilities; diff --git a/src/ModVerify/VerificationTarget.cs b/src/ModVerify/VerificationTarget.cs index c0d5b97..f12bc4d 100644 --- a/src/ModVerify/VerificationTarget.cs +++ b/src/ModVerify/VerificationTarget.cs @@ -1,10 +1,9 @@ using System; -using System.Text; using PG.StarWarsGame.Engine; namespace AET.ModVerify; -public sealed class VerificationTarget +public sealed record VerificationTarget { public required GameEngineType Engine { get; init; } @@ -28,14 +27,4 @@ public required GameLocations Location 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/GameEngineErrorCollector.cs b/src/ModVerify/Verifiers/GameEngineErrorCollector.cs index 6475ea7..975e360 100644 --- a/src/ModVerify/Verifiers/GameEngineErrorCollector.cs +++ b/src/ModVerify/Verifiers/GameEngineErrorCollector.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Threading; using AET.ModVerify.Reporting; -using AET.ModVerify.Reporting.Reporters.Engine; +using AET.ModVerify.Reporting.Engine; using AET.ModVerify.Settings; using PG.StarWarsGame.Engine; @@ -12,7 +12,8 @@ public sealed class GameEngineErrorCollector( IGameEngineErrorCollection errorCollection, IStarWarsGameEngine gameEngine, GameVerifySettings settings, - IServiceProvider serviceProvider) : GameVerifier(null, gameEngine, settings, serviceProvider) + IServiceProvider serviceProvider) + : GameVerifier(null, gameEngine, settings, serviceProvider) { public override string FriendlyName => "Game Engine Initialization"; diff --git a/src/ModVerify/Verifiers/GameVerifierBase.cs b/src/ModVerify/Verifiers/GameVerifierBase.cs index 02bacc4..22b2789 100644 --- a/src/ModVerify/Verifiers/GameVerifierBase.cs +++ b/src/ModVerify/Verifiers/GameVerifierBase.cs @@ -7,7 +7,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO.Abstractions; -using AET.ModVerify.Pipeline.Progress; +using AET.ModVerify.Progress; using PG.StarWarsGame.Engine; namespace AET.ModVerify.Verifiers; diff --git a/src/ModVerify/Verifiers/NameBasedEqualityComparer.cs b/src/ModVerify/Verifiers/NameBasedEqualityComparer.cs new file mode 100644 index 0000000..01f396f --- /dev/null +++ b/src/ModVerify/Verifiers/NameBasedEqualityComparer.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace AET.ModVerify.Verifiers; + +internal sealed class NameBasedEqualityComparer : IEqualityComparer, IEqualityComparer +{ + public static readonly NameBasedEqualityComparer Instance = new(); + + public bool Equals(GameVerifier? x, GameVerifier? y) + { + return Equals(x as IGameVerifierInfo, y); + } + + public int GetHashCode(GameVerifier? obj) + { + return GetHashCode(obj as IGameVerifierInfo); + } + + public bool Equals(IGameVerifierInfo? x, IGameVerifierInfo? y) + { + if (ReferenceEquals(x, y)) + return true; + if (x is null) + return false; + if (y is null) + return false; + return x.Name == y.Name; + } + + public int GetHashCode(IGameVerifierInfo? obj) + { + return obj?.Name.GetHashCode() ?? 0; + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/ReferencedModelsVerifier.cs b/src/ModVerify/Verifiers/ReferencedModelsVerifier.cs index a36648b..4a5feb6 100644 --- a/src/ModVerify/Verifiers/ReferencedModelsVerifier.cs +++ b/src/ModVerify/Verifiers/ReferencedModelsVerifier.cs @@ -17,24 +17,37 @@ public sealed class ReferencedModelsVerifier( public override void Verify(CancellationToken token) { - var models = GameEngine.GameObjectTypeManager.Entries - .SelectMany(x => x.Models) - .Concat(FocHardcodedConstants.HardcodedModels).ToList(); + var gameObjectEntries = GameEngine.GameObjectTypeManager.Entries.ToList(); + var hardcodedModels = FocHardcodedConstants.HardcodedModels.ToList(); - if (models.Count == 0) + var totalModelsCount = gameObjectEntries.Sum(x => x.Models.Count()) + hardcodedModels.Count; + + if (totalModelsCount == 0) return; - var numModels = models.Count; var counter = 0; var inner = new SingleModelVerifier(this, GameEngine, Settings, Services); try { inner.Error += OnModelError; - foreach (var model in models) + + var context = new string[1]; + foreach (var gameObject in gameObjectEntries) + { + context[0] = $"GameObject: {gameObject.Name}"; + foreach (var model in gameObject.Models) + { + OnProgress((double)++counter / totalModelsCount, $"Model - '{model}'"); + inner.Verify(model, context, token); + } + } + + context[0] = "Hardcoded Model"; + foreach (var hardcodedModel in hardcodedModels) { - OnProgress((double)++counter / numModels, $"Model - '{model}'"); - inner.Verify(model, [], token); + OnProgress((double)++counter / totalModelsCount, $"Model - '{hardcodedModel}'"); + inner.Verify(hardcodedModel, context, token); } } finally @@ -47,4 +60,4 @@ private void OnModelError(object sender, VerificationErrorEventArgs e) { AddError(e.Error); } -} +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/GameEngineErrorReporter.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/GameEngineErrorReporter.cs deleted file mode 100644 index 0030344..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/GameEngineErrorReporter.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace PG.StarWarsGame.Engine.ErrorReporting; - -public abstract class GameEngineErrorReporter : IGameEngineErrorReporter -{ - public virtual void Report(XmlError error) - { - } - - public virtual void Report(InitializationError error) - { - } - - public virtual void Assert(EngineAssert assert) - { - } -} \ 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 326fd1d..47b3a7f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj @@ -39,7 +39,4 @@ - - - \ 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 0d071ca..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 5df0123..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 @@ -19,7 +19,4 @@ - - - \ 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 be85ee8..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 @@ -26,7 +26,4 @@ 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 ef3c9ef..58914c1 100644 --- a/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj +++ b/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj @@ -35,10 +35,6 @@ - - - - true true diff --git a/test/ModVerify.CliApp.Test/TestData/NoVerifierProvider.cs b/test/ModVerify.CliApp.Test/TestData/NoVerifierProvider.cs index ef7ad74..649aeb7 100644 --- a/test/ModVerify.CliApp.Test/TestData/NoVerifierProvider.cs +++ b/test/ModVerify.CliApp.Test/TestData/NoVerifierProvider.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using AET.ModVerify.Pipeline; +using AET.ModVerify; using AET.ModVerify.Settings; using AET.ModVerify.Verifiers; using PG.StarWarsGame.Engine; From def81515a0c5340334c14cbdb6d37eaec5ea2392 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:19:48 +0100 Subject: [PATCH 09/14] Bump dotnet/nbgv from 0.4.2 to 0.5.0 in the actions-deps group (#36) Bumps the actions-deps group with 1 update: [dotnet/nbgv](https://github.com/dotnet/nbgv). Updates `dotnet/nbgv` from 0.4.2 to 0.5.0 - [Release notes](https://github.com/dotnet/nbgv/releases) - [Commits](https://github.com/dotnet/nbgv/compare/v0.4.2...v0.5.0) --- updated-dependencies: - dependency-name: dotnet/nbgv dependency-version: 0.5.0 dependency-type: direct:production update-type: version-update:semver-minor 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 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be274ae..444b06b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -103,7 +103,7 @@ jobs: # Change into the artifacts directory to avoid including the directory itself in the zip archive working-directory: ./releases/net10.0 run: zip -r ../ModVerify-Net10.zip . - - uses: dotnet/nbgv@v0.4.2 + - uses: dotnet/nbgv@v0.5.0 id: nbgv - name: Create GitHub release # Create a GitHub release on push to main only From f2487f87a214751a8c96f099fd0e6e5a70c53c29 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:58:28 +0100 Subject: [PATCH 10/14] Bump the actions-deps group with 3 updates (#37) Bumps the actions-deps group with 3 updates: [actions/upload-artifact](https://github.com/actions/upload-artifact), [actions/download-artifact](https://github.com/actions/download-artifact) and [dotnet/nbgv](https://github.com/dotnet/nbgv). Updates `actions/upload-artifact` from 6 to 7 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v6...v7) Updates `actions/download-artifact` from 7 to 8 - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v7...v8) Updates `dotnet/nbgv` from 0.5.0 to 0.5.1 - [Release notes](https://github.com/dotnet/nbgv/releases) - [Commits](https://github.com/dotnet/nbgv/compare/v0.5.0...v0.5.1) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps - dependency-name: actions/download-artifact dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps - dependency-name: dotnet/nbgv dependency-version: 0.5.1 dependency-type: direct:production update-type: version-update:semver-patch 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 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 444b06b..2d7d7f3 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@v6 + uses: actions/upload-artifact@v7 with: name: Binary Releases path: ./releases @@ -68,7 +68,7 @@ jobs: with: fetch-depth: 0 submodules: recursive - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: name: Binary Releases path: ./releases @@ -103,7 +103,7 @@ jobs: # Change into the artifacts directory to avoid including the directory itself in the zip archive working-directory: ./releases/net10.0 run: zip -r ../ModVerify-Net10.zip . - - uses: dotnet/nbgv@v0.5.0 + - uses: dotnet/nbgv@v0.5.1 id: nbgv - name: Create GitHub release # Create a GitHub release on push to main only From 1af951ac38ca6e16f29b91d0669fd5f2f2d730f0 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Wed, 25 Mar 2026 13:00:55 +0100 Subject: [PATCH 11/14] Support Variant of existing type for GameObjects (#35) * report parsing errors of xml file list * rename types * start refactoring xml parsers * started reorganization * new xml error reporting * refactor xml lib * refactor * improve error reports * unify checks and filelist parser behaves like engine * fix parsers * support tags with child nodes * huge parser refactoring * simplified animation type code * refactor commandbar parser * Shell scale and offset calculation * update deps and modules * move some comments * start implementing correct gameobject parser * fix bugs in parser and start engine compliant gameobject parsing * parsing is engine type aware * load base types * GameObjectType PostLoadFixup * order json by erorr ID * update deps * refactorings of verifiers * refactoring sfx verifier * implement xref and models check for gameobjects * refactor ctors * refactor verifierchain * hardcoded assets verifier * update deps * refactorings and new some new verifications * Verify GameObject icons * verify entity name length * support CompanyUnits * change order of json properties * make verifier chain an optional feature for reporting * perf opted * improve precision for gui textures verifier * update baseline * update deps --- Directory.Build.props | 2 +- modules/ModdingToolBase | 2 +- src/ModVerify.CliApp/App/VerifyAction.cs | 5 +- src/ModVerify.CliApp/ModVerify.CliApp.csproj | 24 +- src/ModVerify.CliApp/Program.cs | 6 +- .../Properties/launchSettings.json | 6 +- .../Resources/Baselines/baseline-foc.json | 3744 ++++++++--------- src/ModVerify/DefaultGameVerifiersProvider.cs | 23 +- src/ModVerify/GameVerifyPipeline.cs | 2 + src/ModVerify/IGameVerifiersProvider.cs | 2 +- src/ModVerify/ModVerify.csproj | 10 +- src/ModVerify/ModVerify.csproj.DotSettings | 7 +- src/ModVerify/ModVerifyServiceExtensions.cs | 4 +- .../Baseline/VerificationBaseline.cs | 3 +- .../Engine/EngineErrorReporterBase.cs | 18 +- .../Engine/GameAssertErrorReporter.cs | 8 +- .../Engine/GameEngineErrorCollection.cs | 3 +- .../Engine/IGameEngineErrorCollection.cs | 1 + .../Reporting/Engine/XmlParseErrorReporter.cs | 8 +- .../Json/JsonAggregatedVerificationError.cs | 6 +- .../Reporting/Json/JsonVerificationError.cs | 17 +- .../Json/JsonVerificationErrorBase.cs | 29 +- .../Reporting/Reporters/JSON/JsonReporter.cs | 12 +- .../Reporters/JSON/JsonReporterSettings.cs | 1 - .../Reporting/Reporters/ReporterSettings.cs | 1 + .../Reporting/RestoredVerifierInfo.cs | 5 + src/ModVerify/Reporting/VerificationError.cs | 19 +- .../VerifierChainEqualityComparer.cs | 1 + .../Schemas/{2.1 => 2.2}/baseline.json | 11 +- .../Verifiers/AlreadyVerifiedCache.cs | 28 - src/ModVerify/Verifiers/AudioFilesVerifier.cs | 249 -- .../Verifiers/Caching/AlreadyVerifiedCache.cs | 31 + .../Caching/IAlreadyVerifiedCache.cs | 8 + .../Verifiers/Caching/VerifiedCacheEntry.cs | 14 + .../CommandBar/CommandBarVerifier.Base.cs | 161 - .../CommandBarVerifier.Components.cs | 51 + .../CommandBar/CommandBarVerifier.Groups.cs | 56 + .../CommandBarVerifier.MegaTexture.cs | 41 + .../CommandBarVerifier.SingleComponent.cs | 74 + .../CommandBar/CommandBarVerifier.cs | 52 + .../Verifiers/Commons/Audio/AudioFileInfo.cs | 15 + .../Verifiers/Commons/Audio/AudioFileType.cs | 7 + .../Verifiers/Commons/AudioFileVerifier.cs | 141 + .../Verifiers/Commons/DuplicateVerifier.cs | 43 + .../IDuplicateVerificationContext.cs | 11 + .../MtdDuplicateVerificationContext.cs | 32 + ...edXmlObjectDuplicateVerificationContext.cs | 37 + ...odelVerifier.cs => SingleModelVerifier.cs} | 140 +- .../{TextureVeifier.cs => TextureVerifier.cs} | 29 +- .../Verifiers/DuplicateNameFinder.cs | 104 - .../{ => Engine}/GameEngineErrorCollector.cs | 4 +- .../Engine/HardcodedAssetsVerifier.cs | 36 + .../GameObjectTypeVerifier.Icons.cs | 41 + .../GameObjectTypeVerifier.Models.cs | 13 + .../GameObjectTypeVerifier.XRef.cs | 44 + .../GameObjects/GameObjectTypeVerifier.cs | 49 + src/ModVerify/Verifiers/GameVerifier.cs | 52 +- src/ModVerify/Verifiers/GameVerifierBase.cs | 39 +- .../GuiDialogs/GuiDialogsVerifier.cs | 108 +- .../Verifiers/IAlreadyVerifiedCache.cs | 11 - src/ModVerify/Verifiers/IGameVerifierInfo.cs | 6 +- .../Verifiers/NamedGameEntityVerifier.cs | 70 + .../Verifiers/ReferencedModelsVerifier.cs | 63 - .../SfxEvents/SfxEventVerifier.Samples.cs | 114 + .../SfxEvents/SfxEventVerifier.XRef.cs | 21 + .../Verifiers/SfxEvents/SfxEventVerifier.cs | 81 + .../DuplicateVerificationContextExtensions.cs | 22 + .../Utilities/GameVerifierInfoExtensions.cs | 18 + .../NameBasedEqualityComparer.cs | 2 +- src/ModVerify/Verifiers/VerifierErrorCodes.cs | 11 +- .../Audio/Sfx/SfxEvent.cs | 55 +- .../Audio/Sfx/SfxEventGameManager.cs | 53 +- .../CommandBar/CommandBarGameManager.cs | 239 +- .../CommandBarGameManager_Initialization.cs | 243 ++ .../Components/CommandBarShellComponent.cs | 26 + .../CommandBar/ICommandBarGameManager.cs | 7 +- .../SupportedCommandBarComponentData.cs | 2 + .../CommandBar/Xml/CommandBarComponentData.cs | 76 +- .../Commons/MultiNameReferenceList.cs | 57 + .../ErrorReporting/EngineAssert.cs | 4 +- .../ErrorReporting/EngineAssertKind.cs | 1 + .../GameEngineErrorReporterWrapper.cs | 36 +- .../IGameEngineErrorReporter.cs | 8 +- .../ErrorReporting/XmlError.cs | 19 - .../FocHardcodedConstants.cs | 21 - .../GameConstants/GameConstants.cs | 5 +- .../GameConstants/GameConstantsXml.cs | 7 +- .../PG.StarWarsGame.Engine/GameManagerBase.cs | 13 +- .../GameObjects/GameObject.cs | 107 +- .../GameObjects/GameObjectType.cs | 46 - ...ameObjectTypeGameManager.Initialization.cs | 166 + .../GameObjects/GameObjectTypeGameManager.cs | 94 +- .../GameObjects/IGameObjectTypeGameManager.cs | 26 +- .../GameObjects/MapEnvironmentType.cs | 33 + .../GuiDialog/ComponentTextureEntry.cs | 5 +- .../GuiDialog/ExtensionMethods.cs | 22 + .../GuiDialog/GuiDialogGameManager.cs | 44 +- .../GuiDialogGameManager_Initialization.cs | 91 +- .../GuiDialog/Xml/GuiDialogsXml.cs | 4 +- .../GuiDialog/Xml/GuiDialogsXmlTextureData.cs | 2 +- .../GuiDialog/Xml/XmlComponentTextureData.cs | 2 +- .../HardcodedEngineAssets.cs | 302 ++ .../PG.StarWarsGame.Engine/IO/IRepository.cs | 2 + .../IO/MultiPassRepository.cs | 25 +- .../IO/Repositories/GameRepository.Files.cs | 27 +- .../PG.StarWarsGame.Engine.csproj | 8 +- .../PG.StarWarsGame.Engine.csproj.DotSettings | 4 +- .../PG.StarWarsGame.Engine/PGConstants.cs | 5 +- .../PetroglyphEngineServiceContribution.cs | 2 +- .../PetroglyphStarWarsGameEngineService.cs | 1 + .../Animations/EawModelAnimationTypes.cs | 109 - .../Animations/FocModelAnimationTypes.cs | 124 - .../Animations/ModelAnimationType.cs | 170 +- .../SupportedModelAnimationTypes.cs | 365 +- .../Rendering/Font/FontManager.cs | 14 +- .../Rendering/Font/WindowsFontManager.cs | 3 +- .../Rendering/Matrix3x4.cs | 198 + .../Utilities/ExtensionMethods.cs | 15 + .../Utilities/PGMath.cs | 12 + .../Xml/EnumConversionDictionary.cs | 58 + .../Xml/IPetroglyphXmlFileParserFactory.cs | 9 +- .../Xml/NamedXmlObjectParser.cs | 74 + .../Xml/ParserNotFoundException.cs | 2 +- .../Parsers/Data/CommandBarComponentParser.cs | 365 -- .../Xml/Parsers/Data/GameObjectParser.cs | 148 - .../Xml/Parsers/Data/SfxEventParser.cs | 204 - .../File/CommandBarComponentFileParser.cs | 31 - .../Xml/Parsers/File/GameObjectFileParser.cs | 25 - .../Xml/Parsers/File/GuiDialogParser.cs | 59 - .../Xml/Parsers/File/SfxEventFileParser.cs | 32 - .../GameConstantsParser.cs | 9 +- .../Parsers/FileObjects/GuiDialogParser.cs | 186 + .../Xml/Parsers/GameObjectFileParser.cs | 46 + .../Xml/Parsers/GameObjectParsedEventArgs.cs | 17 + .../NamedObjects/CommandBarComponentParser.cs | 584 +++ .../Parsers/NamedObjects/GameObjectParser.cs | 233 + .../Parsers/NamedObjects/SfxEventParser.cs | 321 ++ .../Xml/Parsers/XmlContainerContentParser.cs | 120 - .../XmlContainerParserErrorEventArgs.cs | 25 - .../Xml/Parsers/XmlObjectParser.cs | 79 - .../PetroglyphStarWarsGameXmlParseSettings.cs | 10 + .../Xml/PetroglyphStarWarsGameXmlParser.cs | 230 + .../Xml/PetroglyphXmlParserFactory.cs | 27 - .../Xml/Tags/CommandBarComponentTags.cs | 108 - .../Xml/Tags/ComponentTextureKeyExtensions.cs | 114 - .../Xml/Tags/SfxEventXmlTags.cs | 41 - .../PG.StarWarsGame.Engine/Xml/XmlObject.cs | 12 - .../Xml/XmlObjectParser.cs | 48 + .../Xml/XmlObjectParserBase.cs | 59 + .../Xml/XmlObjectParserFactory.cs | 30 + .../Xml/XmlTagMapper.cs | 121 + .../PG.StarWarsGame.Files.ALO.csproj | 2 +- .../PG.StarWarsGame.Files.ChunkFiles.csproj | 4 +- .../Data}/NamedXmlObject.cs | 6 +- .../Data/XmlFileList.cs | 13 + .../Data/XmlFileListContainer.cs | 8 - .../Data/XmlObject.cs | 6 + .../ErrorHandling/IXmlParserErrorProvider.cs | 6 - .../ErrorHandling/IXmlParserErrorReporter.cs | 6 +- .../PrimitiveXmlErrorReporter.cs | 13 +- .../ErrorHandling/XmlError.cs | 21 + .../ErrorHandling/XmlErrorEventHandler.cs | 5 - .../ErrorHandling/XmlErrorReporter.cs | 16 +- .../ErrorHandling/XmlParseErrorEventArgs.cs | 42 - .../ErrorHandling/XmlParseErrorKind.cs | 10 +- .../PG.StarWarsGame.Files.XML.csproj | 4 +- .../Base/IPetroglyphXmlElementParser.cs | 8 - .../Parsers/Base/IPetroglyphXmlFileParser.cs | 8 - ...lParser.cs => IPetroglyphXmlParserInfo.cs} | 2 +- .../Base/PetroglyphXmlFileParserBase.cs | 50 +- .../Parsers/Base/PetroglyphXmlParserBase.cs | 68 +- .../Parsers/INamedXmlObjectParser.cs | 11 + ...erParser.cs => IXmlContainerFileParser.cs} | 5 +- .../Parsers/PetroglyphXmlElementParser.cs | 10 - .../PetroglyphXmlFileContainerParser.cs | 21 - .../Parsers/PetroglyphXmlFileParser.cs | 24 - .../CommaSeparatedStringKeyValueListParser.cs | 38 +- .../Primitives/PetroglyphNumberParser.cs | 88 + .../PetroglyphPrimitiveXmlParser.cs | 24 +- .../Primitives/PetroglyphXmlBooleanParser.cs | 11 +- .../Primitives/PetroglyphXmlByteParser.cs | 28 +- .../PetroglyphXmlBytePercentParser.cs | 45 + .../Primitives/PetroglyphXmlFloatParser.cs | 44 +- .../Primitives/PetroglyphXmlIntegerParser.cs | 46 +- .../PetroglyphXmlLooseStringListParser.cs | 12 +- .../PetroglyphXmlMax100ByteParser.cs | 62 - .../PetroglyphXmlRgbaColorParser.cs | 3 +- .../Primitives/PetroglyphXmlSByteParser.cs | 41 + .../Primitives/PetroglyphXmlStringParser.cs | 13 +- .../PetroglyphXmlUnsignedIntegerParser.cs | 24 +- .../Primitives/PetroglyphXmlVector2FParser.cs | 11 +- .../Parsers/XmlContainerFileParser.cs | 33 + .../Parsers/XmlFileListParser.cs | 40 +- .../Parsers/XmlFileParser.cs | 19 + .../Utilities/PGMath.cs | 60 +- .../XElementExtensions.cs | 15 + .../XmlFileConstants.cs | 10 + .../ModVerify.CliApp.Test.csproj | 14 +- 198 files changed, 7805 insertions(+), 5681 deletions(-) rename src/ModVerify/Resources/Schemas/{2.1 => 2.2}/baseline.json (92%) delete mode 100644 src/ModVerify/Verifiers/AlreadyVerifiedCache.cs delete mode 100644 src/ModVerify/Verifiers/AudioFilesVerifier.cs create mode 100644 src/ModVerify/Verifiers/Caching/AlreadyVerifiedCache.cs create mode 100644 src/ModVerify/Verifiers/Caching/IAlreadyVerifiedCache.cs create mode 100644 src/ModVerify/Verifiers/Caching/VerifiedCacheEntry.cs delete mode 100644 src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.Base.cs create mode 100644 src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.Components.cs create mode 100644 src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.Groups.cs create mode 100644 src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.MegaTexture.cs create mode 100644 src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.SingleComponent.cs create mode 100644 src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.cs create mode 100644 src/ModVerify/Verifiers/Commons/Audio/AudioFileInfo.cs create mode 100644 src/ModVerify/Verifiers/Commons/Audio/AudioFileType.cs create mode 100644 src/ModVerify/Verifiers/Commons/AudioFileVerifier.cs create mode 100644 src/ModVerify/Verifiers/Commons/DuplicateVerifier.cs create mode 100644 src/ModVerify/Verifiers/Commons/Duplicates/IDuplicateVerificationContext.cs create mode 100644 src/ModVerify/Verifiers/Commons/Duplicates/MtdDuplicateVerificationContext.cs create mode 100644 src/ModVerify/Verifiers/Commons/Duplicates/NamedXmlObjectDuplicateVerificationContext.cs rename src/ModVerify/Verifiers/Commons/{ModelVerifier.cs => SingleModelVerifier.cs} (80%) rename src/ModVerify/Verifiers/Commons/{TextureVeifier.cs => TextureVerifier.cs} (68%) delete mode 100644 src/ModVerify/Verifiers/DuplicateNameFinder.cs rename src/ModVerify/Verifiers/{ => Engine}/GameEngineErrorCollector.cs (91%) create mode 100644 src/ModVerify/Verifiers/Engine/HardcodedAssetsVerifier.cs create mode 100644 src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.Icons.cs create mode 100644 src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.Models.cs create mode 100644 src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.XRef.cs create mode 100644 src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.cs delete mode 100644 src/ModVerify/Verifiers/IAlreadyVerifiedCache.cs create mode 100644 src/ModVerify/Verifiers/NamedGameEntityVerifier.cs delete mode 100644 src/ModVerify/Verifiers/ReferencedModelsVerifier.cs create mode 100644 src/ModVerify/Verifiers/SfxEvents/SfxEventVerifier.Samples.cs create mode 100644 src/ModVerify/Verifiers/SfxEvents/SfxEventVerifier.XRef.cs create mode 100644 src/ModVerify/Verifiers/SfxEvents/SfxEventVerifier.cs create mode 100644 src/ModVerify/Verifiers/Utilities/DuplicateVerificationContextExtensions.cs create mode 100644 src/ModVerify/Verifiers/Utilities/GameVerifierInfoExtensions.cs rename src/ModVerify/Verifiers/{ => Utilities}/NameBasedEqualityComparer.cs (94%) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager_Initialization.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Commons/MultiNameReferenceList.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/XmlError.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/FocHardcodedConstants.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectType.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectTypeGameManager.Initialization.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/MapEnvironmentType.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/ExtensionMethods.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/HardcodedEngineAssets.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/EawModelAnimationTypes.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/FocModelAnimationTypes.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Matrix3x4.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/ExtensionMethods.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/PGMath.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/EnumConversionDictionary.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/NamedXmlObjectParser.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/CommandBarComponentParser.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/CommandBarComponentFileParser.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileParser.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GuiDialogParser.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs rename src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/{Data => FileObjects}/GameConstantsParser.cs (50%) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/FileObjects/GuiDialogParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/GameObjectFileParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/GameObjectParsedEventArgs.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/NamedObjects/CommandBarComponentParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/NamedObjects/GameObjectParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/NamedObjects/SfxEventParser.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerParserErrorEventArgs.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphStarWarsGameXmlParseSettings.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphStarWarsGameXmlParser.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/CommandBarComponentTags.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/ComponentTextureKeyExtensions.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/SfxEventXmlTags.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObject.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObjectParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObjectParserBase.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObjectParserFactory.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlTagMapper.cs rename src/PetroglyphTools/{PG.StarWarsGame.Engine/Xml => PG.StarWarsGame.Files.XML/Data}/NamedXmlObject.cs (61%) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/XmlFileList.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/XmlFileListContainer.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/XmlObject.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IXmlParserErrorProvider.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlError.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlErrorEventHandler.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlParseErrorEventArgs.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlElementParser.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlFileParser.cs rename src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/{IPetroglyphXmlParser.cs => IPetroglyphXmlParserInfo.cs} (64%) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/INamedXmlObjectParser.cs rename src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/{Base/IPetroglyphXmlFileContainerParser.cs => IXmlContainerFileParser.cs} (55%) delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlElementParser.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileContainerParser.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphNumberParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlBytePercentParser.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlMax100ByteParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlSByteParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/XmlContainerFileParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/XmlFileParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/XElementExtensions.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/XmlFileConstants.cs diff --git a/Directory.Build.props b/Directory.Build.props index 723456e..3e2f119 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -33,7 +33,7 @@ - + all 3.9.50 diff --git a/modules/ModdingToolBase b/modules/ModdingToolBase index e12f6ce..da072f4 160000 --- a/modules/ModdingToolBase +++ b/modules/ModdingToolBase @@ -1 +1 @@ -Subproject commit e12f6ceedb83fe9e3372dd89c68d508f8479cf92 +Subproject commit da072f43e6b85aab35b43d11f6b36eab61bdcfa6 diff --git a/src/ModVerify.CliApp/App/VerifyAction.cs b/src/ModVerify.CliApp/App/VerifyAction.cs index f30305c..e17d7bc 100644 --- a/src/ModVerify.CliApp/App/VerifyAction.cs +++ b/src/ModVerify.CliApp/App/VerifyAction.cs @@ -73,6 +73,7 @@ private IReadOnlyCollection CreateReporters() reporters.Add(IVerificationReporter.CreateConsole(new ConsoleReporterSettings { + Verbose = Settings.ReportSettings.Verbose, MinimumReportSeverity = Settings.VerifierServiceSettings.FailFastSettings.IsFailFast ? VerificationSeverity.Information : VerificationSeverity.Error @@ -83,11 +84,13 @@ private IReadOnlyCollection CreateReporters() { OutputDirectory = outputDirectory, MinimumReportSeverity = Settings.ReportSettings.MinimumReportSeverity, - AggregateResults = true + AggregateResults = true, + Verbose = Settings.ReportSettings.Verbose }, ServiceProvider)); reporters.Add(IVerificationReporter.CreateText(new TextFileReporterSettings { + Verbose = Settings.ReportSettings.Verbose, OutputDirectory = outputDirectory!, MinimumReportSeverity = Settings.ReportSettings.MinimumReportSeverity }, ServiceProvider)); diff --git a/src/ModVerify.CliApp/ModVerify.CliApp.csproj b/src/ModVerify.CliApp/ModVerify.CliApp.csproj index 7dcf361..f7b7ff8 100644 --- a/src/ModVerify.CliApp/ModVerify.CliApp.csproj +++ b/src/ModVerify.CliApp/ModVerify.CliApp.csproj @@ -30,18 +30,18 @@ - - - - + + + + - + - - - - - + + + + + @@ -62,7 +62,7 @@ - + compile runtime; build; native; contentfiles; analyzers; buildtransitive @@ -70,7 +70,7 @@ compile runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/ModVerify.CliApp/Program.cs b/src/ModVerify.CliApp/Program.cs index 68056a4..e5316f4 100644 --- a/src/ModVerify.CliApp/Program.cs +++ b/src/ModVerify.CliApp/Program.cs @@ -15,7 +15,6 @@ using Microsoft.Extensions.Logging; using PG.Commons; using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Engine.Xml.Parsers; using PG.StarWarsGame.Files.ALO; using PG.StarWarsGame.Files.MEG; using PG.StarWarsGame.Files.MTD; @@ -35,6 +34,7 @@ using System.Runtime.InteropServices; using System.Threading.Tasks; using AET.ModVerify.App.Reporting; +using PG.StarWarsGame.Engine.Xml; using Testably.Abstractions; using ILogger = Serilog.ILogger; @@ -51,8 +51,8 @@ private static Task Main(string[] 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 EngineParserNamespace = typeof(PetroglyphStarWarsGameXmlParser).Namespace!; + private static readonly string ParserNamespace = typeof(XmlFileParser<>).Namespace!; private static readonly string ModVerifyRootNameSpace = typeof(Program).Namespace!; private static readonly CompiledExpression PrintToConsoleExpression = SerilogExpression.Compile($"EventId.Id = {ModVerifyConstants.ConsoleEventIdValue}"); diff --git a/src/ModVerify.CliApp/Properties/launchSettings.json b/src/ModVerify.CliApp/Properties/launchSettings.json index 299ce46..46fa0fc 100644 --- a/src/ModVerify.CliApp/Properties/launchSettings.json +++ b/src/ModVerify.CliApp/Properties/launchSettings.json @@ -2,15 +2,15 @@ "profiles": { "Verify": { "commandName": "Project", - "commandLineArgs": "" + "commandLineArgs": "verify --offline" }, "Verify (Interactive)": { "commandName": "Project", - "commandLineArgs": "verify -o verifyResults --offline --minFailSeverity Information" + "commandLineArgs": "verify -o verifyResults --offline --minFailSeverity Information --searchBaseline" }, "Verify (Automatic Target Selection)": { "commandName": "Project", - "commandLineArgs": "verify -o verifyResults --path \"C:\\Program Files (x86)\\Steam\\steamapps\\common\\Star Wars Empire at War\\corruption\"" + "commandLineArgs": "verify --offline -o verifyResults --path \"C:/Program Files (x86)/Steam/steamapps/common/Star Wars Empire at War/corruption/Mods/Test\"" }, "Create Baseline Interactive": { "commandName": "Project", diff --git a/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json b/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json index ce70f8a..39b65f9 100644 --- a/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json +++ b/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json @@ -1,5 +1,5 @@ { - "version": "2.1", + "version": "2.2", "target": { "name": "Forces of Corruption (SteamGold)", "engine": "Foc", @@ -10,3038 +10,2864 @@ "errors": [ { "id": "XML04", - "verifiers": [ - "XMLError" - ], - "message": "Expected double but got value \u002737\u0060\u0027. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #11571\u0027", "severity": "Warning", + "asset": "Size", + "message": "Expected double but got value \u002737\u0060\u0027. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #11571\u0027", "context": [ - "DATA\\XML\\COMMANDBARCOMPONENTS.XML", + "Parser: PG.StarWarsGame.Files.XML.Parsers.PetroglyphXmlFloatParser", + "File: 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", + "asset": "Probability", + "message": "Expected integer but got \u002780, 20\u0027. File=\u0027DATA\\XML\\SFXEVENTSWEAPONS.XML #90\u0027", "context": [ - "DATA\\XML\\SFXEVENTSWEAPONS.XML", + "Parser: PG.StarWarsGame.Files.XML.Parsers.PetroglyphXmlIntegerParser", + "File: DATA\\XML\\SFXEVENTSWEAPONS.XML", "Probability", "parentName=\u0027Unit_TIE_Fighter_Fire\u0027" - ], - "asset": "Probability" + ] }, { - "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027CIN_Reb_CelebHall.alo\u0027", - "severity": "Error", - "context": [], - "asset": "CIN_Reb_CelebHall.alo" + "id": "XML08", + "severity": "Information", + "asset": "DATA\\XML\\UNITS_HERO_REBEL_ROGUE_SQUADRON.XML", + "message": "XML header is not the first entry of the XML file. File=\u0027DATA\\XML\\UNITS_HERO_REBEL_ROGUE_SQUADRON.XML #0\u0027", + "context": [ + "Parser: PG.StarWarsGame.Engine.Xml.Parsers.GameObjectFileParser", + "File: DATA\\XML\\UNITS_HERO_REBEL_ROGUE_SQUADRON.XML" + ] }, { - "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", + "id": "XML10", + "severity": "Information", + "asset": "Disabled_Darken", + "message": "The node \u0027Disabled_Darken\u0027 is not supported. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #8608\u0027", "context": [ - "DATA\\ART\\MODELS\\UV_ECLIPSE_UC_DC.ALO" - ], - "asset": "p_ssd_debris" + "Parser: PG.StarWarsGame.Engine.Xml.Parsers.CommandBarComponentParser", + "File: DATA\\XML\\COMMANDBARCOMPONENTS.XML", + "Disabled_Darken", + "parentName=\u0027b_fast_forward_t\u0027" + ] }, { - "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", + "id": "XML10", + "severity": "Information", + "asset": "Mega_Texture_Name", + "message": "The node \u0027Mega_Texture_Name\u0027 is not supported. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #8\u0027", "context": [ - "W_SITH_LEFTHALL.ALO" - ], - "asset": "Cin_Reb_CelebHall_Wall.tga" + "Parser: PG.StarWarsGame.Engine.Xml.Parsers.CommandBarComponentParser", + "File: DATA\\XML\\COMMANDBARCOMPONENTS.XML", + "Mega_Texture_Name", + "parentName=\u0027i_main_commandbar\u0027" + ] }, { - "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": "XML08", + "severity": "Information", + "asset": "DATA\\XML\\UNITS_SPACE_UNDERWORLD_INTERCEPTOR4.XML", + "message": "XML header is not the first entry of the XML file. File=\u0027DATA\\XML\\UNITS_SPACE_UNDERWORLD_INTERCEPTOR4.XML #0\u0027", + "context": [ + "Parser: PG.StarWarsGame.Engine.Xml.Parsers.GameObjectFileParser", + "File: DATA\\XML\\UNITS_SPACE_UNDERWORLD_INTERCEPTOR4.XML" + ] }, { - "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": "XML08", + "severity": "Information", + "asset": "DATA\\XML\\UNITS_SPACE_EMPIRE_TIE_DEFENDER.XML", + "message": "XML header is not the first entry of the XML file. File=\u0027DATA\\XML\\UNITS_SPACE_EMPIRE_TIE_DEFENDER.XML #0\u0027", + "context": [ + "Parser: PG.StarWarsGame.Engine.Xml.Parsers.GameObjectFileParser", + "File: DATA\\XML\\UNITS_SPACE_EMPIRE_TIE_DEFENDER.XML" + ] }, { - "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": "XML10", + "severity": "Information", + "asset": "Disabled_Darken", + "message": "The node \u0027Disabled_Darken\u0027 is not supported. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #8569\u0027", + "context": [ + "Parser: PG.StarWarsGame.Engine.Xml.Parsers.CommandBarComponentParser", + "File: DATA\\XML\\COMMANDBARCOMPONENTS.XML", + "Disabled_Darken", + "parentName=\u0027b_fast_forward\u0027" + ] }, { - "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", + "id": "XML08", + "severity": "Information", + "asset": "DATA\\XML\\GROUNDSTRUCTURES_UNDERWORLD.XML", + "message": "XML header is not the first entry of the XML file. File=\u0027DATA\\XML\\GROUNDSTRUCTURES_UNDERWORLD.XML #0\u0027", "context": [ - "W_GRENADE.ALO" - ], - "asset": "w_grenade.tga" + "Parser: PG.StarWarsGame.Engine.Xml.Parsers.GameObjectFileParser", + "File: DATA\\XML\\GROUNDSTRUCTURES_UNDERWORLD.XML" + ] }, { - "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", + "id": "XML08", + "severity": "Information", + "asset": "DATA\\XML\\UNITS_LAND_REBEL_GALLOFREE_HTT.XML", + "message": "XML header is not the first entry of the XML file. File=\u0027DATA\\XML\\UNITS_LAND_REBEL_GALLOFREE_HTT.XML #0\u0027", "context": [ - "DATA\\ART\\MODELS\\UB_03_STATION_D.ALO" - ], - "asset": "p_uwstation_death" + "Parser: PG.StarWarsGame.Engine.Xml.Parsers.GameObjectFileParser", + "File: DATA\\XML\\UNITS_LAND_REBEL_GALLOFREE_HTT.XML" + ] }, { - "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": "XML10", + "severity": "Information", + "asset": "Disabled_Darken", + "message": "The node \u0027Disabled_Darken\u0027 is not supported. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #8589\u0027", + "context": [ + "Parser: PG.StarWarsGame.Engine.Xml.Parsers.CommandBarComponentParser", + "File: DATA\\XML\\COMMANDBARCOMPONENTS.XML", + "Disabled_Darken", + "parentName=\u0027b_play_pause_t\u0027" + ] }, { - "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": "XML10", + "severity": "Information", + "asset": "Disabled_Darken", + "message": "The node \u0027Disabled_Darken\u0027 is not supported. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #8550\u0027", + "context": [ + "Parser: PG.StarWarsGame.Engine.Xml.Parsers.CommandBarComponentParser", + "File: DATA\\XML\\COMMANDBARCOMPONENTS.XML", + "Disabled_Darken", + "parentName=\u0027b_play_pause\u0027" + ] }, { - "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": "XML08", + "severity": "Information", + "asset": "DATA\\XML\\SPACEPROPS_UNDERWORLD.XML", + "message": "XML header is not the first entry of the XML file. File=\u0027DATA\\XML\\SPACEPROPS_UNDERWORLD.XML #0\u0027", + "context": [ + "Parser: PG.StarWarsGame.Engine.Xml.Parsers.GameObjectFileParser", + "File: DATA\\XML\\SPACEPROPS_UNDERWORLD.XML" + ] }, { "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", + "asset": "U000_LEI0210_ENG.WAV", + "message": "Audio file \u0027U000_LEI0210_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\NB_PRISON.ALO" - ], - "asset": "p_smoke_small_thin2" + "Unit_Group_Move_Leia" + ] }, { "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", + "asset": "U000_LEI0207_ENG.WAV", + "message": "Audio file \u0027U000_LEI0207_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\UB_01_STATION_D.ALO" - ], - "asset": "p_uwstation_death" + "Unit_Group_Move_Leia" + ] }, { "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" + "asset": "U000_LEI0205_ENG.WAV", + "message": "Audio file \u0027U000_LEI0205_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Move_Leia" + ] }, { "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", + "asset": "U000_LEI0207_ENG.WAV", + "message": "Audio file \u0027U000_LEI0207_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\W_STARS_MEDIUM.ALO" - ], - "asset": "Lensflare0" + "Unit_Move_Leia" + ] }, { "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" + "asset": "U000_DEF3006_ENG.WAV", + "message": "Audio file \u0027U000_DEF3006_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Corrupt_Sabateur" + ] }, { "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" + "asset": "U000_ARC3104_ENG.WAV", + "message": "Audio file \u0027U000_ARC3104_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Produce_Troops_Arc_Hammer" + ] }, { "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", + "asset": "U000_LEI0315_ENG.WAV", + "message": "Audio file \u0027U000_LEI0315_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\NB_PRISON.ALO" - ], - "asset": "p_smoke_small_thin4" + "Unit_Attack_Leia" + ] }, { "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", + "asset": "U000_LEI0201_ENG.WAV", + "message": "Audio file \u0027U000_LEI0201_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\W_STARS_CINE.ALO" - ], - "asset": "Lensflare0" + "Unit_Move_Leia" + ] }, { "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", + "asset": "U000_LEI0211_ENG.WAV", + "message": "Audio file \u0027U000_LEI0211_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\UV_SKIPRAY.ALO" - ], - "asset": "Default.fx" + "Unit_Group_Move_Leia" + ] }, { "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", + "asset": "U000_LEI0303_ENG.WAV", + "message": "Audio file \u0027U000_LEI0303_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\UI_IG88.ALO" - ], - "asset": "p_desert_ground_dust" + "Unit_Attack_Leia" + ] }, { "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" + "asset": "U000_LEI0111_ENG.WAV", + "message": "Audio file \u0027U000_LEI0111_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Select_Leia" + ] }, { "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" + "asset": "AMB_URB_CLEAR_LOOP_1.WAV", + "message": "Audio file \u0027AMB_URB_CLEAR_LOOP_1.WAV\u0027 could not be found.", + "context": [ + "Weather_Ambient_Clear_Urban_Loop" + ] }, { "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", + "asset": "FS_BEETLE_3.WAV", + "message": "Audio file \u0027FS_BEETLE_3.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\UB_04_STATION_D.ALO" - ], - "asset": "p_uwstation_death" + "SFX_Anim_Beetle_Footsteps" + ] }, { "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" + "asset": "U000_LEI0205_ENG.WAV", + "message": "Audio file \u0027U000_LEI0205_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Group_Move_Leia" + ] }, { "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", + "asset": "U000_LEI0215_ENG.WAV", + "message": "Audio file \u0027U000_LEI0215_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\UB_02_STATION_D.ALO" - ], - "asset": "p_uwstation_death" + "Unit_Move_Leia" + ] }, { "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" + "asset": "U000_LEI0504_ENG.WAV", + "message": "Audio file \u0027U000_LEI0504_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Remove_Corruption_Leia" + ] }, { "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", + "asset": "U000_LEI0104_ENG.WAV", + "message": "Audio file \u0027U000_LEI0104_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\NB_MONCAL_BUILDING.ALO" - ], - "asset": "p_smoke_small_thin2" + "Unit_Select_Leia" + ] }, { "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", + "asset": "U000_LEI0303_ENG.WAV", + "message": "Audio file \u0027U000_LEI0303_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\RB_HEAVYVEHICLEFACTORY.ALO" - ], - "asset": "p_steam_small" + "Unit_Group_Attack_Leia" + ] }, { "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" + "asset": "U000_LEI0202_ENG.WAV", + "message": "Audio file \u0027U000_LEI0202_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Fleet_Move_Leia" + ] }, { "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" + "asset": "U000_LEI0212_ENG.WAV", + "message": "Audio file \u0027U000_LEI0212_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Move_Leia" + ] }, { "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" + "asset": "U000_LEI0105_ENG.WAV", + "message": "Audio file \u0027U000_LEI0105_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Select_Leia" + ] }, { "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" + "asset": "U000_LEI0311_ENG.WAV", + "message": "Audio file \u0027U000_LEI0311_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Attack_Leia" + ] }, { "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" + "asset": "U000_LEI0110_ENG.WAV", + "message": "Audio file \u0027U000_LEI0110_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Select_Leia" + ] }, { "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", + "asset": "U000_LEI0313_ENG.WAV", + "message": "Audio file \u0027U000_LEI0313_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\UI_SABOTEUR.ALO" - ], - "asset": "p_desert_ground_dust" + "Unit_Group_Attack_Leia" + ] }, { "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", + "asset": "U000_LEI0103_ENG.WAV", + "message": "Audio file \u0027U000_LEI0103_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\W_STARS_CINE_LUA.ALO" - ], - "asset": "Lensflare0" + "Unit_Select_Leia" + ] }, { "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" + "asset": "U000_LEI0308_ENG.WAV", + "message": "Audio file \u0027U000_LEI0308_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Group_Attack_Leia" + ] }, { "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", + "asset": "U000_TMC0212_ENG.WAV", + "message": "Audio file \u0027U000_TMC0212_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\EV_TIE_LANCET.ALO" - ], - "asset": "Default.fx" + "Unit_Assist_Move_Tie_Mauler" + ] }, { "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" + "asset": "U000_LEI0304_ENG.WAV", + "message": "Audio file \u0027U000_LEI0304_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Attack_Leia" + ] }, { "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" + "asset": "U000_LEI0101_ENG.WAV", + "message": "Audio file \u0027U000_LEI0101_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Select_Leia" + ] }, { "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", + "asset": "U000_LEI0211_ENG.WAV", + "message": "Audio file \u0027U000_LEI0211_ENG.WAV\u0027 could not be found.", "context": [ - "ALTTEST.ALO" - ], - "asset": "Cin_DeathStar.tga" + "Unit_Move_Leia" + ] }, { "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", + "asset": "U000_LEI0305_ENG.WAV", + "message": "Audio file \u0027U000_LEI0305_ENG.WAV\u0027 could not be found.", "context": [ - "UV_MDU_CAGE.ALO" - ], - "asset": "UB_girder_B.tga" + "Unit_Group_Attack_Leia" + ] }, { "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", + "asset": "U000_LEI0208_ENG.WAV", + "message": "Audio file \u0027U000_LEI0208_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\RV_MPTL-2A.ALO" - ], - "asset": "P_mptl-2a_Die" + "Unit_Fleet_Move_Leia" + ] }, { "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", + "asset": "U000_LEI0311_ENG.WAV", + "message": "Audio file \u0027U000_LEI0311_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\EV_MDU_SENSORNODE.ALO" - ], - "asset": "Default.fx" + "Unit_Group_Attack_Leia" + ] }, { "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" + "asset": "U000_LEI0314_ENG.WAV", + "message": "Audio file \u0027U000_LEI0314_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Attack_Leia" + ] }, { "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", + "asset": "U000_LEI0108_ENG.WAV", + "message": "Audio file \u0027U000_LEI0108_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\EV_ARCHAMMER.ALO" - ], - "asset": "p_hp_archammer-damage" + "Unit_Select_Leia" + ] }, { "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", + "asset": "U000_LEI0213_ENG.WAV", + "message": "Audio file \u0027U000_LEI0213_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\UB_05_STATION_D.ALO" - ], - "asset": "p_uwstation_death" + "Unit_Group_Move_Leia" + ] }, { "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", + "asset": "U000_LEI0202_ENG.WAV", + "message": "Audio file \u0027U000_LEI0202_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\NB_NOGHRI_HUT.ALO" - ], - "asset": "p_explosion_smoke_small_thin5" + "Unit_Move_Leia" + ] }, { "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" + "asset": "U000_LEI0113_ENG.WAV", + "message": "Audio file \u0027U000_LEI0113_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Select_Leia" + ] }, { "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", + "asset": "AMB_DES_CLEAR_LOOP_1.WAV", + "message": "Audio file \u0027AMB_DES_CLEAR_LOOP_1.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\W_STARS_HIGH.ALO" - ], - "asset": "Lensflare0" + "Weather_Ambient_Clear_Sandstorm_Loop" + ] }, { "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" + "asset": "U000_LEI0107_ENG.WAV", + "message": "Audio file \u0027U000_LEI0107_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Select_Leia" + ] }, { "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", + "asset": "U000_LEI0305_ENG.WAV", + "message": "Audio file \u0027U000_LEI0305_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\UV_ECLIPSE.ALO" - ], - "asset": "lookat" + "Unit_Attack_Leia" + ] }, { "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" + "asset": "U000_LEI0209_ENG.WAV", + "message": "Audio file \u0027U000_LEI0209_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Group_Move_Leia" + ] }, { "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" + "asset": "U000_LEI0308_ENG.WAV", + "message": "Audio file \u0027U000_LEI0308_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Attack_Leia" + ] }, { "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", + "asset": "U000_LEI0207_ENG.WAV", + "message": "Audio file \u0027U000_LEI0207_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\NB_VCH.ALO" - ], - "asset": "P_heat_small01" + "Unit_Fleet_Move_Leia" + ] }, { "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" + "asset": "U000_MAL0503_ENG.WAV", + "message": "Audio file \u0027U000_MAL0503_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Assist_Move_Missile_Launcher" + ] }, { "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" + "asset": "U000_LEI0102_ENG.WAV", + "message": "Audio file \u0027U000_LEI0102_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Select_Leia" + ] }, { "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", + "asset": "U000_LEI0306_ENG.WAV", + "message": "Audio file \u0027U000_LEI0306_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\UI_EWOK_HANDLER.ALO" - ], - "asset": "p_ewok_drag_dirt" + "Unit_Group_Attack_Leia" + ] }, { "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" + "asset": "FS_BEETLE_2.WAV", + "message": "Audio file \u0027FS_BEETLE_2.WAV\u0027 could not be found.", + "context": [ + "SFX_Anim_Beetle_Footsteps" + ] }, { "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" + "asset": "U000_LEI0312_ENG.WAV", + "message": "Audio file \u0027U000_LEI0312_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Group_Attack_Leia" + ] }, { "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" + "asset": "U000_LEI0402_ENG.WAV", + "message": "Audio file \u0027U000_LEI0402_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Guard_Leia" + ] }, { "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" + "asset": "U000_LEI0309_ENG.WAV", + "message": "Audio file \u0027U000_LEI0309_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Group_Attack_Leia" + ] }, { "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", + "asset": "U000_LEI0501_ENG.WAV", + "message": "Audio file \u0027U000_LEI0501_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\RB_HYPERVELOCITYGUN.ALO" - ], - "asset": "p_smoke_small_thin2" + "Unit_Remove_Corruption_Leia" + ] }, { "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", + "asset": "U000_LEI0201_ENG.WAV", + "message": "Audio file \u0027U000_LEI0201_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\UV_ECLIPSE_UC.ALO" - ], - "asset": "lookat" + "Unit_Group_Move_Leia" + ] }, { "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" + "asset": "U000_MCF1601_ENG.WAV", + "message": "Audio file \u0027U000_MCF1601_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_StarDest_MC30_Frigate" + ] }, { "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" + "asset": "TESTUNITMOVE_ENG.WAV", + "message": "Audio file \u0027TESTUNITMOVE_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Move_Gneneric_Test" + ] }, { "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", + "asset": "U000_LEI0114_ENG.WAV", + "message": "Audio file \u0027U000_LEI0114_ENG.WAV\u0027 could not be found.", "context": [ - "W_SITH_LEFTHALL.ALO" - ], - "asset": "Cin_Reb_CelebHall_Wall_B.tga" + "Unit_Select_Leia" + ] }, { "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", + "asset": "U000_ARC3105_ENG.WAV", + "message": "Audio file \u0027U000_ARC3105_ENG.WAV\u0027 could not be found.", "context": [ - "UV_MDU_CAGE.ALO" - ], - "asset": "NB_YsalamiriTree_B.tga" - }, - { - "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027Cin_Coruscant.alo\u0027", - "severity": "Error", - "context": [], - "asset": "Cin_Coruscant.alo" + "Unit_Complete_Troops_Arc_Hammer" + ] }, { "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", + "asset": "EGL_STAR_VIPER_SPINNING_1.WAV", + "message": "Audio file \u0027EGL_STAR_VIPER_SPINNING_1.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\NB_PRISON.ALO" - ], - "asset": "p_prison_light" + "Unit_Star_Viper_Spinning_By" + ] }, { "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", + "asset": "U000_LEI0401_ENG.WAV", + "message": "Audio file \u0027U000_LEI0401_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\NB_SCH.ALO" - ], - "asset": "p_cold_tiny01" + "Unit_Guard_Leia" + ] }, { "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" + "asset": "U000_LEI0213_ENG.WAV", + "message": "Audio file \u0027U000_LEI0213_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Move_Leia" + ] }, { "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", + "asset": "U000_LEI0212_ENG.WAV", + "message": "Audio file \u0027U000_LEI0212_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\UV_CRUSADERCLASSCORVETTE.ALO" - ], - "asset": "Default.fx" + "Unit_Group_Move_Leia" + ] }, { "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" + "asset": "U000_LEI0115_ENG.WAV", + "message": "Audio file \u0027U000_LEI0115_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Select_Leia" + ] }, { "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", + "asset": "U000_LEI0306_ENG.WAV", + "message": "Audio file \u0027U000_LEI0306_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\RI_KYLEKATARN.ALO" - ], - "asset": "p_desert_ground_dust" + "Unit_Attack_Leia" + ] }, { "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" + "asset": "U000_LEI0601_ENG.WAV", + "message": "Audio file \u0027U000_LEI0601_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Increase_Production_Leia" + ] }, { "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", + "asset": "U000_ARC3106_ENG.WAV", + "message": "Audio file \u0027U000_ARC3106_ENG.WAV\u0027 could not be found.", "context": [ - "P_DIRT_EMITTER_TEST1.ALO" - ], - "asset": "p_particle_master" + "Unit_Complete_Troops_Arc_Hammer" + ] }, { "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" + "asset": "U000_LEI0602_ENG.WAV", + "message": "Audio file \u0027U000_LEI0602_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Increase_Production_Leia" + ] }, { "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" + "asset": "U000_DEF3106_ENG.WAV", + "message": "Audio file \u0027U000_DEF3106_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Weaken_Sabateur" + ] }, { "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" + "asset": "U000_LEI0403_ENG.WAV", + "message": "Audio file \u0027U000_LEI0403_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Guard_Leia" + ] }, { "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" + "asset": "U000_LEI0314_ENG.WAV", + "message": "Audio file \u0027U000_LEI0314_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Group_Attack_Leia" + ] }, { "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" + "asset": "U000_LEI0203_ENG.WAV", + "message": "Audio file \u0027U000_LEI0203_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Group_Move_Leia" + ] }, { "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" + "asset": "U000_LEI0205_ENG.WAV", + "message": "Audio file \u0027U000_LEI0205_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Fleet_Move_Leia" + ] }, { "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", + "asset": "U000_TMC0212_ENG.WAV", + "message": "Audio file \u0027U000_TMC0212_ENG.WAV\u0027 could not be found.", "context": [ - "EV_TIE_PHANTOM.ALO" - ], - "asset": "W_TE_Rock_f_02_b.tga" + "Unit_Move_Tie_Mauler" + ] }, { "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", + "asset": "U000_LEI0203_ENG.WAV", + "message": "Audio file \u0027U000_LEI0203_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\RV_BWING.ALO" - ], - "asset": "pe_bwing_yellow" + "Unit_Move_Leia" + ] }, { "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" + "asset": "U000_LEI0307_ENG.WAV", + "message": "Audio file \u0027U000_LEI0307_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Attack_Leia" + ] }, { "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", + "asset": "U000_LEI0203_ENG.WAV", + "message": "Audio file \u0027U000_LEI0203_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\EB_COMMANDCENTER.ALO" - ], - "asset": "p_explosion_small_delay00" + "Unit_Fleet_Move_Leia" + ] }, { "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" + "asset": "U000_LEI0201_ENG.WAV", + "message": "Audio file \u0027U000_LEI0201_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Fleet_Move_Leia" + ] }, { "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" + "asset": "FS_BEETLE_1.WAV", + "message": "Audio file \u0027FS_BEETLE_1.WAV\u0027 could not be found.", + "context": [ + "SFX_Anim_Beetle_Footsteps" + ] }, { "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", + "asset": "U000_LEI0204_ENG.WAV", + "message": "Audio file \u0027U000_LEI0204_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\W_STARS_LOW.ALO" - ], - "asset": "Lensflare0" + "Unit_Move_Leia" + ] }, { "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", + "asset": "U000_LEI0206_ENG.WAV", + "message": "Audio file \u0027U000_LEI0206_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\EI_MARAJADE.ALO" - ], - "asset": "p_desert_ground_dust" + "Unit_Move_Leia" + ] }, { "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", + "asset": "U000_LEI0603_ENG.WAV", + "message": "Audio file \u0027U000_LEI0603_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\W_THERMAL_DETONATOR_EMPIRE.ALO" - ], - "asset": "p_bomb_spin" + "Unit_Increase_Production_Leia" + ] }, { "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" + "asset": "U000_LEI0313_ENG.WAV", + "message": "Audio file \u0027U000_LEI0313_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Attack_Leia" + ] }, { "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" + "asset": "U000_LEI0301_ENG.WAV", + "message": "Audio file \u0027U000_LEI0301_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0213_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0312_ENG.WAV", + "message": "Audio file \u0027U000_LEI0312_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0213_ENG.WAV" + "Unit_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0113_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0404_ENG.WAV", + "message": "Audio file \u0027U000_LEI0404_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0113_ENG.WAV" + "Unit_Guard_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0603_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0604_ENG.WAV", + "message": "Audio file \u0027U000_LEI0604_ENG.WAV\u0027 could not be found.", "context": [ "Unit_Increase_Production_Leia" - ], - "asset": "U000_LEI0603_ENG.WAV" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0309_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0210_ENG.WAV", + "message": "Audio file \u0027U000_LEI0210_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0309_ENG.WAV" + "Unit_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0212_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0307_ENG.WAV", + "message": "Audio file \u0027U000_LEI0307_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0212_ENG.WAV" + "Unit_Group_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_MAL0503_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "C000_DST0102_ENG.WAV", + "message": "Audio file \u0027C000_DST0102_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Assist_Move_Missile_Launcher" - ], - "asset": "U000_MAL0503_ENG.WAV" + "EHD_Death_Star_Activate" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_MCF1601_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0215_ENG.WAV", + "message": "Audio file \u0027U000_LEI0215_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_StarDest_MC30_Frigate" - ], - "asset": "U000_MCF1601_ENG.WAV" + "Unit_Group_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0111_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0206_ENG.WAV", + "message": "Audio file \u0027U000_LEI0206_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0111_ENG.WAV" + "Unit_Group_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_ARC3106_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0202_ENG.WAV", + "message": "Audio file \u0027U000_LEI0202_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Complete_Troops_Arc_Hammer" - ], - "asset": "U000_ARC3106_ENG.WAV" + "Unit_Group_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0303_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0304_ENG.WAV", + "message": "Audio file \u0027U000_LEI0304_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0303_ENG.WAV" + "Unit_Group_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0404_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0315_ENG.WAV", + "message": "Audio file \u0027U000_LEI0315_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Guard_Leia" - ], - "asset": "U000_LEI0404_ENG.WAV" + "Unit_Group_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027TESTUNITMOVE_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0112_ENG.WAV", + "message": "Audio file \u0027U000_LEI0112_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Move_Gneneric_Test" - ], - "asset": "TESTUNITMOVE_ENG.WAV" + "Unit_Select_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0401_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "FS_BEETLE_4.WAV", + "message": "Audio file \u0027FS_BEETLE_4.WAV\u0027 could not be found.", "context": [ - "Unit_Guard_Leia" - ], - "asset": "U000_LEI0401_ENG.WAV" + "SFX_Anim_Beetle_Footsteps" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027EGL_STAR_VIPER_SPINNING_1.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0109_ENG.WAV", + "message": "Audio file \u0027U000_LEI0109_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Star_Viper_Spinning_By" - ], - "asset": "EGL_STAR_VIPER_SPINNING_1.WAV" + "Unit_Select_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_TMC0212_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0309_ENG.WAV", + "message": "Audio file \u0027U000_LEI0309_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Move_Tie_Mauler" - ], - "asset": "U000_TMC0212_ENG.WAV" + "Unit_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0110_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0106_ENG.WAV", + "message": "Audio file \u0027U000_LEI0106_ENG.WAV\u0027 could not be found.", "context": [ "Unit_Select_Leia" - ], - "asset": "U000_LEI0110_ENG.WAV" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0314_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0301_ENG.WAV", + "message": "Audio file \u0027U000_LEI0301_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0314_ENG.WAV" + "Unit_Group_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0305_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0209_ENG.WAV", + "message": "Audio file \u0027U000_LEI0209_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0305_ENG.WAV" + "Unit_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0112_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0208_ENG.WAV", + "message": "Audio file \u0027U000_LEI0208_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0112_ENG.WAV" + "Unit_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0209_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0503_ENG.WAV", + "message": "Audio file \u0027U000_LEI0503_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0209_ENG.WAV" + "Unit_Remove_Corruption_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0211_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0208_ENG.WAV", + "message": "Audio file \u0027U000_LEI0208_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0211_ENG.WAV" + "Unit_Group_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0205_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0206_ENG.WAV", + "message": "Audio file \u0027U000_LEI0206_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0205_ENG.WAV" + "Unit_Fleet_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0115_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0502_ENG.WAV", + "message": "Audio file \u0027U000_LEI0502_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0115_ENG.WAV" + "Unit_Remove_Corruption_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0604_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0204_ENG.WAV", + "message": "Audio file \u0027U000_LEI0204_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Increase_Production_Leia" - ], - "asset": "U000_LEI0604_ENG.WAV" + "Unit_Group_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0602_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "underworld_logo_off.tga", + "message": "Could not find GUI texture \u0027underworld_logo_off.tga\u0027 at location \u0027MegaTexture\u0027.", "context": [ - "Unit_Increase_Production_Leia" - ], - "asset": "U000_LEI0602_ENG.WAV" + "IDC_PLAY_FACTION_A_BUTTON_BIG", + "MegaTexture" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0315_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "i_dialogue_button_large_middle_off.tga", + "message": "Could not find GUI texture \u0027i_dialogue_button_large_middle_off.tga\u0027 at location \u0027Repository\u0027.", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0315_ENG.WAV" + "IDC_PLAY_FACTION_B_BUTTON_BIG", + "Repository" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_DEF3006_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "underworld_logo_rollover.tga", + "message": "Could not find GUI texture \u0027underworld_logo_rollover.tga\u0027 at location \u0027MegaTexture\u0027.", "context": [ - "Unit_Corrupt_Sabateur" - ], - "asset": "U000_DEF3006_ENG.WAV" + "IDC_PLAY_FACTION_A_BUTTON_BIG", + "MegaTexture" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0210_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "underworld_logo_selected.tga", + "message": "Could not find GUI texture \u0027underworld_logo_selected.tga\u0027 at location \u0027MegaTexture\u0027.", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0210_ENG.WAV" + "IDC_PLAY_FACTION_A_BUTTON_BIG", + "MegaTexture" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0105_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "lookat", + "message": "Proxy particle \u0027lookat\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_ECLIPSE_UC.ALO\u0027", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0105_ENG.WAV" + "Eclipse_Super_Star_Destroyer", + "DATA\\ART\\MODELS\\UV_ECLIPSE_UC.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0208_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "w_planet_volcanic.alo", + "message": "Unable to find .ALO file \u0027w_planet_volcanic.alo\u0027", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0208_ENG.WAV" + "Volcanic_Backdrop_Large" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0106_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Default.fx", + "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_SKIPRAY.ALO\u0027.", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0106_ENG.WAV" + "Skipray_Bombing_Run", + "DATA\\ART\\MODELS\\UV_SKIPRAY.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0202_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "pe_bwing_yellow", + "message": "Proxy particle \u0027pe_bwing_yellow\u0027 not found for model \u0027DATA\\ART\\MODELS\\RV_BWING.ALO\u0027", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0202_ENG.WAV" + "B-Wing", + "DATA\\ART\\MODELS\\RV_BWING.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0306_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Cin_DeathStar_High.alo", + "message": "Unable to find .ALO file \u0027Cin_DeathStar_High.alo\u0027", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0306_ENG.WAV" + "Death_Star_Whole_small" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0101_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "CIN_Rbel_NavyRow.alo", + "message": "Unable to find .ALO file \u0027CIN_Rbel_NavyRow.alo\u0027", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0101_ENG.WAV" + "Cin_Rebel_NavyRow" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027C000_DST0102_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "W_AllShaders.ALO", + "message": "Unable to find .ALO file \u0027W_AllShaders.ALO\u0027", "context": [ - "EHD_Death_Star_Activate" - ], - "asset": "C000_DST0102_ENG.WAV" + "Prop_AllShaders" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0103_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "p_prison_light", + "message": "Proxy particle \u0027p_prison_light\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_PRISON.ALO\u0027", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0103_ENG.WAV" + "Imperial_Prison_Facility", + "DATA\\ART\\MODELS\\NB_PRISON.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0403_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Cin_DeathStar_High.alo", + "message": "Unable to find .ALO file \u0027Cin_DeathStar_High.alo\u0027", "context": [ - "Unit_Guard_Leia" - ], - "asset": "U000_LEI0403_ENG.WAV" + "Death_Star_Whole_Vsmall" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027FS_BEETLE_2.WAV\u0027 could not be found.", "severity": "Error", + "asset": "W_Vol_Steam01.ALO", + "message": "Unable to find .ALO file \u0027W_Vol_Steam01.ALO\u0027", "context": [ - "SFX_Anim_Beetle_Footsteps" - ], - "asset": "FS_BEETLE_2.WAV" + "Prop_Vol_Steam01" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0201_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Cin_Coruscant.alo", + "message": "Unable to find .ALO file \u0027Cin_Coruscant.alo\u0027", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0201_ENG.WAV" + "Corusant_Backdrop_Large 6x" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0203_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Default.fx", + "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_CRUSADERCLASSCORVETTE.ALO\u0027.", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0203_ENG.WAV" + "Crusader_Gunship", + "DATA\\ART\\MODELS\\UV_CRUSADERCLASSCORVETTE.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0114_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Lensflare0", + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_HIGH.ALO\u0027", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0114_ENG.WAV" + "Stars_High", + "DATA\\ART\\MODELS\\W_STARS_HIGH.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027FS_BEETLE_1.WAV\u0027 could not be found.", "severity": "Error", + "asset": "lookat", + "message": "Proxy particle \u0027lookat\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_ECLIPSE.ALO\u0027", "context": [ - "SFX_Anim_Beetle_Footsteps" - ], - "asset": "FS_BEETLE_1.WAV" + "Eclipse_Prop", + "DATA\\ART\\MODELS\\UV_ECLIPSE.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0304_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "p_uwstation_death", + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_05_STATION_D.ALO\u0027", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0304_ENG.WAV" + "Underworld_Star_Base_5_Death_Clone", + "DATA\\ART\\MODELS\\UB_05_STATION_D.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_DeathStar.tga", + "message": "Could not find texture \u0027Cin_DeathStar.tga\u0027 for context: [Test_Base_Hector--\u003EALTTEST.ALO].", + "context": [ + "Test_Base_Hector", + "ALTTEST.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Lensflare0", + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_CINE_LUA.ALO\u0027", + "context": [ + "Stars_Lua_Cinematic", + "DATA\\ART\\MODELS\\W_STARS_CINE_LUA.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_DStar_LeverPanel.alo", + "message": "Unable to find .ALO file \u0027Cin_DStar_LeverPanel.alo\u0027", + "context": [ + "Death_Star_LeverPanel" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Lensflare0", + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_LOW.ALO\u0027", + "context": [ + "Stars_Low", + "DATA\\ART\\MODELS\\W_STARS_LOW.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "W_TE_Rock_f_02_b.tga", + "message": "Could not find texture \u0027W_TE_Rock_f_02_b.tga\u0027 for context: [Vengeance_Frigate--\u003EUV_VENGEANCE.ALO].", + "context": [ + "Vengeance_Frigate", + "UV_VENGEANCE.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_rv_XWingProp.alo", + "message": "Unable to find .ALO file \u0027Cin_rv_XWingProp.alo\u0027", + "context": [ + "Cin_X-WingProp" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CIN_Lambda_Mouth.alo", + "message": "Unable to find .ALO file \u0027CIN_Lambda_Mouth.alo\u0027", + "context": [ + "Cin_Lambda_Mouth" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_uwstation_death", + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_04_STATION_D.ALO\u0027", + "context": [ + "Underworld_Star_Base_4_Death_Clone", + "DATA\\ART\\MODELS\\UB_04_STATION_D.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_EV_lambdaShuttle_150.alo", + "message": "Unable to find .ALO file \u0027Cin_EV_lambdaShuttle_150.alo\u0027", + "context": [ + "Lambda_Shuttle_150X6-9" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CIN_Rbel_Soldier_Group.alo", + "message": "Unable to find .ALO file \u0027CIN_Rbel_Soldier_Group.alo\u0027", + "context": [ + "Cin_Rebel_SoldierRow" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_EI_Palpatine.alo", + "message": "Unable to find .ALO file \u0027Cin_EI_Palpatine.alo\u0027", + "context": [ + "Cin_Emperor_Shot_5" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_Reb_CelebHall_Wall_B.tga", + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall_B.tga\u0027 for context: [Cin_sith_console--\u003EW_SITH_CONSOLE.ALO].", + "context": [ + "Cin_sith_console", + "W_SITH_CONSOLE.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_rv_XWingProp.alo", + "message": "Unable to find .ALO file \u0027Cin_rv_XWingProp.alo\u0027", + "context": [ + "Grounded_Xwing" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_EV_Stardestroyer_Warp.alo", + "message": "Unable to find .ALO file \u0027Cin_EV_Stardestroyer_Warp.alo\u0027", + "context": [ + "Star_Destroyer_Warp" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_particle_master", + "message": "Could not find texture \u0027p_particle_master\u0027 for context: [Test_Particle--\u003EP_DIRT_EMITTER_TEST1.ALO].", + "context": [ + "Test_Particle", + "P_DIRT_EMITTER_TEST1.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "W_Bush_Swmp00.ALO", + "message": "Unable to find .ALO file \u0027W_Bush_Swmp00.ALO\u0027", + "context": [ + "Prop_Swamp_Bush00" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "W_TE_Rock_f_02_b.tga", + "message": "Could not find texture \u0027W_TE_Rock_f_02_b.tga\u0027 for context: [The_Peacebringer--\u003EUV_KRAYTCLASSDESTROYER_TYBER.ALO].", + "context": [ + "The_Peacebringer", + "UV_KRAYTCLASSDESTROYER_TYBER.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_Reb_CelebHall_Wall_B.tga", + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall_B.tga\u0027 for context: [Cin_sith_lefthall--\u003EW_SITH_LEFTHALL.ALO].", + "context": [ + "Cin_sith_lefthall", + "W_SITH_LEFTHALL.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_Shuttle_Tyderium.alo", + "message": "Unable to find .ALO file \u0027Cin_Shuttle_Tyderium.alo\u0027", + "context": [ + "Intro2_Shuttle_Tyderium" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_smoke_small_thin2", + "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_PRISON.ALO\u0027", + "context": [ + "Imperial_Prison_Facility", + "DATA\\ART\\MODELS\\NB_PRISON.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "P_mptl-2a_Die", + "message": "Proxy particle \u0027P_mptl-2a_Die\u0027 not found for model \u0027DATA\\ART\\MODELS\\RV_MPTL-2A.ALO\u0027", + "context": [ + "MPTL", + "DATA\\ART\\MODELS\\RV_MPTL-2A.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_DeathStar_High.alo", + "message": "Unable to find .ALO file \u0027Cin_DeathStar_High.alo\u0027", + "context": [ + "UM05_PROP_DSTAR" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CIN_Rbel_GreyGroup.alo", + "message": "Unable to find .ALO file \u0027CIN_Rbel_GreyGroup.alo\u0027", + "context": [ + "Cin_Rebel_GreyGroup" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_Officer.alo", + "message": "Unable to find .ALO file \u0027Cin_Officer.alo\u0027", + "context": [ + "FIN_Officer" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CIN_Reb_CelebHall.alo", + "message": "Unable to find .ALO file \u0027CIN_Reb_CelebHall.alo\u0027", + "context": [ + "REb_CelebHall" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_ewok_drag_dirt", + "message": "Proxy particle \u0027p_ewok_drag_dirt\u0027 not found for model \u0027DATA\\ART\\MODELS\\UI_EWOK_HANDLER.ALO\u0027", + "context": [ + "Ewok_Handler", + "DATA\\ART\\MODELS\\UI_EWOK_HANDLER.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_steam_small", + "message": "Proxy particle \u0027p_steam_small\u0027 not found for model \u0027DATA\\ART\\MODELS\\RB_HEAVYVEHICLEFACTORY.ALO\u0027", + "context": [ + "R_Ground_Heavy_Vehicle_Factory", + "DATA\\ART\\MODELS\\RB_HEAVYVEHICLEFACTORY.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_DeathStar_Wall.alo", + "message": "Unable to find .ALO file \u0027Cin_DeathStar_Wall.alo\u0027", + "context": [ + "Death_Star_Hangar_Outside" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "W_Kamino_Reflect.ALO", + "message": "Unable to find .ALO file \u0027W_Kamino_Reflect.ALO\u0027", + "context": [ + "Prop_Kamino_Reflection_00" + ] + }, + { + "id": "FILE00", + "severity": "Warning", + "asset": "i_button_general_dodonna.tga", + "message": "Could not find icon \u0027i_button_general_dodonna.tga\u0027 for game object type \u0027General_Dodonna\u0027.", + "context": [ + "General_Dodonna" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_cold_tiny01", + "message": "Proxy particle \u0027p_cold_tiny01\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_SCH.ALO\u0027", + "context": [ + "Arctic_Civilian_Spawn_House", + "DATA\\ART\\MODELS\\NB_SCH.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_desert_ground_dust", + "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\EI_MARAJADE.ALO\u0027", + "context": [ + "Mara_Jade", + "DATA\\ART\\MODELS\\EI_MARAJADE.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_smoke_small_thin2", + "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027DATA\\ART\\MODELS\\RB_HYPERVELOCITYGUN.ALO\u0027", + "context": [ + "Ground_Empire_Hypervelocity_Gun", + "DATA\\ART\\MODELS\\RB_HYPERVELOCITYGUN.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_uwstation_death", + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_02_STATION_D.ALO\u0027", + "context": [ + "Underworld_Star_Base_2_Death_Clone", + "DATA\\ART\\MODELS\\UB_02_STATION_D.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_splash_wake_lava.alo", + "message": "Unable to find .ALO file \u0027p_splash_wake_lava.alo\u0027", + "context": [ + "Splash_Wake_Lava" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_Reb_CelebHall_Wall.tga", + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall.tga\u0027 for context: [Cin_sith_console--\u003EW_SITH_CONSOLE.ALO].", + "context": [ + "Cin_sith_console", + "W_SITH_CONSOLE.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_EV_TieAdvanced.alo", + "message": "Unable to find .ALO file \u0027Cin_EV_TieAdvanced.alo\u0027", + "context": [ + "Fin_Vader_TIE" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_uwstation_death", + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_03_STATION_D.ALO\u0027", + "context": [ + "Underworld_Star_Base_3_Death_Clone", + "DATA\\ART\\MODELS\\UB_03_STATION_D.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CIN_Trooper_Row.alo", + "message": "Unable to find .ALO file \u0027CIN_Trooper_Row.alo\u0027", + "context": [ + "Cin_Trooper_Row" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CIN_Officer_Row.alo", + "message": "Unable to find .ALO file \u0027CIN_Officer_Row.alo\u0027", + "context": [ + "Cin_Officer_Row" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CINE_EV_StarDestroyer.ALO", + "message": "Unable to find .ALO file \u0027CINE_EV_StarDestroyer.ALO\u0027", + "context": [ + "CIN_Star_Destroyer3X" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_EV_lambdaShuttle_150.alo", + "message": "Unable to find .ALO file \u0027Cin_EV_lambdaShuttle_150.alo\u0027", + "context": [ + "Lambda_Shuttle_150" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Lensflare0", + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_MEDIUM.ALO\u0027", + "context": [ + "Stars_Medium", + "DATA\\ART\\MODELS\\W_STARS_MEDIUM.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_DStar_TurretLasers.alo", + "message": "Unable to find .ALO file \u0027Cin_DStar_TurretLasers.alo\u0027", + "context": [ + "TurretLasers_DStar_Xplode" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CIN_Rbel_Soldier.alo", + "message": "Unable to find .ALO file \u0027CIN_Rbel_Soldier.alo\u0027", + "context": [ + "Cin_Rebel_soldier" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0301_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "p_smoke_small_thin4", + "message": "Proxy particle \u0027p_smoke_small_thin4\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_PRISON.ALO\u0027", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0301_ENG.WAV" + "Imperial_Prison_Facility", + "DATA\\ART\\MODELS\\NB_PRISON.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0503_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Cin_ImperialCraft.alo", + "message": "Unable to find .ALO file \u0027Cin_ImperialCraft.alo\u0027", "context": [ - "Unit_Remove_Corruption_Leia" - ], - "asset": "U000_LEI0503_ENG.WAV" + "Intro2_ImperialCraft" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0109_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "W_SwampGasEmit.ALO", + "message": "Unable to find .ALO file \u0027W_SwampGasEmit.ALO\u0027", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0109_ENG.WAV" + "Prop_SwampGasEmitter" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0308_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "p_desert_ground_dust", + "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\UI_IG88.ALO\u0027", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0308_ENG.WAV" + "IG-88", + "DATA\\ART\\MODELS\\UI_IG88.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0402_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Cin_Planet_Hoth_High.alo", + "message": "Unable to find .ALO file \u0027Cin_Planet_Hoth_High.alo\u0027", "context": [ - "Unit_Guard_Leia" - ], - "asset": "U000_LEI0402_ENG.WAV" + "Hoth_Backdrop_Large 6x" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0108_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "NB_YsalamiriTree_B.tga", + "message": "Could not find texture \u0027NB_YsalamiriTree_B.tga\u0027 for context: [Ysalamiri_Tree--\u003ENB_YSALAMIRI_TREE.ALO].", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0108_ENG.WAV" + "Ysalamiri_Tree", + "NB_YSALAMIRI_TREE.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0307_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "CIN_DeathStar_Hangar.alo", + "message": "Unable to find .ALO file \u0027CIN_DeathStar_Hangar.alo\u0027", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0307_ENG.WAV" + "Cin_DeathStar_Hangar" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0311_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Lensflare0", + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_CINE.ALO\u0027", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0311_ENG.WAV" + "Stars_Cinematic", + "DATA\\ART\\MODELS\\W_STARS_CINE.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0102_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "w_sith_arch.alo", + "message": "Unable to find .ALO file \u0027w_sith_arch.alo\u0027", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0102_ENG.WAV" + "Cin_sith_arch" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0104_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "W_TE_Rock_f_02_b.tga", + "message": "Could not find texture \u0027W_TE_Rock_f_02_b.tga\u0027 for context: [F9TZ_Cloaking_Transport--\u003EUV_F9TZTRANSPORT.ALO].", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0104_ENG.WAV" + "F9TZ_Cloaking_Transport", + "UV_F9TZTRANSPORT.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027FS_BEETLE_3.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Cin_Reb_CelebHall_Wall_B.tga", + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall_B.tga\u0027 for context: [Cin_w_tile--\u003EW_TILE.ALO].", "context": [ - "SFX_Anim_Beetle_Footsteps" - ], - "asset": "FS_BEETLE_3.WAV" + "Cin_w_tile", + "W_TILE.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0313_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "CIN_Probe_Droid.alo", + "message": "Unable to find .ALO file \u0027CIN_Probe_Droid.alo\u0027", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0313_ENG.WAV" + "Empire_Droid" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0206_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "CIN_Fire_Medium.alo", + "message": "Unable to find .ALO file \u0027CIN_Fire_Medium.alo\u0027", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0206_ENG.WAV" + "Fin_Fire_Medium" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_ARC3104_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Cin_Planet_Alderaan_High.alo", + "message": "Unable to find .ALO file \u0027Cin_Planet_Alderaan_High.alo\u0027", "context": [ - "Unit_Produce_Troops_Arc_Hammer" - ], - "asset": "U000_ARC3104_ENG.WAV" + "Alderaan_Backdrop_Large 6x" + ] + }, + { + "id": "FILE00", + "severity": "Warning", + "asset": "i_button_ni_nightsister_ranger.tga", + "message": "Could not find icon \u0027i_button_ni_nightsister_ranger.tga\u0027 for game object type \u0027Dathomir_Night_Sister\u0027.", + "context": [ + "Dathomir_Night_Sister" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0312_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Cin_Reb_CelebHall_Wall.tga", + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall.tga\u0027 for context: [Cin_w_tile--\u003EW_TILE.ALO].", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0312_ENG.WAV" + "Cin_w_tile", + "W_TILE.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0215_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "W_Kamino_Reflect.ALO", + "message": "Unable to find .ALO file \u0027W_Kamino_Reflect.ALO\u0027", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0215_ENG.WAV" + "Prop_Kamino_Reflection_01" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0107_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "CIN_Lambda_Head.alo", + "message": "Unable to find .ALO file \u0027CIN_Lambda_Head.alo\u0027", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0107_ENG.WAV" + "Cin_Lambda_Head" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0501_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "p_explosion_small_delay00", + "message": "Proxy particle \u0027p_explosion_small_delay00\u0027 not found for model \u0027DATA\\ART\\MODELS\\EB_COMMANDCENTER.ALO\u0027", "context": [ - "Unit_Remove_Corruption_Leia" - ], - "asset": "U000_LEI0501_ENG.WAV" + "Imperial_Command_Center", + "DATA\\ART\\MODELS\\EB_COMMANDCENTER.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027AMB_DES_CLEAR_LOOP_1.WAV\u0027 could not be found.", "severity": "Error", + "asset": "p_smoke_small_thin2", + "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_MONCAL_BUILDING.ALO\u0027", "context": [ - "Weather_Ambient_Clear_Sandstorm_Loop" - ], - "asset": "AMB_DES_CLEAR_LOOP_1.WAV" + "MonCalamari_Spawn_House", + "DATA\\ART\\MODELS\\NB_MONCAL_BUILDING.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0504_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "W_Volcano_Rock02.ALO", + "message": "Unable to find .ALO file \u0027W_Volcano_Rock02.ALO\u0027", "context": [ - "Unit_Remove_Corruption_Leia" - ], - "asset": "U000_LEI0504_ENG.WAV" + "Prop_Volcano_RockForm03" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0502_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Cin_EI_Vader.alo", + "message": "Unable to find .ALO file \u0027Cin_EI_Vader.alo\u0027", "context": [ - "Unit_Remove_Corruption_Leia" - ], - "asset": "U000_LEI0502_ENG.WAV" + "Cin_Vader_Shot_6-9" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_DEF3106_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "p_uwstation_death", + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_01_STATION_D.ALO\u0027", "context": [ - "Unit_Weaken_Sabateur" - ], - "asset": "U000_DEF3106_ENG.WAV" + "Underworld_Star_Base_1_Death_Clone", + "DATA\\ART\\MODELS\\UB_01_STATION_D.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_ARC3105_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Cin_Reb_CelebHall_Wall.tga", + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall.tga\u0027 for context: [Cin_sith_lefthall--\u003EW_SITH_LEFTHALL.ALO].", "context": [ - "Unit_Complete_Troops_Arc_Hammer" - ], - "asset": "U000_ARC3105_ENG.WAV" + "Cin_sith_lefthall", + "W_SITH_LEFTHALL.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0601_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "CIN_NavyTrooper_Row.alo", + "message": "Unable to find .ALO file \u0027CIN_NavyTrooper_Row.alo\u0027", "context": [ - "Unit_Increase_Production_Leia" - ], - "asset": "U000_LEI0601_ENG.WAV" + "Cin_NavyTrooper_Row" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0204_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "p_ssd_debris", + "message": "Proxy particle \u0027p_ssd_debris\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_ECLIPSE_UC_DC.ALO\u0027", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0204_ENG.WAV" + "Eclipse_Super_Star_Destroyer_Death_Clone", + "DATA\\ART\\MODELS\\UV_ECLIPSE_UC_DC.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0207_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Cin_EI_Vader.alo", + "message": "Unable to find .ALO file \u0027Cin_EI_Vader.alo\u0027", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0207_ENG.WAV" + "Cin_Vader" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027FS_BEETLE_4.WAV\u0027 could not be found.", "severity": "Error", + "asset": "p_explosion_smoke_small_thin5", + "message": "Proxy particle \u0027p_explosion_smoke_small_thin5\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_NOGHRI_HUT.ALO\u0027", "context": [ - "SFX_Anim_Beetle_Footsteps" - ], - "asset": "FS_BEETLE_4.WAV" + "Noghri_Spawn_House", + "DATA\\ART\\MODELS\\NB_NOGHRI_HUT.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027AMB_URB_CLEAR_LOOP_1.WAV\u0027 could not be found.", "severity": "Error", + "asset": "p_hp_archammer-damage", + "message": "Proxy particle \u0027p_hp_archammer-damage\u0027 not found for model \u0027DATA\\ART\\MODELS\\EV_ARCHAMMER.ALO\u0027", "context": [ - "Weather_Ambient_Clear_Urban_Loop" - ], - "asset": "AMB_URB_CLEAR_LOOP_1.WAV" + "Arc_Hammer", + "DATA\\ART\\MODELS\\EV_ARCHAMMER.ALO" + ] }, { "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", + "asset": "NB_YsalamiriTree_B.tga", + "message": "Could not find texture \u0027NB_YsalamiriTree_B.tga\u0027 for context: [Underworld_Ysalamiri_Cage--\u003EUV_MDU_CAGE.ALO].", "context": [ - "IDC_PLAY_FACTION_B_BUTTON_BIG", - "Repository" - ], - "asset": "i_dialogue_button_large_middle_off.tga" + "Underworld_Ysalamiri_Cage", + "UV_MDU_CAGE.ALO" + ] }, { "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", + "asset": "Cin_bridge.alo", + "message": "Unable to find .ALO file \u0027Cin_bridge.alo\u0027", "context": [ - "IDC_PLAY_FACTION_A_BUTTON_BIG", - "MegaTexture" - ], - "asset": "underworld_logo_selected.tga" + "UM05_PROP_BRIDGE" + ] }, { "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", + "asset": "Cin_DStar_protons.alo", + "message": "Unable to find .ALO file \u0027Cin_DStar_protons.alo\u0027", "context": [ - "IDC_PLAY_FACTION_A_BUTTON_BIG", - "MegaTexture" - ], - "asset": "underworld_logo_off.tga" + "Protons_DStar_Xplode" + ] }, { "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", + "asset": "Cin_DeathStar_High.alo", + "message": "Unable to find .ALO file \u0027Cin_DeathStar_High.alo\u0027", "context": [ - "IDC_MENU_PETRO_LOGO", - "MegaTexture" - ], - "asset": "i_button_petro_sliver.tga" + "Death_Star_Whole" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Default.fx", + "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\EV_TIE_LANCET.ALO\u0027.", + "context": [ + "Lancet_Air_Artillery", + "DATA\\ART\\MODELS\\EV_TIE_LANCET.ALO" + ] }, { "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", + "asset": "UB_girder_B.tga", + "message": "Could not find texture \u0027UB_girder_B.tga\u0027 for context: [Underworld_Ysalamiri_Cage--\u003EUV_MDU_CAGE.ALO].", "context": [ - "IDC_PLAY_FACTION_A_BUTTON_BIG", - "MegaTexture" - ], - "asset": "underworld_logo_rollover.tga" + "Underworld_Ysalamiri_Cage", + "UV_MDU_CAGE.ALO" + ] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_planet_land_forces\u0027 is not connected to a shell component.", + "id": "FILE00", + "severity": "Error", + "asset": "Cin_bridge.alo", + "message": "Unable to find .ALO file \u0027Cin_bridge.alo\u0027", + "context": [ + "Imperial_Bridge" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_desert_ground_dust", + "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\UI_SABOTEUR.ALO\u0027", + "context": [ + "Underworld_Saboteur", + "DATA\\ART\\MODELS\\UI_SABOTEUR.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CIN_p_proton_torpedo.alo", + "message": "Unable to find .ALO file \u0027CIN_p_proton_torpedo.alo\u0027", + "context": [ + "Cin_Proj_Ground_Proton_Torpedo" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CIN_Rbel_grey.alo", + "message": "Unable to find .ALO file \u0027CIN_Rbel_grey.alo\u0027", + "context": [ + "Cin_Rebel_Grey" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Default.fx", + "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\EV_MDU_SENSORNODE.ALO\u0027.", + "context": [ + "Empire_Offensive_Sensor_Node", + "DATA\\ART\\MODELS\\EV_MDU_SENSORNODE.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "w_grenade.tga", + "message": "Could not find texture \u0027w_grenade.tga\u0027 for context: [Proj_Merc_Concussion_Grenade--\u003EW_GRENADE.ALO].", + "context": [ + "Proj_Merc_Concussion_Grenade", + "W_GRENADE.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CIN_Fire_Huge.alo", + "message": "Unable to find .ALO file \u0027CIN_Fire_Huge.alo\u0027", + "context": [ + "Fin_Fire_Huge" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_EI_Palpatine.alo", + "message": "Unable to find .ALO file \u0027Cin_EI_Palpatine.alo\u0027", + "context": [ + "Cin_Emperor_Shot_6-9" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_desert_ground_dust", + "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\RI_KYLEKATARN.ALO\u0027", + "context": [ + "Kyle_Katarn", + "DATA\\ART\\MODELS\\RI_KYLEKATARN.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "P_heat_small01", + "message": "Proxy particle \u0027P_heat_small01\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_VCH.ALO\u0027", + "context": [ + "Volcanic_Civilian_Spawn_House_Independent_AI", + "DATA\\ART\\MODELS\\NB_VCH.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CIN_REb_CelebCharacters.alo", + "message": "Unable to find .ALO file \u0027CIN_REb_CelebCharacters.alo\u0027", + "context": [ + "REb_CelebCharacters" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "W_TE_Rock_f_02_b.tga", + "message": "Could not find texture \u0027W_TE_Rock_f_02_b.tga\u0027 for context: [TIE_Phantom--\u003EEV_TIE_PHANTOM.ALO].", + "context": [ + "TIE_Phantom", + "EV_TIE_PHANTOM.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CIN_Biker_Row.alo", + "message": "Unable to find .ALO file \u0027CIN_Biker_Row.alo\u0027", + "context": [ + "Cin_Biker_Row" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_bomb_spin", + "message": "Proxy particle \u0027p_bomb_spin\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_THERMAL_DETONATOR_EMPIRE.ALO\u0027", + "context": [ + "TIE_Bomber_Bombing_Run_Bomb", + "DATA\\ART\\MODELS\\W_THERMAL_DETONATOR_EMPIRE.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_DStar_Dish_close.alo", + "message": "Unable to find .ALO file \u0027Cin_DStar_Dish_close.alo\u0027", + "context": [ + "Death_Star_Dish_Close" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "W_droid_steam.alo", + "message": "Unable to find .ALO file \u0027W_droid_steam.alo\u0027", + "context": [ + "Prop_Droid_Steam" + ] + }, + { + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_planet_land_forces" + "asset": "b_planet_left", + "message": "The CommandBar component \u0027b_planet_left\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_ground_sell\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_ground_sell" + "asset": "encyclopedia_header_text", + "message": "The CommandBar component \u0027encyclopedia_header_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_bracket_medium\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_bracket_medium" + "asset": "st_control_group", + "message": "The CommandBar component \u0027st_control_group\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027b_planet_right\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "b_planet_right" + "asset": "tutorial_text_back", + "message": "The CommandBar component \u0027tutorial_text_back\u0027 is not connected to a shell component.", + "context": [] }, { "id": "CMDBAR04", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_credit_bar\u0027 is not supported by the game.", - "severity": "Information", - "context": [], - "asset": "g_credit_bar" + "severity": "Warning", + "asset": "encyclopedia_back", + "message": "The CommandBar component \u0027encyclopedia_back\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027zoomed_header_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "zoomed_header_text" + "asset": "g_build", + "message": "The CommandBar component \u0027g_build\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_space_level_pips\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_space_level_pips" + "asset": "generic_collision", + "message": "The CommandBar component \u0027generic_collision\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027bribed_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "bribed_icon" + "asset": "objective_back", + "message": "The CommandBar component \u0027objective_back\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027encyclopedia_header_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "encyclopedia_header_text" + "asset": "st_shields_large", + "message": "The CommandBar component \u0027st_shields_large\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027tutorial_text_back\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "tutorial_text_back" + "asset": "zoomed_cost_text", + "message": "The CommandBar component \u0027zoomed_cost_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027encyclopedia_back\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "encyclopedia_back" + "asset": "st_bracket_medium", + "message": "The CommandBar component \u0027st_bracket_medium\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027encyclopedia_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "encyclopedia_text" + "asset": "g_smuggled", + "message": "The CommandBar component \u0027g_smuggled\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027balance_pip\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "balance_pip" + "asset": "skirmish_upgrade", + "message": "The CommandBar component \u0027skirmish_upgrade\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027objective_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "objective_text" + "asset": "st_ability_icon", + "message": "The CommandBar component \u0027st_ability_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027skirmish_upgrade\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "skirmish_upgrade" + "asset": "g_bounty_hunter", + "message": "The CommandBar component \u0027g_bounty_hunter\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027surface_mod_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "surface_mod_icon" + "asset": "radar_blip", + "message": "The CommandBar component \u0027radar_blip\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_hero_health\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_hero_health" + "asset": "g_weather", + "message": "The CommandBar component \u0027g_weather\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027bribe_display\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "bribe_display" + "asset": "st_health_medium", + "message": "The CommandBar component \u0027st_health_medium\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_build\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_build" + "asset": "tutorial_text", + "message": "The CommandBar component \u0027tutorial_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027garrison_slot_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "garrison_slot_icon" + "asset": "garrison_slot_icon", + "message": "The CommandBar component \u0027garrison_slot_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_conflict\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_conflict" + "asset": "encyclopedia_right_text", + "message": "The CommandBar component \u0027encyclopedia_right_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027tooltip_name\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "tooltip_name" + "asset": "g_planet_ring", + "message": "The CommandBar component \u0027g_planet_ring\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027garrison_respawn_counter\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "garrison_respawn_counter" + "asset": "tooltip_price", + "message": "The CommandBar component \u0027tooltip_price\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_ability_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_ability_icon" + "asset": "bribe_display", + "message": "The CommandBar component \u0027bribe_display\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_shields_medium\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_shields_medium" + "asset": "tooltip_icon_land", + "message": "The CommandBar component \u0027tooltip_icon_land\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_health\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_health" + "asset": "encyclopedia_center_text", + "message": "The CommandBar component \u0027encyclopedia_center_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_weather\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_weather" + "asset": "st_bracket_small", + "message": "The CommandBar component \u0027st_bracket_small\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_health_medium\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_health_medium" + "asset": "g_ground_sell", + "message": "The CommandBar component \u0027g_ground_sell\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_power\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_power" + "asset": "st_shields_medium", + "message": "The CommandBar component \u0027st_shields_medium\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_ground_level_pips\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_ground_level_pips" + "asset": "g_corruption_text", + "message": "The CommandBar component \u0027g_corruption_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027zoomed_cost_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "zoomed_cost_text" + "asset": "st_health_bar", + "message": "The CommandBar component \u0027st_health_bar\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027bm_title_4011\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "bm_title_4011" + "asset": "b_quick_ref", + "message": "The CommandBar component \u0027b_quick_ref\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_planet_name\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_planet_name" + "asset": "g_special_ability", + "message": "The CommandBar component \u0027g_special_ability\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_shields_large\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_shields_large" + "asset": "g_hero_icon", + "message": "The CommandBar component \u0027g_hero_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_hero_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_hero_icon" + "asset": "st_hero_health", + "message": "The CommandBar component \u0027st_hero_health\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027generic_flytext\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "generic_flytext" + "asset": "bribed_icon", + "message": "The CommandBar component \u0027bribed_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027reinforcement_counter\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "reinforcement_counter" + "asset": "g_credit_bar", + "message": "The CommandBar component \u0027g_credit_bar\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_planet_value\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_planet_value" + "asset": "g_space_icon", + "message": "The CommandBar component \u0027g_space_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027radar_blip\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "radar_blip" + "asset": "bm_title_4010", + "message": "The CommandBar component \u0027bm_title_4010\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_political_control\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_political_control" + "asset": "help_back", + "message": "The CommandBar component \u0027help_back\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_planet_ring\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_planet_ring" + "asset": "g_space_level", + "message": "The CommandBar component \u0027g_space_level\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_garrison_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_garrison_icon" + "asset": "g_planet_ability", + "message": "The CommandBar component \u0027g_planet_ability\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027encyclopedia_right_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "encyclopedia_right_text" + "asset": "st_shields", + "message": "The CommandBar component \u0027st_shields\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027b_quick_ref\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "b_quick_ref" + "asset": "b_planet_right", + "message": "The CommandBar component \u0027b_planet_right\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027objective_back\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "objective_back" + "asset": "st_health_large", + "message": "The CommandBar component \u0027st_health_large\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027encyclopedia_center_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "encyclopedia_center_text" + "asset": "bm_title_4011", + "message": "The CommandBar component \u0027bm_title_4011\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_shields\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_shields" + "asset": "g_ground_level_pips", + "message": "The CommandBar component \u0027g_ground_level_pips\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_grab_bar\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_grab_bar" + "asset": "g_political_control", + "message": "The CommandBar component \u0027g_political_control\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_smuggler\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_smuggler" + "asset": "st_hero_icon", + "message": "The CommandBar component \u0027st_hero_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_enemy_hero\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_enemy_hero" + "asset": "objective_text", + "message": "The CommandBar component \u0027objective_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027cs_ability_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "cs_ability_text" + "asset": "zoomed_text", + "message": "The CommandBar component \u0027zoomed_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027encyclopedia_cost_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "encyclopedia_cost_text" + "asset": "garrison_respawn_counter", + "message": "The CommandBar component \u0027garrison_respawn_counter\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_planet_ability\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_planet_ability" + "asset": "st_grab_bar", + "message": "The CommandBar component \u0027st_grab_bar\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_control_group\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_control_group" + "asset": "zoomed_center_text", + "message": "The CommandBar component \u0027zoomed_center_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027gui_dialog_tooltip\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "gui_dialog_tooltip" + "asset": "g_space_level_pips", + "message": "The CommandBar component \u0027g_space_level_pips\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027remote_bomb_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "remote_bomb_icon" + "asset": "generic_flytext", + "message": "The CommandBar component \u0027generic_flytext\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027tutorial_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "tutorial_text" + "asset": "gui_dialog_tooltip", + "message": "The CommandBar component \u0027gui_dialog_tooltip\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_space_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_space_icon" + "asset": "st_health", + "message": "The CommandBar component \u0027st_health\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_bracket_large\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_bracket_large" + "asset": "g_planet_name", + "message": "The CommandBar component \u0027g_planet_name\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027zoomed_back\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "zoomed_back" + "asset": "zoomed_header_text", + "message": "The CommandBar component \u0027zoomed_header_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027encyclopedia_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "encyclopedia_icon" + "asset": "g_conflict", + "message": "The CommandBar component \u0027g_conflict\u0027 is not connected to a shell component.", + "context": [] }, { - "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": "CMDBAR03", + "severity": "Information", + "asset": "g_credit_bar", + "message": "The CommandBar component \u0027g_credit_bar\u0027 is not supported by the game.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027b_beacon_t\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "b_beacon_t" + "asset": "zoomed_back", + "message": "The CommandBar component \u0027zoomed_back\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_bounty_hunter\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_bounty_hunter" + "asset": "zoomed_right_text", + "message": "The CommandBar component \u0027zoomed_right_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_credit_bar\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_credit_bar" + "asset": "st_power", + "message": "The CommandBar component \u0027st_power\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_hero\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_hero" + "asset": "st_bracket_large", + "message": "The CommandBar component \u0027st_bracket_large\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027bm_title_4010\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "bm_title_4010" + "asset": "g_ground_icon", + "message": "The CommandBar component \u0027g_ground_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_planet_fleet\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_planet_fleet" + "asset": "g_planet_land_forces", + "message": "The CommandBar component \u0027g_planet_land_forces\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_corruption_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_corruption_icon" + "asset": "tooltip_back", + "message": "The CommandBar component \u0027tooltip_back\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_smuggled\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_smuggled" + "asset": "b_beacon_t", + "message": "The CommandBar component \u0027b_beacon_t\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027help_back\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "help_back" + "asset": "balance_pip", + "message": "The CommandBar component \u0027balance_pip\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_bracket_small\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_bracket_small" + "asset": "g_corruption_icon", + "message": "The CommandBar component \u0027g_corruption_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027objective_header_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "objective_header_text" + "asset": "g_radar_blip", + "message": "The CommandBar component \u0027g_radar_blip\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_corruption_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_corruption_text" + "asset": "g_hero", + "message": "The CommandBar component \u0027g_hero\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_ground_level\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_ground_level" + "asset": "encyclopedia_cost_text", + "message": "The CommandBar component \u0027encyclopedia_cost_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027lt_weather_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "lt_weather_icon" + "asset": "remote_bomb_icon", + "message": "The CommandBar component \u0027remote_bomb_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027cs_ability_button\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "cs_ability_button" + "asset": "surface_mod_icon", + "message": "The CommandBar component \u0027surface_mod_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_radar_view\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_radar_view" + "asset": "cs_ability_button", + "message": "The CommandBar component \u0027cs_ability_button\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027objective_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "objective_icon" + "asset": "encyclopedia_text", + "message": "The CommandBar component \u0027encyclopedia_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027tooltip_back\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "tooltip_back" + "asset": "objective_icon", + "message": "The CommandBar component \u0027objective_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027zoomed_center_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "zoomed_center_text" + "asset": "st_garrison_icon", + "message": "The CommandBar component \u0027st_garrison_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_health_bar\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_health_bar" + "asset": "g_enemy_hero", + "message": "The CommandBar component \u0027g_enemy_hero\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027zoomed_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "zoomed_text" + "asset": "g_radar_view", + "message": "The CommandBar component \u0027g_radar_view\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027generic_collision\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "generic_collision" + "asset": "tactical_sell", + "message": "The CommandBar component \u0027tactical_sell\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027tooltip_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "tooltip_icon" + "asset": "cs_ability_text", + "message": "The CommandBar component \u0027cs_ability_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_radar_blip\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_radar_blip" + "asset": "g_smuggler", + "message": "The CommandBar component \u0027g_smuggler\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_hero_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_hero_icon" + "asset": "reinforcement_counter", + "message": "The CommandBar component \u0027reinforcement_counter\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_space_level\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_space_level" + "asset": "g_planet_fleet", + "message": "The CommandBar component \u0027g_planet_fleet\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027b_planet_left\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "b_planet_left" + "asset": "tooltip_left_text", + "message": "The CommandBar component \u0027tooltip_left_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027tooltip_icon_land\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "tooltip_icon_land" + "asset": "objective_header_text", + "message": "The CommandBar component \u0027objective_header_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_ground_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_ground_icon" + "asset": "lt_weather_icon", + "message": "The CommandBar component \u0027lt_weather_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027tooltip_price\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "tooltip_price" + "asset": "g_planet_value", + "message": "The CommandBar component \u0027g_planet_value\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027tooltip_left_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "tooltip_left_text" + "asset": "tooltip_icon", + "message": "The CommandBar component \u0027tooltip_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_health_large\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_health_large" + "asset": "g_ground_level", + "message": "The CommandBar component \u0027g_ground_level\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027tactical_sell\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "tactical_sell" + "asset": "tooltip_name", + "message": "The CommandBar component \u0027tooltip_name\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_special_ability\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_special_ability" + "asset": "encyclopedia_icon", + "message": "The CommandBar component \u0027encyclopedia_icon\u0027 is not connected to a shell component.", + "context": [] } ] } \ No newline at end of file diff --git a/src/ModVerify/DefaultGameVerifiersProvider.cs b/src/ModVerify/DefaultGameVerifiersProvider.cs index c9982b7..d7ce226 100644 --- a/src/ModVerify/DefaultGameVerifiersProvider.cs +++ b/src/ModVerify/DefaultGameVerifiersProvider.cs @@ -1,23 +1,28 @@ -using System; -using System.Collections.Generic; -using AET.ModVerify.Settings; +using AET.ModVerify.Settings; using AET.ModVerify.Verifiers; +using AET.ModVerify.Verifiers.CommandBar; +using AET.ModVerify.Verifiers.Engine; +using AET.ModVerify.Verifiers.GameObjects; using AET.ModVerify.Verifiers.GuiDialogs; +using AET.ModVerify.Verifiers.SfxEvents; using PG.StarWarsGame.Engine; +using System; +using System.Collections.Generic; namespace AET.ModVerify; public sealed class DefaultGameVerifiersProvider : IGameVerifiersProvider { public IEnumerable GetVerifiers( - IStarWarsGameEngine database, + IStarWarsGameEngine gameEngine, GameVerifySettings settings, IServiceProvider serviceProvider) { - yield return new ReferencedModelsVerifier(database, settings, serviceProvider); - yield return new DuplicateNameFinder(database, settings, serviceProvider); - yield return new AudioFilesVerifier(database, settings, serviceProvider); - yield return new GuiDialogsVerifier(database, settings, serviceProvider); - //yield return new CommandBarVerifier(database, settings, serviceProvider); + //yield break; + yield return new SfxEventVerifier(gameEngine, settings, serviceProvider); + yield return new HardcodedAssetsVerifier(gameEngine, settings, serviceProvider); + yield return new GuiDialogsVerifier(gameEngine, settings, serviceProvider); + yield return new GameObjectTypeVerifier(gameEngine, settings, serviceProvider); + yield return new CommandBarVerifier(gameEngine, settings, serviceProvider); } } \ No newline at end of file diff --git a/src/ModVerify/GameVerifyPipeline.cs b/src/ModVerify/GameVerifyPipeline.cs index 4ed351e..4a7e0fb 100644 --- a/src/ModVerify/GameVerifyPipeline.cs +++ b/src/ModVerify/GameVerifyPipeline.cs @@ -11,6 +11,8 @@ using AET.ModVerify.Settings; using AET.ModVerify.Utilities; using AET.ModVerify.Verifiers; +using AET.ModVerify.Verifiers.Engine; +using AET.ModVerify.Verifiers.Utilities; using AnakinRaW.CommonUtilities.SimplePipeline; using AnakinRaW.CommonUtilities.SimplePipeline.Runners; using Microsoft.Extensions.DependencyInjection; diff --git a/src/ModVerify/IGameVerifiersProvider.cs b/src/ModVerify/IGameVerifiersProvider.cs index 8cde9dc..c34c98d 100644 --- a/src/ModVerify/IGameVerifiersProvider.cs +++ b/src/ModVerify/IGameVerifiersProvider.cs @@ -9,7 +9,7 @@ namespace AET.ModVerify; public interface IGameVerifiersProvider { IEnumerable GetVerifiers( - IStarWarsGameEngine database, + IStarWarsGameEngine gameEngine, GameVerifySettings settings, IServiceProvider serviceProvider); } \ No newline at end of file diff --git a/src/ModVerify/ModVerify.csproj b/src/ModVerify/ModVerify.csproj index aecf306..96656cc 100644 --- a/src/ModVerify/ModVerify.csproj +++ b/src/ModVerify/ModVerify.csproj @@ -1,7 +1,7 @@  - netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1;net10.0 AlamoEngineTools.ModVerify AET.ModVerify AET.ModVerify @@ -22,13 +22,13 @@ - + - - - + + + diff --git a/src/ModVerify/ModVerify.csproj.DotSettings b/src/ModVerify/ModVerify.csproj.DotSettings index 3859842..d3b37e3 100644 --- a/src/ModVerify/ModVerify.csproj.DotSettings +++ b/src/ModVerify/ModVerify.csproj.DotSettings @@ -4,6 +4,9 @@ True True False - True + False + True + True True - True \ No newline at end of file + False + False \ No newline at end of file diff --git a/src/ModVerify/ModVerifyServiceExtensions.cs b/src/ModVerify/ModVerifyServiceExtensions.cs index 41fbeaa..04567e7 100644 --- a/src/ModVerify/ModVerifyServiceExtensions.cs +++ b/src/ModVerify/ModVerifyServiceExtensions.cs @@ -1,4 +1,4 @@ -using AET.ModVerify.Verifiers; +using AET.ModVerify.Verifiers.Caching; using Microsoft.Extensions.DependencyInjection; namespace AET.ModVerify; @@ -14,7 +14,7 @@ public IServiceCollection AddModVerify() public IServiceCollection RegisterVerifierCache() { - return serviceCollection.AddSingleton(sp => new AlreadyVerifiedCache(sp)); + return serviceCollection.AddSingleton(new AlreadyVerifiedCache()); } } } \ No newline at end of file diff --git a/src/ModVerify/Reporting/Baseline/VerificationBaseline.cs b/src/ModVerify/Reporting/Baseline/VerificationBaseline.cs index d734e6e..80f95b8 100644 --- a/src/ModVerify/Reporting/Baseline/VerificationBaseline.cs +++ b/src/ModVerify/Reporting/Baseline/VerificationBaseline.cs @@ -13,7 +13,7 @@ namespace AET.ModVerify.Reporting.Baseline; public sealed class VerificationBaseline : IReadOnlyCollection { - public static readonly Version LatestVersion = new(2, 1); + public static readonly Version LatestVersion = new(2, 2); public static readonly string LatestVersionString = LatestVersion.ToString(2); public static readonly VerificationBaseline Empty = new(VerificationSeverity.Information, [], null); @@ -61,7 +61,6 @@ public void ToJson(Stream stream) { JsonSerializer.Serialize(stream, new JsonVerificationBaseline(this), ModVerifyJsonSettings.JsonSettings); } - public Task ToJsonAsync(Stream stream) { return JsonSerializer.SerializeAsync(stream, new JsonVerificationBaseline(this), ModVerifyJsonSettings.JsonSettings); diff --git a/src/ModVerify/Reporting/Engine/EngineErrorReporterBase.cs b/src/ModVerify/Reporting/Engine/EngineErrorReporterBase.cs index 7200f23..f8c7cd2 100644 --- a/src/ModVerify/Reporting/Engine/EngineErrorReporterBase.cs +++ b/src/ModVerify/Reporting/Engine/EngineErrorReporterBase.cs @@ -6,25 +6,33 @@ namespace AET.ModVerify.Reporting.Engine; -internal abstract class EngineErrorReporterBase(IGameRepository gameRepository, IServiceProvider serviceProvider) - : IGameVerifierInfo +internal abstract class EngineErrorReporterBase : IGameVerifierInfo { - protected readonly IGameRepository GameRepository = gameRepository ?? throw new ArgumentNullException(nameof(gameRepository)); - protected readonly IServiceProvider ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + protected readonly IGameRepository GameRepository; + protected readonly IServiceProvider ServiceProvider; public IGameVerifierInfo? Parent => null; + public IReadOnlyList VerifierChain { get; } + public string Name => GetType().FullName; public abstract string FriendlyName { get; } + protected EngineErrorReporterBase(IGameRepository gameRepository, IServiceProvider serviceProvider) + { + GameRepository = gameRepository ?? throw new ArgumentNullException(nameof(gameRepository)); + ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + VerifierChain = [this]; + } + public IEnumerable GetErrors(IEnumerable errors) { foreach (var error in errors) { var errorData = CreateError(error); yield return new VerificationError( - errorData.Identifier, errorData.Message, [this], errorData.Context, errorData.Asset, errorData.Severity); + errorData.Identifier, errorData.Message, this, errorData.Context, errorData.Asset, errorData.Severity); } } diff --git a/src/ModVerify/Reporting/Engine/GameAssertErrorReporter.cs b/src/ModVerify/Reporting/Engine/GameAssertErrorReporter.cs index f75334c..0c8c633 100644 --- a/src/ModVerify/Reporting/Engine/GameAssertErrorReporter.cs +++ b/src/ModVerify/Reporting/Engine/GameAssertErrorReporter.cs @@ -17,7 +17,12 @@ protected override ErrorData CreateError(EngineAssert assert) var context = new List(); context.AddRange(assert.Context); context.Add($"location='{GetLocation(assert)}'"); - return new ErrorData(GetIdFromError(assert.Kind), assert.Message, context, assert.Value, VerificationSeverity.Warning); + return new ErrorData( + GetIdFromError(assert.Kind), + assert.Message, + context, + assert.Value, + VerificationSeverity.Warning); } private static string GetLocation(EngineAssert assert) @@ -41,6 +46,7 @@ private static string GetIdFromError(EngineAssertKind assertKind) EngineAssertKind.ValueOutOfRange => VerifierErrorCodes.AssertValueOutOfRange, EngineAssertKind.InvalidValue => VerifierErrorCodes.AssertValueInvalid, EngineAssertKind.FileNotFound => VerifierErrorCodes.FileNotFound, + EngineAssertKind.DuplicateEntry => VerifierErrorCodes.Duplicate, _ => throw new ArgumentOutOfRangeException(nameof(assertKind), assertKind, null) }; } diff --git a/src/ModVerify/Reporting/Engine/GameEngineErrorCollection.cs b/src/ModVerify/Reporting/Engine/GameEngineErrorCollection.cs index ba0abf5..8cb67c0 100644 --- a/src/ModVerify/Reporting/Engine/GameEngineErrorCollection.cs +++ b/src/ModVerify/Reporting/Engine/GameEngineErrorCollection.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using PG.StarWarsGame.Engine.ErrorReporting; +using PG.StarWarsGame.Files.XML.ErrorHandling; namespace AET.ModVerify.Reporting.Engine; @@ -17,7 +18,7 @@ public sealed class GameEngineErrorCollection : IGameEngineErrorCollection, IGam public IEnumerable Asserts => _asserts.ToList(); - void IGameEngineErrorReporter.Report(XmlError error) + void IXmlParserErrorReporter.Report(XmlError error) { _xmlErrors.Add(error); } diff --git a/src/ModVerify/Reporting/Engine/IGameEngineErrorCollection.cs b/src/ModVerify/Reporting/Engine/IGameEngineErrorCollection.cs index adf734d..286d047 100644 --- a/src/ModVerify/Reporting/Engine/IGameEngineErrorCollection.cs +++ b/src/ModVerify/Reporting/Engine/IGameEngineErrorCollection.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using PG.StarWarsGame.Engine.ErrorReporting; +using PG.StarWarsGame.Files.XML.ErrorHandling; namespace AET.ModVerify.Reporting.Engine; diff --git a/src/ModVerify/Reporting/Engine/XmlParseErrorReporter.cs b/src/ModVerify/Reporting/Engine/XmlParseErrorReporter.cs index 2629f26..9548ad4 100644 --- a/src/ModVerify/Reporting/Engine/XmlParseErrorReporter.cs +++ b/src/ModVerify/Reporting/Engine/XmlParseErrorReporter.cs @@ -4,7 +4,6 @@ using AET.ModVerify.Utilities; using AET.ModVerify.Verifiers; using Microsoft.Extensions.DependencyInjection; -using PG.StarWarsGame.Engine.ErrorReporting; using PG.StarWarsGame.Engine.IO; using PG.StarWarsGame.Files.XML.ErrorHandling; @@ -29,7 +28,8 @@ protected override ErrorData CreateError(XmlError error) var context = new List { - strippedFileName + $"Parser: {error.Parser.Name}", + $"File: {strippedFileName}" }; var xmlElement = error.Element; @@ -75,6 +75,8 @@ private static VerificationSeverity GetSeverityFromError(XmlParseErrorKind xmlEr XmlParseErrorKind.DataBeforeHeader => VerificationSeverity.Information, XmlParseErrorKind.MissingNode => VerificationSeverity.Critical, XmlParseErrorKind.UnknownNode => VerificationSeverity.Information, + XmlParseErrorKind.TagHasElements => VerificationSeverity.Warning, + XmlParseErrorKind.UnexceptedElementName => VerificationSeverity.Information, _ => VerificationSeverity.Warning }; } @@ -94,6 +96,8 @@ private static string GetIdFromError(XmlParseErrorKind xmlErrorErrorKind) XmlParseErrorKind.DataBeforeHeader => VerifierErrorCodes.XmlDataBeforeHeader, XmlParseErrorKind.MissingNode => VerifierErrorCodes.XmlMissingNode, XmlParseErrorKind.UnknownNode => VerifierErrorCodes.XmlUnsupportedTag, + XmlParseErrorKind.TagHasElements => VerifierErrorCodes.XmlElementsInTag, + XmlParseErrorKind.UnexceptedElementName => VerifierErrorCodes.XmlUnexceptedElementName, _ => throw new ArgumentOutOfRangeException(nameof(xmlErrorErrorKind), xmlErrorErrorKind, null) }; } diff --git a/src/ModVerify/Reporting/Json/JsonAggregatedVerificationError.cs b/src/ModVerify/Reporting/Json/JsonAggregatedVerificationError.cs index 5a9cbb3..a60b3de 100644 --- a/src/ModVerify/Reporting/Json/JsonAggregatedVerificationError.cs +++ b/src/ModVerify/Reporting/Json/JsonAggregatedVerificationError.cs @@ -16,14 +16,16 @@ public JsonAggregatedVerificationError( string message, VerificationSeverity severity, IEnumerable>? contexts, - string? asset) : base(id, verifierChain, message, severity, asset) + string? asset) : base(id, severity, asset, message, verifierChain) { Contexts = contexts ?? []; } public JsonAggregatedVerificationError( VerificationError error, - IEnumerable> contexts) : base(error) + IEnumerable> contexts, + bool verbose = false) + : base(error, verbose) { Contexts = contexts; } diff --git a/src/ModVerify/Reporting/Json/JsonVerificationError.cs b/src/ModVerify/Reporting/Json/JsonVerificationError.cs index 0e23283..8094058 100644 --- a/src/ModVerify/Reporting/Json/JsonVerificationError.cs +++ b/src/ModVerify/Reporting/Json/JsonVerificationError.cs @@ -8,22 +8,23 @@ internal class JsonVerificationError : JsonVerificationErrorBase { [JsonPropertyName("context")] [JsonPropertyOrder(99)] - public IEnumerable? ContextEntries { get; } + public IEnumerable ContextEntries { get; } [JsonConstructor] public JsonVerificationError( - string id, - IReadOnlyList? verifierChain, + string id, + VerificationSeverity severity, + string? asset, string message, - VerificationSeverity severity, - IEnumerable? contextEntries, - string? asset) : base(id, verifierChain, message, severity, asset) + IReadOnlyList? verifierChain, + IEnumerable contextEntries) : base(id, severity, asset, message, verifierChain) { ContextEntries = contextEntries; } - public JsonVerificationError(VerificationError error) : base(error) + public JsonVerificationError(VerificationError error, bool verbose = false) + : base(error, verbose) { - ContextEntries = error.ContextEntries.Any() ? error.ContextEntries : null; + ContextEntries = error.ContextEntries.Any() ? error.ContextEntries : []; } } \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonVerificationErrorBase.cs b/src/ModVerify/Reporting/Json/JsonVerificationErrorBase.cs index 06af012..b7aa298 100644 --- a/src/ModVerify/Reporting/Json/JsonVerificationErrorBase.cs +++ b/src/ModVerify/Reporting/Json/JsonVerificationErrorBase.cs @@ -11,12 +11,6 @@ internal abstract class JsonVerificationErrorBase [JsonPropertyName("id")] public string Id { get; } - [JsonPropertyName("verifiers")] - public IReadOnlyList VerifierChain { get; } - - [JsonPropertyName("message")] - public string Message { get; } - [JsonPropertyName("severity")] [JsonConverter(typeof(JsonStringEnumConverter))] public VerificationSeverity Severity { get; } @@ -24,24 +18,29 @@ internal abstract class JsonVerificationErrorBase [JsonPropertyName("asset")] public string Asset { get; } - protected JsonVerificationErrorBase( - string id, - IReadOnlyList? verifierChain, - string message, + [JsonPropertyName("message")] + public string Message { get; } + + [JsonPropertyName("verifiers")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IReadOnlyList? VerifierChain { get; } + + protected JsonVerificationErrorBase(string id, VerificationSeverity severity, - string? asset) + string? asset, + string message, + IReadOnlyList? verifierChain) { Id = id; - VerifierChain = verifierChain ?? []; + VerifierChain = verifierChain; Message = message; Severity = severity; Asset = asset ?? string.Empty; } - - protected JsonVerificationErrorBase(VerificationError error) + protected JsonVerificationErrorBase(VerificationError error, bool verbose = false) { Id = error.Id; - VerifierChain = error.VerifierChain.Select(x => x.Name).ToList(); + VerifierChain = verbose ? error.VerifierChain.Select(x => x.Name).ToList() : null; Message = error.Message; Severity = error.Severity; Asset = error.Asset; diff --git a/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs b/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs index 1feb655..ac11bb6 100644 --- a/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs +++ b/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs @@ -30,20 +30,24 @@ private JsonVerificationReport CreateJsonReport(VerificationResult result) if (Settings.AggregateResults) { errors = result.Errors + .OrderByDescending(x => x.Severity) + .ThenBy(x => x.Id) .GroupBy(x => new GroupKey(x.Asset, x.Id, x.VerifierChain)) .Select, JsonVerificationErrorBase>(g => { var first = g.First(); var contexts = g.Select(x => x.ContextEntries).ToList(); - if (contexts.Count == 1) - return new JsonVerificationError(first); - return new JsonAggregatedVerificationError(first, contexts); + return new JsonVerificationError(first, Settings.Verbose); + return new JsonAggregatedVerificationError(first, contexts, Settings.Verbose); }); } else { - errors = result.Errors.Select(x => new JsonVerificationError(x)); + errors = result.Errors + .OrderByDescending(x => x.Severity) + .ThenBy(x => x.Id) + .Select(x => new JsonVerificationError(x, Settings.Verbose)); } return new JsonVerificationReport diff --git a/src/ModVerify/Reporting/Reporters/JSON/JsonReporterSettings.cs b/src/ModVerify/Reporting/Reporters/JSON/JsonReporterSettings.cs index 7ccbba9..a2503ed 100644 --- a/src/ModVerify/Reporting/Reporters/JSON/JsonReporterSettings.cs +++ b/src/ModVerify/Reporting/Reporters/JSON/JsonReporterSettings.cs @@ -1,5 +1,4 @@ namespace AET.ModVerify.Reporting.Reporters; - public record JsonReporterSettings : FileBasedReporterSettings { public bool AggregateResults { get; init; } diff --git a/src/ModVerify/Reporting/Reporters/ReporterSettings.cs b/src/ModVerify/Reporting/Reporters/ReporterSettings.cs index 73332eb..6cd56d3 100644 --- a/src/ModVerify/Reporting/Reporters/ReporterSettings.cs +++ b/src/ModVerify/Reporting/Reporters/ReporterSettings.cs @@ -3,4 +3,5 @@ public record ReporterSettings { public VerificationSeverity MinimumReportSeverity { get; init; } = VerificationSeverity.Information; + public bool Verbose { get; init; } } \ No newline at end of file diff --git a/src/ModVerify/Reporting/RestoredVerifierInfo.cs b/src/ModVerify/Reporting/RestoredVerifierInfo.cs index 0c29488..0f0a87b 100644 --- a/src/ModVerify/Reporting/RestoredVerifierInfo.cs +++ b/src/ModVerify/Reporting/RestoredVerifierInfo.cs @@ -1,10 +1,15 @@ using AET.ModVerify.Verifiers; +using AET.ModVerify.Verifiers.Utilities; +using System.Collections.Generic; namespace AET.ModVerify.Reporting; internal sealed class RestoredVerifierInfo : IGameVerifierInfo { public IGameVerifierInfo? Parent { get; init; } + + public IReadOnlyList VerifierChain => field ??= this.GetVerifierChain(); + public required string Name { get; init; } public string FriendlyName => Name; } \ No newline at end of file diff --git a/src/ModVerify/Reporting/VerificationError.cs b/src/ModVerify/Reporting/VerificationError.cs index d55f71b..2e95327 100644 --- a/src/ModVerify/Reporting/VerificationError.cs +++ b/src/ModVerify/Reporting/VerificationError.cs @@ -28,11 +28,13 @@ public sealed class VerificationError : IEquatable public VerificationError( string id, string message, - IReadOnlyList verifiers, + IGameVerifierInfo verifier, IEnumerable contextEntries, string asset, VerificationSeverity severity) { + if (verifier == null) + throw new ArgumentNullException(nameof(verifier)); if (contextEntries == null) throw new ArgumentNullException(nameof(contextEntries)); if (asset is null) @@ -41,7 +43,7 @@ public VerificationError( Id = id; Message = message ?? throw new ArgumentNullException(nameof(message)); - VerifierChain = [.. verifiers]; + VerifierChain = verifier.VerifierChain; Severity = severity; ContextEntries = _contextEntries = [.. contextEntries]; Asset = asset; @@ -58,18 +60,18 @@ internal VerificationError(JsonVerificationError error) } public static VerificationError Create( - IReadOnlyList verifiers, + IGameVerifierInfo verifier, string id, string message, VerificationSeverity severity, IEnumerable context, string asset) { - return new VerificationError(id, message, verifiers, context, asset, severity); + return new VerificationError(id, message, verifier, context, asset, severity); } public static VerificationError Create( - IReadOnlyList verifiers, + IGameVerifierInfo verifier, string id, string message, VerificationSeverity severity, @@ -78,7 +80,7 @@ public static VerificationError Create( return new VerificationError( id, message, - verifiers, + verifier, [], asset, severity); @@ -120,8 +122,11 @@ public override string ToString() $"{Id}: Message={Message}; Asset='{Asset}'; Context=[{string.Join(",", ContextEntries)}];"; } - private static IReadOnlyList RestoreVerifierChain(IReadOnlyList errorVerifierChain) + private static IReadOnlyList RestoreVerifierChain(IReadOnlyList? errorVerifierChain) { + if (errorVerifierChain is null) + return []; + var verifierChain = new List(); IGameVerifierInfo? previousVerifier = null; diff --git a/src/ModVerify/Reporting/VerifierChainEqualityComparer.cs b/src/ModVerify/Reporting/VerifierChainEqualityComparer.cs index abb01ef..27897cb 100644 --- a/src/ModVerify/Reporting/VerifierChainEqualityComparer.cs +++ b/src/ModVerify/Reporting/VerifierChainEqualityComparer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using AET.ModVerify.Verifiers; +using AET.ModVerify.Verifiers.Utilities; namespace AET.ModVerify.Reporting; diff --git a/src/ModVerify/Resources/Schemas/2.1/baseline.json b/src/ModVerify/Resources/Schemas/2.2/baseline.json similarity index 92% rename from src/ModVerify/Resources/Schemas/2.1/baseline.json rename to src/ModVerify/Resources/Schemas/2.2/baseline.json index da37c4d..d893b4d 100644 --- a/src/ModVerify/Resources/Schemas/2.1/baseline.json +++ b/src/ModVerify/Resources/Schemas/2.2/baseline.json @@ -1,5 +1,5 @@ { - "$id": "https://AlamoEngine-Tools.github.io/schemas/mod-verify/2.1/baseline", + "$id": "https://AlamoEngine-Tools.github.io/schemas/mod-verify/2.2/baseline", "$schema": "https://json-schema.org/draft/2020-12/schema", "description": "Represents a baseline for AET ModVerify", "type": "object", @@ -73,12 +73,6 @@ "severity": { "$ref": "#/$defs/severity" }, - "verifiers": { - "type": "array", - "items": { - "type": "string" - } - }, "context": { "type": "array", "items": { @@ -91,7 +85,6 @@ "message", "asset", "severity", - "verifiers", "context" ], "additionalProperties": false @@ -99,7 +92,7 @@ }, "properties": { "version": { - "const": "2.1" + "const": "2.2" }, "minSeverity": { "$ref": "#/$defs/severity" diff --git a/src/ModVerify/Verifiers/AlreadyVerifiedCache.cs b/src/ModVerify/Verifiers/AlreadyVerifiedCache.cs deleted file mode 100644 index 530ca4e..0000000 --- a/src/ModVerify/Verifiers/AlreadyVerifiedCache.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Concurrent; -using Microsoft.Extensions.DependencyInjection; -using PG.Commons.Hashing; -using PG.StarWarsGame.Engine; - -namespace AET.ModVerify.Verifiers; - -internal sealed class AlreadyVerifiedCache(IServiceProvider serviceProvider) : IAlreadyVerifiedCache -{ - private readonly ICrc32HashingService _crc32Hashing = serviceProvider.GetRequiredService(); - private readonly ConcurrentDictionary _cachedChecksums = new(); - - public bool TryAddEntry(string entry) - { - return TryAddEntry(entry.AsSpan()); - } - - public bool TryAddEntry(ReadOnlySpan entry) - { - return TryAddEntry(_crc32Hashing.GetCrc32Upper(entry, PGConstants.DefaultPGEncoding)); - } - - public bool TryAddEntry(Crc32 checksum) - { - return _cachedChecksums.TryAdd(checksum, 0); - } -} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/AudioFilesVerifier.cs b/src/ModVerify/Verifiers/AudioFilesVerifier.cs deleted file mode 100644 index ddd9252..0000000 --- a/src/ModVerify/Verifiers/AudioFilesVerifier.cs +++ /dev/null @@ -1,249 +0,0 @@ -using System; -using System.Buffers; -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions; -using System.Linq; -using System.Text; -using System.Threading; -using AET.ModVerify.Reporting; -using AET.ModVerify.Settings; -using AnakinRaW.CommonUtilities.FileSystem.Normalization; -using Microsoft.Extensions.DependencyInjection; -using PG.Commons.Hashing; -using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Engine.Audio.Sfx; -using PG.StarWarsGame.Engine.Localization; -#if NETSTANDARD2_0 -using AnakinRaW.CommonUtilities.FileSystem; -#endif - -namespace AET.ModVerify.Verifiers; - -public class AudioFilesVerifier : GameVerifier -{ - private static readonly PathNormalizeOptions SampleNormalizerOptions = new() - { - UnifyCase = UnifyCasingKind.UpperCaseForce, - UnifySeparatorKind = DirectorySeparatorKind.Windows, - UnifyDirectorySeparators = true - }; - - private readonly ICrc32HashingService _hashingService; - private readonly IFileSystem _fileSystem; - private readonly IGameLanguageManager _languageManager; - - public AudioFilesVerifier(IStarWarsGameEngine gameEngine, GameVerifySettings settings, IServiceProvider serviceProvider) - : base(null, gameEngine, settings, serviceProvider) - { - _hashingService = serviceProvider.GetRequiredService(); - _fileSystem = serviceProvider.GetRequiredService(); - _languageManager = serviceProvider.GetRequiredService() - .GetLanguageManager(Repository.EngineType); - } - - public override string FriendlyName => "Audio Files"; - - public override void Verify(CancellationToken token) - { - var visitedSamples = new HashSet(); - var languagesToVerify = GetLanguagesToVerify().ToList(); - - - var numSamples = GameEngine.SfxGameManager.Entries.Sum(x => x.AllSamples.Count()); - double counter = 0; - - foreach (var sfxEvent in GameEngine.SfxGameManager.Entries) - { - foreach (var codedSample in sfxEvent.AllSamples) - { - OnProgress(++counter / numSamples, $"Audio File - '{codedSample}'"); - VerifySample(codedSample.AsSpan(), sfxEvent, languagesToVerify, visitedSamples); - } - } - } - - private void VerifySample(ReadOnlySpan sample, SfxEvent sfxEvent, IEnumerable languagesToVerify, HashSet visitedSamples) - { - char[]? pooledBuffer = null; - - var buffer = sample.Length < PGConstants.MaxMegEntryPathLength - ? stackalloc char[PGConstants.MaxMegEntryPathLength] - : pooledBuffer = ArrayPool.Shared.Rent(sample.Length); - - try - { - var length = PathNormalizer.Normalize(sample, buffer, SampleNormalizerOptions); - var sampleNameBuffer = buffer.Slice(0, length); - - var crc = _hashingService.GetCrc32(sampleNameBuffer, Encoding.ASCII); - if (!visitedSamples.Add(crc)) - return; - - if (sfxEvent.IsLocalized) - { - foreach (var language in languagesToVerify) - { - VerifySampleLocalized(sfxEvent, sampleNameBuffer, language, out var localized); - if (!localized) - return; - } - } - else - { - VerifySample(sampleNameBuffer, sfxEvent); - } - } - finally - { - if (pooledBuffer is not null) - ArrayPool.Shared.Return(pooledBuffer); - } - - } - - private void VerifySampleLocalized(SfxEvent sfxEvent, ReadOnlySpan sample, LanguageType language, out bool localized) - { - char[]? pooledBuffer = null; - - var buffer = sample.Length < PGConstants.MaxMegEntryPathLength - ? stackalloc char[PGConstants.MaxMegEntryPathLength] - : pooledBuffer = ArrayPool.Shared.Rent(sample.Length); - try - { - var l = _languageManager.LocalizeFileName(sample, language, buffer, out localized); - var localizedName = buffer.Slice(0, l); - VerifySample(localizedName, sfxEvent); - } - finally - { - if (pooledBuffer is not null) - ArrayPool.Shared.Return(pooledBuffer); - } - } - - private void VerifySample(ReadOnlySpan sample, SfxEvent sfxEvent) - { - using var sampleStream = Repository.TryOpenFile(sample); - if (sampleStream is null) - { - var sampleString = sample.ToString(); - AddError(VerificationError.Create( - VerifierChain, - VerifierErrorCodes.FileNotFound, - $"Audio file '{sampleString}' could not be found.", - VerificationSeverity.Error, - [sfxEvent.Name], - sampleString)); - return; - } - using var binaryReader = new BinaryReader(sampleStream); - - // Skip Header + "fmt " - binaryReader.BaseStream.Seek(16, SeekOrigin.Begin); - - var fmtSize = binaryReader.ReadInt32(); - var format = (WaveFormats)binaryReader.ReadInt16(); - var channels = binaryReader.ReadInt16(); - - var sampleRate = binaryReader.ReadInt32(); - var bytesPerSecond = binaryReader.ReadInt32(); - - var frameSize = binaryReader.ReadInt16(); - var bitPerSecondPerChannel = binaryReader.ReadInt16(); - - if (format != WaveFormats.PCM) - { - var sampleString = sample.ToString(); - AddError(VerificationError.Create( - VerifierChain, - VerifierErrorCodes.SampleNotPCM, - $"Audio file '{sampleString}' has an invalid format '{format}'. Supported is {WaveFormats.PCM}", - VerificationSeverity.Error, - [sfxEvent.Name], - sampleString)); - } - - if (channels > 1 && !IsAmbient2D(sfxEvent)) - { - var sampleString = sample.ToString(); - AddError(VerificationError.Create( - VerifierChain, - VerifierErrorCodes.SampleNotMono, - $"Audio file '{sampleString}' is not mono audio.", - VerificationSeverity.Information, - sampleString)); - } - - if (sampleRate > 48_000) - { - var sampleString = sample.ToString(); - AddError(VerificationError.Create( - VerifierChain, - VerifierErrorCodes. InvalidSampleRate, - $"Audio file '{sampleString}' has a too high sample rate of {sampleRate}. Maximum is 48.000Hz.", - VerificationSeverity.Error, - [sfxEvent.Name], - sampleString)); - } - - if (bitPerSecondPerChannel > 16) - { - var sampleString = sample.ToString(); - AddError(VerificationError.Create( - VerifierChain, - VerifierErrorCodes.InvalidBitsPerSeconds, - $"Audio file '{sampleString}' has an invalid bit size of {bitPerSecondPerChannel}. Supported are 16bit.", - VerificationSeverity.Error, - [sfxEvent.Name], - sampleString)); - } - } - - // Some heuristics whether a SFXEvent is most likely to be an ambient sound. - private bool IsAmbient2D(SfxEvent sfxEvent) - { - if (!sfxEvent.Is2D) - return false; - - if (sfxEvent.IsPreset) - return false; - - // If the event is located in SFXEventsAmbient.xml we simply assume it's an ambient sound. - var fileName = _fileSystem.Path.GetFileName(sfxEvent.Location.XmlFile.AsSpan()); - if (fileName.Equals("SFXEventsAmbient.xml".AsSpan(), StringComparison.OrdinalIgnoreCase)) - return true; - - if (string.IsNullOrEmpty(sfxEvent.UsePresetName)) - return false; - - if (sfxEvent.UsePresetName!.StartsWith("Preset_AMB_2D")) - return true; - - return true; - } - - private IEnumerable GetLanguagesToVerify() - { - switch (Settings.LocalizationOption) - { - case VerifyLocalizationOption.English: - return new List { LanguageType.English }; - case VerifyLocalizationOption.CurrentSystem: - return new List { _languageManager.GetLanguagesFromUser() }; - case VerifyLocalizationOption.AllInstalled: - return GameEngine.InstalledLanguages; - case VerifyLocalizationOption.All: - return _languageManager.SupportedLanguages; - default: - throw new NotSupportedException($"{Settings.LocalizationOption} is not supported"); - } - } - - private enum WaveFormats - { - PCM = 1, - MSADPCM = 2, - IEEE_Float = 3, - } -} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/Caching/AlreadyVerifiedCache.cs b/src/ModVerify/Verifiers/Caching/AlreadyVerifiedCache.cs new file mode 100644 index 0000000..c6dda7c --- /dev/null +++ b/src/ModVerify/Verifiers/Caching/AlreadyVerifiedCache.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace AET.ModVerify.Verifiers.Caching; + +internal sealed class AlreadyVerifiedCache : IAlreadyVerifiedCache +{ + private readonly Dictionary _cachedEntries = new(); + + public bool TryAddEntry(string entry, bool assetExists) + { + var upper = entry.ToUpperInvariant(); + +#if NETSTANDARD2_1 || NET + return _cachedEntries.TryAdd(upper, assetExists); +#else + var alreadyVerified = _cachedEntries.ContainsKey(upper); + if (alreadyVerified) + return false; + + _cachedEntries[upper] = assetExists; + return true; +#endif + } + + public VerifiedCacheEntry GetEntry(string entry) + { + var upper = entry.ToUpperInvariant(); + var alreadyVerified = _cachedEntries.TryGetValue(upper, out var exists); + return alreadyVerified ? new VerifiedCacheEntry(true, exists) : default; + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/Caching/IAlreadyVerifiedCache.cs b/src/ModVerify/Verifiers/Caching/IAlreadyVerifiedCache.cs new file mode 100644 index 0000000..68f39b4 --- /dev/null +++ b/src/ModVerify/Verifiers/Caching/IAlreadyVerifiedCache.cs @@ -0,0 +1,8 @@ +namespace AET.ModVerify.Verifiers.Caching; + +public interface IAlreadyVerifiedCache +{ + bool TryAddEntry(string entry, bool assetExists); + + VerifiedCacheEntry GetEntry(string entry); +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/Caching/VerifiedCacheEntry.cs b/src/ModVerify/Verifiers/Caching/VerifiedCacheEntry.cs new file mode 100644 index 0000000..252a7c0 --- /dev/null +++ b/src/ModVerify/Verifiers/Caching/VerifiedCacheEntry.cs @@ -0,0 +1,14 @@ +namespace AET.ModVerify.Verifiers.Caching; + +public readonly struct VerifiedCacheEntry +{ + public bool AlreadyVerified { get; } + + public bool AssetExists { get; } + + public VerifiedCacheEntry(bool alreadyVerified, bool assetExists) + { + AlreadyVerified = alreadyVerified; + AssetExists = assetExists; + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.Base.cs b/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.Base.cs deleted file mode 100644 index 3373f8e..0000000 --- a/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.Base.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using AET.ModVerify.Reporting; -using AET.ModVerify.Settings; -using AnakinRaW.CommonUtilities.Collections; -using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Engine.CommandBar; -using PG.StarWarsGame.Engine.CommandBar.Components; - -namespace AET.ModVerify.Verifiers; - -public partial class CommandBarVerifier(IStarWarsGameEngine gameEngine, GameVerifySettings settings, IServiceProvider serviceProvider) - : GameVerifier(null, gameEngine, settings, serviceProvider) -{ - public const string CommandBarNoShellsGroup = "CMDBAR00"; - public const string CommandBarManyShellsGroup = "CMDBAR01"; - public const string CommandBarNoShellsComponentInShellGroup = "CMDBAR02"; - public const string CommandBarDuplicateComponent = "CMDBAR03"; - public const string CommandBarUnsupportedComponent = "CMDBAR04"; - public const string CommandBarShellNoModel = "CMDBAR05"; - - public override string FriendlyName => "CommandBar"; - - public override void Verify(CancellationToken token) - { - VerifyCommandBarShellsGroups(); - VerifyCommandBarComponents(); - } - - private void VerifySingleComponent(CommandBarBaseComponent component) - { - VerifyCommandBarModel(component); - VerifyComponentBone(component); - } -} - -partial class CommandBarVerifier -{ - private void VerifyCommandBarModel(CommandBarBaseComponent component) - { - if (component is not CommandBarShellComponent shellComponent) - return; - - if (shellComponent.ModelPath is null) - { - AddError(VerificationError.Create(VerifierChain, - CommandBarShellNoModel, $"The CommandBarShellComponent '{component.Name}' has no model specified.", - VerificationSeverity.Error, shellComponent.Name)); - return; - } - - var model = GameEngine.PGRender.LoadModelAndAnimations(shellComponent.ModelPath.AsSpan(), null); - if (model is null) - { - AddError(VerificationError.Create(VerifierChain, - CommandBarShellNoModel, $"Could not find model '{shellComponent.ModelPath}' for CommandBarShellComponent '{component.Name}'.", - VerificationSeverity.Error, [shellComponent.Name], shellComponent.ModelPath)); - return; - } - } - - private void VerifyComponentBone(CommandBarBaseComponent component) - { - if (component is CommandBarShellComponent) - return; - - if (component.Bone == -1) - { - AddError(VerificationError.Create(VerifierChain, - CommandBarShellNoModel, $"The CommandBar component '{component.Name}' is not connected to a shell component.", - VerificationSeverity.Warning, component.Name)); - } - } -} - -partial class CommandBarVerifier -{ - private void VerifyCommandBarComponents() - { - var occupiedComponentIds = SupportedCommandBarComponentData.GetComponentIdsForEngine(Repository.EngineType).Keys - .ToDictionary(value => value, _ => false); - - foreach (var component in GameEngine.CommandBar.Components) - { - if (!occupiedComponentIds.TryGetValue(component.Id, out var alreadyOccupied)) - { - AddError(VerificationError.Create( - VerifierChain, - CommandBarUnsupportedComponent, - $"The CommandBar component '{component.Name}' is not supported by the game.", - VerificationSeverity.Information, - component.Name)); - } - else - { - occupiedComponentIds[component.Id] = true; - } - - if (alreadyOccupied) - { - AddError(VerificationError.Create(VerifierChain, - CommandBarDuplicateComponent, - $"The CommandBar component '{component.Name}' with ID '{component.Id}' already exists.", - VerificationSeverity.Warning, - component.Name)); - } - - VerifySingleComponent(component); - } - } -} - -partial class CommandBarVerifier -{ - private void VerifyCommandBarShellsGroups() - { - var shellGroups = new FrugalList(); - foreach (var groupPair in GameEngine.CommandBar.Groups) - { - if (groupPair.Key == CommandBarConstants.ShellGroupName) - { - shellGroups.Add(groupPair.Key); - VerifyShellGroup(groupPair.Value); - } - else if (groupPair.Key.Equals(CommandBarConstants.ShellGroupName, StringComparison.OrdinalIgnoreCase)) - { - shellGroups.Add(groupPair.Key); - } - } - - if (shellGroups.Count == 0) - AddError(VerificationError.Create(VerifierChain, - CommandBarNoShellsGroup, - $"No CommandBarGroup '{CommandBarConstants.ShellGroupName}' found.", - VerificationSeverity.Error, - "GameCommandBar")); - - if (shellGroups.Count > 1) - AddError(VerificationError.Create(VerifierChain, - CommandBarManyShellsGroup, - $"Found more than one Shells CommandBarGroup. Mind that group names are case-sensitive. Correct name is '{CommandBarConstants.ShellGroupName}'", - VerificationSeverity.Warning, - shellGroups, "GameCommandBar")); - } - - private void VerifyShellGroup(CommandBarComponentGroup shellGroup) - { - foreach (var component in shellGroup.Components) - { - var shellComponent = component as CommandBarShellComponent; - if (shellComponent?.Type is not CommandBarComponentType.Shell) - { - AddError(VerificationError.Create(VerifierChain, - CommandBarNoShellsComponentInShellGroup, - $"The CommandBar component '{component.Name}' is not a shell component, but part of the '{CommandBarConstants.ShellGroupName}' group.", - VerificationSeverity.Warning, component.Name)); - } - } - } -} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.Components.cs b/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.Components.cs new file mode 100644 index 0000000..3b43093 --- /dev/null +++ b/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.Components.cs @@ -0,0 +1,51 @@ +using AET.ModVerify.Reporting; +using PG.StarWarsGame.Engine.CommandBar; +using System.Linq; +using System.Threading; + +namespace AET.ModVerify.Verifiers.CommandBar; + +partial class CommandBarVerifier +{ + private void VerifyCommandBarComponents(CancellationToken token, double startProgress) + { + var occupiedComponentIds = SupportedCommandBarComponentData + .GetComponentIdsForEngine(Repository.EngineType).Keys + .ToDictionary(value => value, _ => false); + + var counter = 0; + var numEntities = GameEngine.CommandBar.Components.Count; + var num = 1 - startProgress; + + foreach (var component in GameEngine.CommandBar.Components) + { + var progress = num + (++counter / (double)numEntities) * startProgress; + OnProgress(progress, $"CommandBarComponent - '{component.Name}'"); + + if (!occupiedComponentIds.TryGetValue(component.Id, out var alreadyOccupied)) + { + AddError(VerificationError.Create( + this, + CommandBarUnsupportedComponent, + $"The CommandBar component '{component.Name}' is not supported by the game.", + VerificationSeverity.Information, + component.Name)); + } + else + { + occupiedComponentIds[component.Id] = true; + } + + if (alreadyOccupied) + { + AddError(VerificationError.Create(this, + VerifierErrorCodes.Duplicate, + $"The CommandBar component '{component.Name}' with ID '{component.Id}' already exists.", + VerificationSeverity.Warning, + component.Name)); + } + + VerifySingleComponent(component, token); + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.Groups.cs b/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.Groups.cs new file mode 100644 index 0000000..e3ed107 --- /dev/null +++ b/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.Groups.cs @@ -0,0 +1,56 @@ +using System; +using AET.ModVerify.Reporting; +using AnakinRaW.CommonUtilities.Collections; +using PG.StarWarsGame.Engine.CommandBar; +using PG.StarWarsGame.Engine.CommandBar.Components; + +namespace AET.ModVerify.Verifiers.CommandBar; + +partial class CommandBarVerifier +{ + private void VerifyCommandBarShellsGroups() + { + var shellGroups = new FrugalList(); + foreach (var groupPair in GameEngine.CommandBar.Groups) + { + if (groupPair.Key == CommandBarConstants.ShellGroupName) + { + shellGroups.Add(groupPair.Key); + VerifyShellGroup(groupPair.Value); + } + else if (groupPair.Key.Equals(CommandBarConstants.ShellGroupName, StringComparison.OrdinalIgnoreCase)) + { + shellGroups.Add(groupPair.Key); + } + } + + if (shellGroups.Count == 0) + AddError(VerificationError.Create(this, + CommandBarNoShellsGroup, + $"No CommandBarGroup '{CommandBarConstants.ShellGroupName}' found.", + VerificationSeverity.Error, + "GameCommandBar")); + + if (shellGroups.Count > 1) + AddError(VerificationError.Create(this, + CommandBarManyShellsGroup, + $"Found more than one Shells CommandBarGroup. Mind that group names are case-sensitive. Correct name is '{CommandBarConstants.ShellGroupName}'", + VerificationSeverity.Warning, + shellGroups, "GameCommandBar")); + } + + private void VerifyShellGroup(CommandBarComponentGroup shellGroup) + { + foreach (var component in shellGroup.Components) + { + var shellComponent = component as CommandBarShellComponent; + if (shellComponent?.Type is not CommandBarComponentType.Shell) + { + AddError(VerificationError.Create(this, + CommandBarNoShellsComponentInShellGroup, + $"The CommandBar component '{component.Name}' is not a shell component, but part of the '{CommandBarConstants.ShellGroupName}' group.", + VerificationSeverity.Warning, component.Name)); + } + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.MegaTexture.cs b/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.MegaTexture.cs new file mode 100644 index 0000000..8d86bad --- /dev/null +++ b/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.MegaTexture.cs @@ -0,0 +1,41 @@ +using System.Threading; +using AET.ModVerify.Reporting; +using AET.ModVerify.Verifiers.Commons; +using AET.ModVerify.Verifiers.Utilities; +using PG.StarWarsGame.Engine.CommandBar; + +namespace AET.ModVerify.Verifiers.CommandBar; + +partial class CommandBarVerifier +{ + private void VerifyMegaTexture(CancellationToken token) + { + if (CommandBar.MtdFile is null) + { + AddError(VerificationError.Create(this, VerifierErrorCodes.FileNotFound, + $"Cannot find CommandBar MegaTextureDirectory '{CommandBarConstants.MegaTextureBaseName}.mtd'", + VerificationSeverity.Critical, $"{CommandBarConstants.MegaTextureBaseName}.mtd")); + } + else + { + var dupVerifier = new DuplicateVerifier(this); + dupVerifier.Verify(IDuplicateVerificationContext.CreateForMtd(CommandBar.MtdFile), [], token); + + foreach (var duplicateError in dupVerifier.VerifyErrors) + AddError(duplicateError); + } + + if (CommandBar.MegaTextureFileName is null) + { + AddError(VerificationError.Create(this, VerifierErrorCodes.FileNotFound, + $"Cannot find CommandBar MegaTexture '{CommandBarConstants.MegaTextureBaseName}.tga'", + VerificationSeverity.Critical, $"{CommandBarConstants.MegaTextureBaseName}.tga")); + } + else if (!GameEngine.GameRepository.TextureRepository.FileExists(CommandBar.MegaTextureFileName)) + { + AddError(VerificationError.Create(this, VerifierErrorCodes.FileNotFound, + $"Cannot find CommandBar MegaTexture '{CommandBarConstants.MegaTextureBaseName}.tga'", + VerificationSeverity.Critical, $"{CommandBarConstants.MegaTextureBaseName}.tga")); + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.SingleComponent.cs b/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.SingleComponent.cs new file mode 100644 index 0000000..4a0d951 --- /dev/null +++ b/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.SingleComponent.cs @@ -0,0 +1,74 @@ +using AET.ModVerify.Reporting; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Engine.CommandBar.Components; +using System; +using System.Threading; + +namespace AET.ModVerify.Verifiers.CommandBar; + +partial class CommandBarVerifier +{ + private void VerifySingleComponent(CommandBarBaseComponent component, CancellationToken token) + { + VerifyName(component); + VerifyCommandBarModel(component, token); + VerifyComponentBone(component); + + // TODO: Textures + } + + private void VerifyName(CommandBarBaseComponent component) + { + if (component.Name.Length > PGConstants.MaxCommandBarComponentNameBuffer) + { + AddError(VerificationError.Create(this, VerifierErrorCodes.NameTooLong, + // Deliberately not reporting the buffer length as max, as it's considered to be internal data + $"The CommandBarShellComponent name '{component.Name}' is too long. Maximum length is {PGConstants.MaxCommandBarComponentName}.", + VerificationSeverity.Critical, [], component.Name)); + } + } + + private void VerifyCommandBarModel(CommandBarBaseComponent component, CancellationToken token) + { + if (component is not CommandBarShellComponent shellComponent) + return; + + if (shellComponent.ModelPath is null) + { + AddError(VerificationError.Create(this, + CommandBarShellNoModel, $"The CommandBarShellComponent '{component.Name}' has no model specified.", + VerificationSeverity.Error, [shellComponent.Name], shellComponent.Name)); + return; + } + + using var model = GameEngine.PGRender.LoadModelAndAnimations(shellComponent.ModelPath.AsSpan(), null); + if (model is null) + { + AddError(VerificationError.Create(this, + CommandBarShellNoModel, $"Could not find model '{shellComponent.ModelPath}' for CommandBarShellComponent '{component.Name}'.", + VerificationSeverity.Error, [shellComponent.Name], shellComponent.ModelPath)); + return; + } + + _modelVerifier.VerifyModelOrParticle(model.File, [shellComponent.Name], token); + + if (model.Animations.Cout == 0) + return; + + // TODO: Verify Animations + + } + + private void VerifyComponentBone(CommandBarBaseComponent component) + { + if (component is CommandBarShellComponent) + return; + + if (component.Bone == -1) + { + AddError(VerificationError.Create(this, + CommandBarShellNoModel, $"The CommandBar component '{component.Name}' is not connected to a shell component.", + VerificationSeverity.Warning, component.Name)); + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.cs b/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.cs new file mode 100644 index 0000000..4894673 --- /dev/null +++ b/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq; +using System.Threading; +using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers.Commons; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Engine.CommandBar; + +namespace AET.ModVerify.Verifiers.CommandBar; + +public partial class CommandBarVerifier : GameVerifier +{ + public const string CommandBarNoShellsGroup = "CMDBAR00"; + public const string CommandBarManyShellsGroup = "CMDBAR01"; + public const string CommandBarNoShellsComponentInShellGroup = "CMDBAR02"; + public const string CommandBarUnsupportedComponent = "CMDBAR03"; + public const string CommandBarShellNoModel = "CMDBAR04"; + + private readonly SingleModelVerifier _modelVerifier; + private readonly TextureVerifier _textureVerifier; + + public override string FriendlyName => "CommandBar"; + + public ICommandBarGameManager CommandBar { get; } + + public CommandBarVerifier(IStarWarsGameEngine gameEngine, GameVerifySettings settings, IServiceProvider serviceProvider) + : base(gameEngine, settings, serviceProvider) + { + CommandBar = gameEngine.CommandBar; + _modelVerifier = new SingleModelVerifier(this); + _textureVerifier = new TextureVerifier(this); + } + + public override void Verify(CancellationToken token) + { + var progress = 0.0d; + OnProgress(progress, "Verifying MegaTexture"); + VerifyMegaTexture(token); + progress = 1 / 3.0; + OnProgress(progress, "Verifying CommandBar Shell"); + VerifyCommandBarShellsGroups(); + progress = 2 / 3.0; + OnProgress(progress, "Verifying CommandBar components"); + VerifyCommandBarComponents(token, progress); + + foreach (var subError in _modelVerifier.VerifyErrors.Concat(_textureVerifier.VerifyErrors)) + AddError(subError); + + progress = 1.0; + OnProgress(progress, null); + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/Commons/Audio/AudioFileInfo.cs b/src/ModVerify/Verifiers/Commons/Audio/AudioFileInfo.cs new file mode 100644 index 0000000..ee428cb --- /dev/null +++ b/src/ModVerify/Verifiers/Commons/Audio/AudioFileInfo.cs @@ -0,0 +1,15 @@ +namespace AET.ModVerify.Verifiers.Commons; + +public class AudioFileInfo +{ + public string SampleName { get; } + public AudioFileType ExpectedType { get; } + public bool IsAmbient { get; } + + public AudioFileInfo(string sampleName, AudioFileType expectedType, bool isAmbient) + { + SampleName = sampleName; + ExpectedType = expectedType; + IsAmbient = isAmbient; + } +} diff --git a/src/ModVerify/Verifiers/Commons/Audio/AudioFileType.cs b/src/ModVerify/Verifiers/Commons/Audio/AudioFileType.cs new file mode 100644 index 0000000..05ea0bc --- /dev/null +++ b/src/ModVerify/Verifiers/Commons/Audio/AudioFileType.cs @@ -0,0 +1,7 @@ +namespace AET.ModVerify.Verifiers.Commons; + +public enum AudioFileType +{ + Wav, + Mp3 +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/Commons/AudioFileVerifier.cs b/src/ModVerify/Verifiers/Commons/AudioFileVerifier.cs new file mode 100644 index 0000000..4537a64 --- /dev/null +++ b/src/ModVerify/Verifiers/Commons/AudioFileVerifier.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.IO; +using AET.ModVerify.Reporting; +using AET.ModVerify.Settings; +using PG.StarWarsGame.Engine; +using System.Threading; +using Microsoft.Extensions.DependencyInjection; +using AET.ModVerify.Verifiers.Caching; + +namespace AET.ModVerify.Verifiers.Commons; + +public class AudioFileVerifier : GameVerifier +{ + private readonly IAlreadyVerifiedCache? _alreadyVerifiedCache; + + public AudioFileVerifier(GameVerifierBase parent) : base(parent) + { + _alreadyVerifiedCache = Services.GetService(); + } + + public AudioFileVerifier(IGameVerifierInfo? parent, + IStarWarsGameEngine gameEngine, + GameVerifySettings settings, + IServiceProvider serviceProvider) : base(parent, gameEngine, settings, serviceProvider) + { + _alreadyVerifiedCache = serviceProvider.GetService(); + } + + public override string FriendlyName => "Audio File format"; + + public override void Verify(AudioFileInfo sampleInfo, IReadOnlyCollection contextInfo, CancellationToken token) + { + var cached = _alreadyVerifiedCache?.GetEntry(sampleInfo.SampleName); + + if (cached?.AlreadyVerified is true) + { + if (!cached.Value.AssetExists) + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.FileNotFound, + $"Audio file '{sampleInfo.SampleName}' could not be found.", + VerificationSeverity.Error, + [.. contextInfo], + sampleInfo.SampleName)); + } + return; + } + + + var sampleString = sampleInfo.SampleName; + + using var sampleStream = Repository.TryOpenFile(sampleString.AsSpan()); + + _alreadyVerifiedCache?.TryAddEntry(sampleInfo.SampleName, sampleStream is not null); + + if (sampleStream is null) + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.FileNotFound, + $"Audio file '{sampleString}' could not be found.", + VerificationSeverity.Error, + [..contextInfo], + sampleString)); + return; + } + + if (sampleInfo.ExpectedType == AudioFileType.Mp3) + { + // TODO: MP3 support to be implemented + return; + } + + using var binaryReader = new BinaryReader(sampleStream); + + // Skip Header + "fmt " + binaryReader.BaseStream.Seek(16, SeekOrigin.Begin); + + var fmtSize = binaryReader.ReadInt32(); + var format = (WaveFormats)binaryReader.ReadInt16(); + var channels = binaryReader.ReadInt16(); + + var sampleRate = binaryReader.ReadInt32(); + var bytesPerSecond = binaryReader.ReadInt32(); + + var frameSize = binaryReader.ReadInt16(); + var bitPerSecondPerChannel = binaryReader.ReadInt16(); + + if (format != WaveFormats.PCM) + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.SampleNotPCM, + $"Audio file '{sampleString}' has an invalid format '{format}'. Supported is {WaveFormats.PCM}", + VerificationSeverity.Error, + [..contextInfo], + sampleString)); + } + + if (channels > 1 && !sampleInfo.IsAmbient) + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.SampleNotMono, + $"Audio file '{sampleString}' is not mono audio.", + VerificationSeverity.Information, + sampleString)); + } + + if (sampleRate > 48_000) + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.InvalidSampleRate, + $"Audio file '{sampleString}' has a too high sample rate of {sampleRate}. Maximum is 48.000Hz.", + VerificationSeverity.Error, + [..contextInfo], + sampleString)); + } + + if (bitPerSecondPerChannel > 16) + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.InvalidBitsPerSeconds, + $"Audio file '{sampleString}' has an invalid bit size of {bitPerSecondPerChannel}. Supported are 16bit.", + VerificationSeverity.Error, + [..contextInfo], + sampleString)); + } + } + + private enum WaveFormats + { + PCM = 1, + MSADPCM = 2, + IEEE_Float = 3, + } +} diff --git a/src/ModVerify/Verifiers/Commons/DuplicateVerifier.cs b/src/ModVerify/Verifiers/Commons/DuplicateVerifier.cs new file mode 100644 index 0000000..7aa19e6 --- /dev/null +++ b/src/ModVerify/Verifiers/Commons/DuplicateVerifier.cs @@ -0,0 +1,43 @@ +using AET.ModVerify.Reporting; +using AET.ModVerify.Settings; +using PG.StarWarsGame.Engine; +using System; +using System.Collections.Generic; +using System.Threading; + +namespace AET.ModVerify.Verifiers.Commons; + +public sealed class DuplicateVerifier : GameVerifier +{ + public override string FriendlyName => "Duplicate Verifier"; + + public DuplicateVerifier(GameVerifierBase parent) : base(parent) + { + } + + public DuplicateVerifier( + IGameVerifierInfo? parent, + IStarWarsGameEngine gameEngine, + GameVerifySettings settings, + IServiceProvider serviceProvider) + : base(parent, gameEngine, settings, serviceProvider) + { + } + + public override void Verify(IDuplicateVerificationContext toVerify, IReadOnlyCollection contextInfo, CancellationToken token) + { + foreach (var crc32 in toVerify.GetCrcs()) + { + if (toVerify.HasDuplicates(crc32, out var entryNames, out var context, out var errorMessage)) + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.Duplicate, + errorMessage, + VerificationSeverity.Error, + context, + entryNames)); + } + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/Commons/Duplicates/IDuplicateVerificationContext.cs b/src/ModVerify/Verifiers/Commons/Duplicates/IDuplicateVerificationContext.cs new file mode 100644 index 0000000..4301e54 --- /dev/null +++ b/src/ModVerify/Verifiers/Commons/Duplicates/IDuplicateVerificationContext.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using PG.Commons.Hashing; + +namespace AET.ModVerify.Verifiers.Commons; + +public interface IDuplicateVerificationContext +{ + string SourceName { get; } + IEnumerable GetCrcs(); + bool HasDuplicates(Crc32 crc, out string entryNames, out IEnumerable duplicateContext, out string errorMessage); +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/Commons/Duplicates/MtdDuplicateVerificationContext.cs b/src/ModVerify/Verifiers/Commons/Duplicates/MtdDuplicateVerificationContext.cs new file mode 100644 index 0000000..624a0a6 --- /dev/null +++ b/src/ModVerify/Verifiers/Commons/Duplicates/MtdDuplicateVerificationContext.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq; +using PG.Commons.Hashing; +using PG.StarWarsGame.Files.MTD.Files; + +namespace AET.ModVerify.Verifiers.Commons; + +internal sealed class MtdDuplicateVerificationContext(IMtdFile mtdFile) : IDuplicateVerificationContext +{ + public string SourceName => mtdFile.FileName; + + public IEnumerable GetCrcs() => mtdFile.Content.Select(x => x.Crc32); + + public bool HasDuplicates(Crc32 crc, out string entryNames, out IEnumerable duplicateContext, out string errorMessage) + { + var entries = mtdFile.Content.EntriesWithCrc(crc); + if (entries.Count > 1) + { + var firstEntry = entries.First(); + entryNames = firstEntry.FileName; + duplicateContext = entries.Select(x => $"'{x.FileName}' (CRC: {x.Crc32})"); + errorMessage = $"MTD File '{SourceName}' has duplicate definitions for CRC ({firstEntry}): " + + $"{string.Join(",", entries.Select(x => x.FileName))}"; + return true; + } + + entryNames = string.Empty; + duplicateContext = []; + errorMessage = string.Empty; + return false; + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/Commons/Duplicates/NamedXmlObjectDuplicateVerificationContext.cs b/src/ModVerify/Verifiers/Commons/Duplicates/NamedXmlObjectDuplicateVerificationContext.cs new file mode 100644 index 0000000..d50057d --- /dev/null +++ b/src/ModVerify/Verifiers/Commons/Duplicates/NamedXmlObjectDuplicateVerificationContext.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using PG.Commons.Hashing; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Files.XML.Data; + +namespace AET.ModVerify.Verifiers.Commons; + +internal sealed class NamedXmlObjectDuplicateVerificationContext(string databaseName, IGameManager gameManager) + : IDuplicateVerificationContext + where T : NamedXmlObject +{ + public string SourceName => databaseName; + + public IEnumerable GetCrcs() => gameManager.EntryKeys; + + public bool HasDuplicates(Crc32 crc, out string entryNames, out IEnumerable duplicateContext, out string errorMessage) + { + var entries = gameManager.GetEntries(crc); + if (entries.Count > 1) + { + var firstEntry = entries.First(); + entryNames = firstEntry.Name; + duplicateContext = entries.Select(x => $"'{x.Name}' - {x.Location}"); + var message = $"{SourceName} '{firstEntry.Name}' ({firstEntry.Crc32}) has duplicate definitions: "; + message = entries.Aggregate(message, (current, entry) => current + $"['{entry.Name}' in {entry.Location.XmlFile}:{entry.Location.Line}] "); + errorMessage = message.TrimEnd(); + return true; + } + + entryNames = string.Empty; + duplicateContext = Array.Empty(); + errorMessage = string.Empty; + return false; + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/Commons/ModelVerifier.cs b/src/ModVerify/Verifiers/Commons/SingleModelVerifier.cs similarity index 80% rename from src/ModVerify/Verifiers/Commons/ModelVerifier.cs rename to src/ModVerify/Verifiers/Commons/SingleModelVerifier.cs index 25704d1..fa621e1 100644 --- a/src/ModVerify/Verifiers/Commons/ModelVerifier.cs +++ b/src/ModVerify/Verifiers/Commons/SingleModelVerifier.cs @@ -4,6 +4,7 @@ using AET.ModVerify.Reporting; using AET.ModVerify.Settings; using AET.ModVerify.Utilities; +using AET.ModVerify.Verifiers.Caching; using Microsoft.Extensions.DependencyInjection; using PG.StarWarsGame.Engine; using PG.StarWarsGame.Files; @@ -22,47 +23,93 @@ public sealed class SingleModelVerifier : GameVerifier { private const string ProxyAltIdentifier = "_ALT"; - private readonly TextureVeifier _textureVerifier; + private readonly TextureVerifier _textureVerifier; private readonly IAlreadyVerifiedCache? _cache; - public SingleModelVerifier(IGameVerifierInfo? parent, + public SingleModelVerifier(GameVerifierBase parent) : base(parent) + { + _textureVerifier = new TextureVerifier(this); + _cache = Services.GetService(); + } + + public SingleModelVerifier( + IGameVerifierInfo? parent, IStarWarsGameEngine engine, GameVerifySettings settings, IServiceProvider serviceProvider) : base(parent, engine, settings, serviceProvider) { - _textureVerifier = new TextureVeifier(this, engine, settings, serviceProvider); + _textureVerifier = new TextureVerifier(this); _cache = serviceProvider.GetService(); } public override void Verify(string modelName, IReadOnlyCollection contextInfo, CancellationToken token) { - try + var cacheEntry = _cache?.GetEntry(modelName); + if (cacheEntry?.AlreadyVerified is true) { - _textureVerifier.Error += OnTextureError; - - var modelPath = BuildModelPath(modelName); - VerifyAlamoFile(modelPath, contextInfo, token); + if (!cacheEntry.Value.AssetExists) + { + var error = VerificationError.Create( + this, + VerifierErrorCodes.FileNotFound, + $"Unable to find .ALO file '{modelName}'", + VerificationSeverity.Error, + contextInfo, + modelName); + AddError(error); + } + return; } - finally + + var modelPath = BuildModelPath(modelName); + VerifyAlamoFile(modelPath, contextInfo, token, out var modelExists); + + if (!modelExists) { - _textureVerifier.Error -= OnTextureError; + var error = VerificationError.Create( + this, + VerifierErrorCodes.FileNotFound, + $"Unable to find .ALO file '{modelName}'", + VerificationSeverity.Error, + contextInfo, + modelName); + AddError(error); } + + _cache?.TryAddEntry(modelName, modelExists); + + foreach (var textureError in _textureVerifier.VerifyErrors) + AddError(textureError); } - private void OnTextureError(object sender, VerificationErrorEventArgs e) + public void VerifyModelOrParticle( + IAloFile aloFile, + IReadOnlyCollection contextInfo, + CancellationToken token) { - AddError(e.Error); + switch (aloFile) + { + case IAloModelFile model: + VerifyModel(model, contextInfo, token); + break; + case IAloParticleFile particle: + VerifyParticle(particle, contextInfo); + break; + default: + throw new InvalidOperationException("The data stream is neither a model nor particle."); + } } - private void VerifyAlamoFile(string modelPath, IReadOnlyCollection contextInfo, CancellationToken token) + private void VerifyAlamoFile( + string modelPath, + IReadOnlyCollection contextInfo, + CancellationToken token, + out bool modelExists) { token.ThrowIfCancellationRequested(); - var modelName = FileSystem.Path.GetFileName(modelPath.AsSpan()); + modelExists = true; - if (_cache?.TryAddEntry(modelName) == false) - return; - IAloFile? aloFile = null; try { @@ -74,22 +121,16 @@ private void VerifyAlamoFile(string modelPath, IReadOnlyCollection conte { var aloFilePath = FileSystem.Path.GetGameStrippedPath(Repository.Path.AsSpan(), modelPath.AsSpan()).ToString(); var message = $"'{aloFilePath}' is corrupted: {e.Message}"; - AddError(VerificationError.Create(VerifierChain, VerifierErrorCodes.FileCorrupt, message, + AddError(VerificationError.Create(this, VerifierErrorCodes.BinaryFileCorrupt, message, VerificationSeverity.Critical, contextInfo, aloFilePath)); return; } + // Because throwsException is true, we know that if aloFile is null, + // the file does not exist if (aloFile is null) { - var modelNameString = modelName.ToString(); - var error = VerificationError.Create( - VerifierChain, - VerifierErrorCodes.FileNotFound, - $"Unable to find .ALO file '{modelNameString}'", - VerificationSeverity.Error, - contextInfo, - modelNameString); - AddError(error); + modelExists = false; return; } @@ -101,24 +142,6 @@ private void VerifyAlamoFile(string modelPath, IReadOnlyCollection conte } } - private void VerifyModelOrParticle( - IAloFile aloFile, - IReadOnlyCollection contextInfo, - CancellationToken token) - { - switch (aloFile) - { - case IAloModelFile model: - VerifyModel(model, contextInfo, token); - break; - case IAloParticleFile particle: - VerifyParticle(particle, contextInfo); - break; - default: - throw new InvalidOperationException("The data stream is neither a model nor particle."); - } - } - private void VerifyParticle(IAloParticleFile file, IReadOnlyCollection contextInfo) { foreach (var texture in file.Content.Textures) @@ -129,7 +152,7 @@ private void VerifyParticle(IAloParticleFile file, IReadOnlyCollection c { var particlePath = FileSystem.Path.GetGameStrippedPath(Repository.Path.AsSpan(), file.FilePath.AsSpan()).ToString(); AddError(VerificationError.Create( - VerifierChain, + this, VerifierErrorCodes.InvalidFilePath, $"Invalid texture file name '{texture}' in particle '{particlePath}'", VerificationSeverity.Error, @@ -144,7 +167,7 @@ private void VerifyParticle(IAloParticleFile file, IReadOnlyCollection c { var particlePath = FileSystem.Path.GetGameStrippedPath(Repository.Path.AsSpan(), file.FilePath.AsSpan()).ToString(); AddError(VerificationError.Create( - VerifierChain, + this, VerifierErrorCodes.InvalidParticleName, $"The particle name '{file.Content.Name}' does not match file name '{particlePath}'", VerificationSeverity.Error, @@ -163,7 +186,7 @@ private void VerifyModel(IAloModelFile file, IReadOnlyCollection context { var modelFilePath = FileSystem.Path.GetGameStrippedPath(Repository.Path.AsSpan(), file.FilePath.AsSpan()).ToString(); AddError(VerificationError.Create( - VerifierChain, + this, VerifierErrorCodes.InvalidFilePath, $"Invalid texture file name '{texture}' in model '{modelFilePath}'", VerificationSeverity.Error, @@ -181,7 +204,7 @@ private void VerifyModel(IAloModelFile file, IReadOnlyCollection context var modelFilePath = FileSystem.Path.GetGameStrippedPath(Repository.Path.AsSpan(), file.FilePath.AsSpan()).ToString(); AddError(VerificationError.Create( - VerifierChain, + this, VerifierErrorCodes.InvalidFilePath, $"Invalid shader file name '{shader}' in model '{modelFilePath}'", VerificationSeverity.Error, @@ -200,7 +223,7 @@ private void VerifyModel(IAloModelFile file, IReadOnlyCollection context var modelFilePath = FileSystem.Path .GetGameStrippedPath(Repository.Path.AsSpan(), file.FilePath.AsSpan()).ToString(); AddError(VerificationError.Create( - VerifierChain, + this, VerifierErrorCodes.InvalidFilePath, $"Invalid proxy file name '{proxy}' for model '{modelFilePath}'", VerificationSeverity.Error, @@ -223,22 +246,21 @@ private void VerifyProxyExists(IPetroglyphFileHolder model, string proxy, IReadO var proxyPath = BuildModelPath(proxyName); var modelFilePath = FileSystem.Path.GetGameStrippedPath(Repository.Path.AsSpan(), model.FilePath.AsSpan()).ToString(); + + VerifyAlamoFile(proxyPath, [..contextInfo, modelFilePath], token, out var proxyExists); - if (!Repository.ModelRepository.FileExists(proxyPath)) + if (!proxyExists) { var message = $"Proxy particle '{proxyName}' not found for model '{modelFilePath}'"; var error = VerificationError.Create( - VerifierChain, + this, VerifierErrorCodes.FileNotFound, - message, - VerificationSeverity.Error, - [..contextInfo, modelFilePath], + message, + VerificationSeverity.Error, + [.. contextInfo, modelFilePath], proxyName); AddError(error); - return; } - - VerifyAlamoFile(proxyPath, [..contextInfo, modelFilePath], token); } private void VerifyShaderExists(IPetroglyphFileHolder model, string shader, IReadOnlyCollection contextInfo) @@ -251,7 +273,7 @@ private void VerifyShaderExists(IPetroglyphFileHolder model, string shader, IRea var modelFilePath = FileSystem.Path.GetGameStrippedPath(Repository.Path.AsSpan(), model.FilePath.AsSpan()).ToString(); var message = $"Shader effect '{shader}' not found for model '{modelFilePath}'."; var error = VerificationError.Create( - VerifierChain, + this, VerifierErrorCodes.FileNotFound, message, VerificationSeverity.Error, diff --git a/src/ModVerify/Verifiers/Commons/TextureVeifier.cs b/src/ModVerify/Verifiers/Commons/TextureVerifier.cs similarity index 68% rename from src/ModVerify/Verifiers/Commons/TextureVeifier.cs rename to src/ModVerify/Verifiers/Commons/TextureVerifier.cs index 1bd3a08..e7709e3 100644 --- a/src/ModVerify/Verifiers/Commons/TextureVeifier.cs +++ b/src/ModVerify/Verifiers/Commons/TextureVerifier.cs @@ -4,19 +4,24 @@ using System.Threading; using AET.ModVerify.Reporting; using AET.ModVerify.Settings; -using Microsoft.Extensions.DependencyInjection; using PG.StarWarsGame.Engine; namespace AET.ModVerify.Verifiers.Commons; -public sealed class TextureVeifier( - IGameVerifierInfo? parent, - IStarWarsGameEngine gameEngine, - GameVerifySettings settings, - IServiceProvider serviceProvider) - : GameVerifier(parent, gameEngine, settings, serviceProvider) +public sealed class TextureVerifier : GameVerifier { - private readonly IAlreadyVerifiedCache? _cache = serviceProvider.GetService(); + public TextureVerifier(GameVerifierBase parent) : base(parent) + { + } + + public TextureVerifier( + IGameVerifierInfo? parent, + IStarWarsGameEngine gameEngine, + GameVerifySettings settings, + IServiceProvider serviceProvider) : + base(parent, gameEngine, settings, serviceProvider) + { + } public override void Verify(string texturePath, IReadOnlyCollection contextInfo, CancellationToken token) { @@ -27,10 +32,6 @@ public void Verify(ReadOnlySpan textureName, IReadOnlyCollection c { token.ThrowIfCancellationRequested(); - - if (_cache?.TryAddEntry(textureName) == false) - return; - if (Repository.TextureRepository.FileExists(textureName, false, out var tooLongPath)) return; @@ -38,7 +39,7 @@ public void Verify(ReadOnlySpan textureName, IReadOnlyCollection c if (tooLongPath) { - AddError(VerificationError.Create(VerifierChain, VerifierErrorCodes.FilePathTooLong, + AddError(VerificationError.Create(this, VerifierErrorCodes.FilePathTooLong, $"Could not find texture '{pathString}' because the engine resolved a path that is too long.", VerificationSeverity.Error, contextInfo, pathString)); return; @@ -55,7 +56,7 @@ public void Verify(ReadOnlySpan textureName, IReadOnlyCollection c messageBuilder.Append('.'); - AddError(VerificationError.Create(VerifierChain, VerifierErrorCodes.FileNotFound, + AddError(VerificationError.Create(this, VerifierErrorCodes.FileNotFound, messageBuilder.ToString(), VerificationSeverity.Error, contextInfo, pathString)); } diff --git a/src/ModVerify/Verifiers/DuplicateNameFinder.cs b/src/ModVerify/Verifiers/DuplicateNameFinder.cs deleted file mode 100644 index 0b3b585..0000000 --- a/src/ModVerify/Verifiers/DuplicateNameFinder.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using AET.ModVerify.Reporting; -using AET.ModVerify.Settings; -using AnakinRaW.CommonUtilities.Collections; -using PG.Commons.Hashing; -using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Engine.Xml; -using PG.StarWarsGame.Files.MTD.Data; -using PG.StarWarsGame.Files.MTD.Files; - -namespace AET.ModVerify.Verifiers; - -public sealed class DuplicateNameFinder( - IStarWarsGameEngine gameEngine, - GameVerifySettings settings, - IServiceProvider serviceProvider) - : GameVerifier(null, gameEngine, settings, serviceProvider) -{ - public override string FriendlyName => "Duplicates"; - - public override void Verify(CancellationToken token) - { - CheckXmlObjectsForDuplicates("GameObject", GameEngine.GameObjectTypeManager); - CheckXmlObjectsForDuplicates("SFXEvent", GameEngine.SfxGameManager); - - if (GameEngine.GuiDialogManager.MtdFile is not null) - CheckMtdForDuplicates(GameEngine.GuiDialogManager.MtdFile); - - if (GameEngine.CommandBar.MegaTextureFile is not null) - { - if (!GameEngine.CommandBar.MegaTextureFile.FilePath.Equals(GameEngine.GuiDialogManager.MtdFile?.FileName)) - CheckMtdForDuplicates(GameEngine.CommandBar.MegaTextureFile); - } - } - - private void CheckForDuplicateCrcEntries( - string sourceName, - TSource source, - Func> crcSelector, - Func> entrySelector, - Func entryToStringSelector, - Func, IEnumerable> contextSelector, - Func, string, string> errorMessageCreator) - { - foreach (var crc32 in crcSelector(source)) - { - var entries = entrySelector(source, crc32); - if (entries.Count > 1) - { - var entryNames = entryToStringSelector(entries.First()); - var context = contextSelector(entries); - AddError(VerificationError.Create( - VerifierChain, - VerifierErrorCodes.DuplicateFound, - errorMessageCreator(entries, sourceName), - VerificationSeverity.Error, - context, - entryNames)); - } - } - } - - private void CheckMtdForDuplicates(IMtdFile mtdFile) - { - CheckForDuplicateCrcEntries( - mtdFile.FileName, - mtdFile, - mtd => mtd.Content.Select(x => x.Crc32), - (mtd, crc32) => mtd.Content.EntriesWithCrc(crc32), - entry => entry.FileName, - entries => entries.Select(x => $"'{x.FileName}' (CRC: {x.Crc32})"), - CreateDuplicateMtdErrorMessage); - } - - private void CheckXmlObjectsForDuplicates(string databaseName, IGameManager gameManager) where T : NamedXmlObject - { - CheckForDuplicateCrcEntries( - databaseName, - gameManager, - manager => manager.EntryKeys, - (manager, crc32) => manager.GetEntries(crc32), - entry => entry.Name, - entries => entries.Select(x => $"'{x.Name}' - {x.Location}"), - CreateDuplicateXmlErrorMessage); - } - - 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(ImmutableFrugalList entries, string databaseName) where T : NamedXmlObject - { - var firstEntry = entries.First(); - var message = $"{databaseName} '{firstEntry.Name}' ({firstEntry.Crc32}) has duplicate definitions: "; - foreach (var entry in entries) - message += $"['{entry.Name}' in {entry.Location.XmlFile}:{entry.Location.Line}] "; - return message.TrimEnd(); - } -} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/GameEngineErrorCollector.cs b/src/ModVerify/Verifiers/Engine/GameEngineErrorCollector.cs similarity index 91% rename from src/ModVerify/Verifiers/GameEngineErrorCollector.cs rename to src/ModVerify/Verifiers/Engine/GameEngineErrorCollector.cs index 975e360..70c1cb3 100644 --- a/src/ModVerify/Verifiers/GameEngineErrorCollector.cs +++ b/src/ModVerify/Verifiers/Engine/GameEngineErrorCollector.cs @@ -6,14 +6,14 @@ using AET.ModVerify.Settings; using PG.StarWarsGame.Engine; -namespace AET.ModVerify.Verifiers; +namespace AET.ModVerify.Verifiers.Engine; public sealed class GameEngineErrorCollector( IGameEngineErrorCollection errorCollection, IStarWarsGameEngine gameEngine, GameVerifySettings settings, IServiceProvider serviceProvider) - : GameVerifier(null, gameEngine, settings, serviceProvider) + : GameVerifier(gameEngine, settings, serviceProvider) { public override string FriendlyName => "Game Engine Initialization"; diff --git a/src/ModVerify/Verifiers/Engine/HardcodedAssetsVerifier.cs b/src/ModVerify/Verifiers/Engine/HardcodedAssetsVerifier.cs new file mode 100644 index 0000000..69a6800 --- /dev/null +++ b/src/ModVerify/Verifiers/Engine/HardcodedAssetsVerifier.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading; +using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers.Commons; +using PG.StarWarsGame.Engine; + +namespace AET.ModVerify.Verifiers.Engine; + +public sealed class HardcodedAssetsVerifier : GameVerifier +{ + private readonly SingleModelVerifier _modelVerifier; + + public HardcodedAssetsVerifier(IStarWarsGameEngine gameEngine, GameVerifySettings settings, IServiceProvider serviceProvider) + : base(gameEngine, settings, serviceProvider) + { + _modelVerifier = new SingleModelVerifier(this); + } + + public override void Verify(CancellationToken token) + { + OnProgress(0.0d, "Verifying Hardcoded Models"); + VerifyModels(token); + OnProgress(1.0, null); + } + + private void VerifyModels(CancellationToken token) + { + var models = HardcodedEngineAssets.GetHardcodedModels(GameEngine.EngineType); + + foreach (var model in models) + _modelVerifier.Verify(model, [], token); + + foreach (var error in _modelVerifier.VerifyErrors) + AddError(error); + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.Icons.cs b/src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.Icons.cs new file mode 100644 index 0000000..36541bb --- /dev/null +++ b/src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.Icons.cs @@ -0,0 +1,41 @@ +using AET.ModVerify.Reporting; +using PG.StarWarsGame.Engine.GameObjects; + +namespace AET.ModVerify.Verifiers.GameObjects; + +public sealed partial class GameObjectTypeVerifier +{ + private void VerifyIcons(GameObject gameObject, string[] context) + { + VerifyObjectIcon(gameObject, context); + } + + private void VerifyObjectIcon(GameObject gameObject, string[] context) + { + if (string.IsNullOrEmpty(gameObject.IconName)) + return; + + /* + * The engine loads game object icons with different strategies, depending on where the icon is displayed: + * 1. the game loads the texture from MTD and supports the faction prefixes e.g, r_ or e_ + * Faction prefixes have higher priority than the non-prefix versions. The player's faction (not the object owner) is used. + * This applies to all command bar components (such as build buttons) + * 2. the game loads the texture form MTD and does NOT support faction prefix. + * If the texture is not found, the game searches the texture with forced .dds name in the Textures folder. + * This applies to the GUI dialogs (such as battle summary) + * 3. the game only loads the texture from MTD NOT supporting faction prefix nor textures folder fallback. + * This applies (only) to the neutralize hero dialog + * + * We only verify whether the icon exists in the MTD data, as this is the primary case to all strategies + * (and it's what really should only be used for mods) + * Faction-specific icons are not verified as they are statically not decidable. + */ + + if (!GameEngine.CommandBar.IconExists(gameObject)) + { + AddError(VerificationError.Create(this, VerifierErrorCodes.FileNotFound, + $"Could not find icon '{gameObject.IconName}' for game object type '{gameObject.Name}'.", + VerificationSeverity.Warning, context, gameObject.IconName!)); + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.Models.cs b/src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.Models.cs new file mode 100644 index 0000000..39699a7 --- /dev/null +++ b/src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.Models.cs @@ -0,0 +1,13 @@ +using PG.StarWarsGame.Engine.GameObjects; +using System.Threading; + +namespace AET.ModVerify.Verifiers.GameObjects; + +public sealed partial class GameObjectTypeVerifier +{ + private void VerifyModels(GameObject gameObject, string[] context, CancellationToken token) + { + foreach (var model in GameEngine.GameObjectTypeManager.GetModels(gameObject)) + _singleModelVerifier.Verify(model, context, token); + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.XRef.cs b/src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.XRef.cs new file mode 100644 index 0000000..1bca0e9 --- /dev/null +++ b/src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.XRef.cs @@ -0,0 +1,44 @@ +using System.Linq; +using AET.ModVerify.Reporting; +using PG.StarWarsGame.Engine.GameObjects; + +namespace AET.ModVerify.Verifiers.GameObjects; + +public sealed partial class GameObjectTypeVerifier +{ + private void VerifyXRefs(GameObject gameObject, string[] context) + { + if (!string.IsNullOrEmpty(gameObject.VariantOfExistingTypeName) && gameObject.VariantOfExistingType is null) + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.MissingXRef, + $"Missing base type '{gameObject.VariantOfExistingTypeName}' for GameObject '{gameObject.Name}'", + VerificationSeverity.Critical, + [..context, "VariantOfExistingType"], + gameObject.VariantOfExistingTypeName)); + } + + VerifyCompanyUnits(gameObject, context); + } + + private void VerifyCompanyUnits(GameObject gameObject, string[] context) + { + if (gameObject.GroundCompanyUnits.Count == 0) + return; + + var uniqueCompanyUnits = gameObject.GroundCompanyUnits + .Select(x => x.ToUpperInvariant()) + .Distinct(); + + foreach (var companyUnit in uniqueCompanyUnits) + { + if (GameEngine.GameObjectTypeManager.FindObjectType(companyUnit) is null) + { + AddError(VerificationError.Create(this, VerifierErrorCodes.MissingXRef, + $"Missing company unit '{companyUnit}' for GameObject '{gameObject.Name}'", + VerificationSeverity.Critical, [..context, "CompanyUnits"], companyUnit)); + } + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.cs b/src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.cs new file mode 100644 index 0000000..66d4672 --- /dev/null +++ b/src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading; +using AET.ModVerify.Reporting; +using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers.Commons; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Engine.GameObjects; + +namespace AET.ModVerify.Verifiers.GameObjects; + +// TODO: Add GameObjectTypeVerifier and check that LandModelTerrainOverride is correct (all keys correct, no dups) +public sealed partial class GameObjectTypeVerifier : NamedGameEntityVerifier +{ + private readonly SingleModelVerifier _singleModelVerifier; + + public override string FriendlyName => "GameObjectType Verifier"; + + public override IGameManager GameManager => GameEngine.GameObjectTypeManager; + + public override string EntityTypeName => "GameObjectType"; + + public GameObjectTypeVerifier( + IStarWarsGameEngine gameEngine, + GameVerifySettings settings, + IServiceProvider serviceProvider) + : base(gameEngine, settings, serviceProvider) + { + _singleModelVerifier = new SingleModelVerifier(this); + } + + protected override void VerifyEntity(GameObject entity, string[] context, double progress, CancellationToken token) + { + if (entity.Name.Length >= PGConstants.MaxGameObjectTypeName) + { + AddError(VerificationError.Create(this, VerifierErrorCodes.NameTooLong, + $"The GameObjectType name '{entity.Name}' is too long. Maximum length is {PGConstants.MaxGameObjectTypeName}.", + VerificationSeverity.Critical, entity.Name)); + } + VerifyXRefs(entity, context); + VerifyModels(entity, context, token); + VerifyIcons(entity, context); + } + + protected override void PostEntityVerify(CancellationToken token) + { + foreach (var modelError in _singleModelVerifier.VerifyErrors) + AddError(modelError); + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/GameVerifier.cs b/src/ModVerify/Verifiers/GameVerifier.cs index aebfe94..83871d9 100644 --- a/src/ModVerify/Verifiers/GameVerifier.cs +++ b/src/ModVerify/Verifiers/GameVerifier.cs @@ -6,22 +6,50 @@ namespace AET.ModVerify.Verifiers; -public abstract class GameVerifier( - IGameVerifierInfo? parent, - IStarWarsGameEngine gameEngine, - GameVerifySettings settings, - IServiceProvider serviceProvider) - : GameVerifierBase(parent, gameEngine, settings, serviceProvider) where T : notnull +public abstract class GameVerifier : GameVerifierBase where T : notnull { + protected GameVerifier( + IGameVerifierInfo? parent, + IStarWarsGameEngine gameEngine, + GameVerifySettings settings, + IServiceProvider serviceProvider) : base(parent, gameEngine, settings, serviceProvider) + { + } + protected GameVerifier(GameVerifierBase parent) : base(parent) + { + } + + protected GameVerifier( + IStarWarsGameEngine gameEngine, + GameVerifySettings settings, + IServiceProvider serviceProvider) : base(gameEngine, settings, serviceProvider) + { + } + public abstract void Verify(T toVerify, IReadOnlyCollection contextInfo, CancellationToken token); } -public abstract class GameVerifier( - IGameVerifierInfo? parent, - IStarWarsGameEngine gameEngine, - GameVerifySettings settings, - IServiceProvider serviceProvider) - : GameVerifierBase(parent, gameEngine, settings, serviceProvider) +public abstract class GameVerifier : GameVerifierBase { + protected GameVerifier( + IGameVerifierInfo? parent, + IStarWarsGameEngine gameEngine, + GameVerifySettings settings, + IServiceProvider serviceProvider) + : base(parent, gameEngine, settings, serviceProvider) + { + } + protected GameVerifier(GameVerifierBase parent) : base(parent) + { + } + + protected GameVerifier( + IStarWarsGameEngine gameEngine, + GameVerifySettings settings, + IServiceProvider serviceProvider) + : base(gameEngine, settings, serviceProvider) + { + } + public abstract void Verify(CancellationToken token); } \ No newline at end of file diff --git a/src/ModVerify/Verifiers/GameVerifierBase.cs b/src/ModVerify/Verifiers/GameVerifierBase.cs index 22b2789..2da47bc 100644 --- a/src/ModVerify/Verifiers/GameVerifierBase.cs +++ b/src/ModVerify/Verifiers/GameVerifierBase.cs @@ -8,7 +8,10 @@ using System.Collections.Generic; using System.IO.Abstractions; using AET.ModVerify.Progress; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using PG.StarWarsGame.Engine; +using AET.ModVerify.Verifiers.Utilities; namespace AET.ModVerify.Verifiers; @@ -22,6 +25,7 @@ public abstract class GameVerifierBase : IGameVerifierInfo protected readonly IFileSystem FileSystem; protected readonly IServiceProvider Services; protected readonly GameVerifySettings Settings; + protected readonly ILogger Logger; public IReadOnlyCollection VerifyErrors => [.. _verifyErrors.Keys]; @@ -35,7 +39,23 @@ public abstract class GameVerifierBase : IGameVerifierInfo protected IGameRepository Repository => GameEngine.GameRepository; - protected IReadOnlyList VerifierChain { get; } + public IReadOnlyList VerifierChain { get; } + + + protected GameVerifierBase(GameVerifierBase parent) + : this(parent, parent.GameEngine, parent.Settings, parent.Services) + { + if (parent == null) + throw new ArgumentNullException(nameof(parent)); + } + + protected GameVerifierBase( + IStarWarsGameEngine gameEngine, + GameVerifySettings settings, + IServiceProvider serviceProvider) + : this (null, gameEngine, settings, serviceProvider) + { + } protected GameVerifierBase( IGameVerifierInfo? parent, @@ -45,12 +65,13 @@ protected GameVerifierBase( { if (serviceProvider == null) throw new ArgumentNullException(nameof(serviceProvider)); + Logger = serviceProvider.GetService()?.CreateLogger(GetType()) ?? NullLogger.Instance; FileSystem = serviceProvider.GetRequiredService(); Services = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); Parent = parent; Settings = settings ?? throw new ArgumentNullException(nameof(settings)); GameEngine = gameEngine ?? throw new ArgumentNullException(nameof(gameEngine)); - VerifierChain = CreateVerifierChain(); + VerifierChain = this.GetVerifierChain(); } protected void AddError(VerificationError error) @@ -80,18 +101,4 @@ protected void OnProgress(double progress, string? message) { Progress?.Invoke(this, new(progress, message)); } - - private IReadOnlyList CreateVerifierChain() - { - var verifierChain = new List { this }; - - var parent = Parent; - while (parent != null) - { - verifierChain.Insert(0, parent); - parent = parent.Parent; - } - - return verifierChain; - } } \ No newline at end of file diff --git a/src/ModVerify/Verifiers/GuiDialogs/GuiDialogsVerifier.cs b/src/ModVerify/Verifiers/GuiDialogs/GuiDialogsVerifier.cs index 435c5de..a33f900 100644 --- a/src/ModVerify/Verifiers/GuiDialogs/GuiDialogsVerifier.cs +++ b/src/ModVerify/Verifiers/GuiDialogs/GuiDialogsVerifier.cs @@ -1,5 +1,6 @@ using AET.ModVerify.Reporting; using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers.Caching; using AET.ModVerify.Verifiers.Commons; using Microsoft.Extensions.DependencyInjection; using PG.StarWarsGame.Engine; @@ -8,6 +9,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Threading; namespace AET.ModVerify.Verifiers.GuiDialogs; @@ -16,32 +18,29 @@ sealed class GuiDialogsVerifier : GameVerifier { internal const string DefaultComponentIdentifier = "<>"; - private static readonly GuiComponentType[] GuiComponentTypes = + private static readonly IReadOnlyList GuiComponentTypes = Enum.GetValues(typeof(GuiComponentType)).OfType().ToArray(); private readonly IAlreadyVerifiedCache? _cache; - private readonly TextureVeifier _textureVerifier; + private readonly TextureVerifier _textureVerifier; - public GuiDialogsVerifier(IStarWarsGameEngine gameEngine, + public GuiDialogsVerifier( + IStarWarsGameEngine gameEngine, GameVerifySettings settings, - IServiceProvider serviceProvider) : base(null, gameEngine, settings, serviceProvider) + IServiceProvider serviceProvider) + : base(gameEngine, settings, serviceProvider) { _cache = serviceProvider.GetService(); - _textureVerifier = new TextureVeifier(this, gameEngine, settings, serviceProvider); + _textureVerifier = new TextureVerifier(this); } public override void Verify(CancellationToken token) { - try - { - _textureVerifier.Error += OnTextureError; - VerifyMegaTexturesExist(token); - VerifyGuiTextures(); - } - finally - { - _textureVerifier.Error -= OnTextureError; - } + VerifyMegaTexturesExist(token); + VerifyGuiTextures(); + + foreach (var textureError in _textureVerifier.VerifyErrors) + AddError(textureError); } private void VerifyGuiTextures() @@ -52,6 +51,8 @@ private void VerifyGuiTextures() }; components.AddRange(GameEngine.GuiDialogManager.Components); + // TODO: Verify no double definitions for textures and components exit + foreach (var component in components) VerifyGuiComponentTexturesExist(component); @@ -63,8 +64,8 @@ private void VerifyMegaTexturesExist(CancellationToken token) if (GameEngine.GuiDialogManager.MtdFile is null) { var mtdFileName = megaTextureName ?? "<>"; - VerificationError.Create(VerifierChain, VerifierErrorCodes.FileNotFound, $"MtdFile '{mtdFileName}.mtd' could not be found", - VerificationSeverity.Critical, mtdFileName); + AddError(VerificationError.Create(this, VerifierErrorCodes.FileNotFound, $"MtdFile '{mtdFileName}.mtd' could not be found", + VerificationSeverity.Critical, mtdFileName)); } if (megaTextureName is not null) @@ -84,13 +85,19 @@ private void VerifyMegaTexturesExist(CancellationToken token) private void VerifyGuiComponentTexturesExist(string component) { - var middleButtonInRepoMode = false; - - + var buttonSpecialMode = false; + var entriesForComponent = GetTextureEntriesForComponents(component, out var defined); if (!defined) return; + if (entriesForComponent.TryGetValue(GuiComponentType.ButtonMiddle, out var middleTexture)) + { + GameEngine.GuiDialogManager.TextureExists(middleTexture, out var origin, out _); + if (origin == GuiTextureOrigin.Repository) + buttonSpecialMode = true; + } + foreach (var componentType in GuiComponentTypes) { try @@ -98,55 +105,68 @@ private void VerifyGuiComponentTexturesExist(string component) if (!entriesForComponent.TryGetValue(componentType, out var texture)) continue; - if (_cache?.TryAddEntry(texture.Texture) == false) + if (buttonSpecialMode && componentType.IsButton() && !componentType.SupportsSpecialTextureMode()) + { + // If we are in special button mode, non-supported button textures won't be loaded anyway. + continue; + } + + var cached = _cache?.GetEntry(texture.Texture); + if (cached?.AlreadyVerified is true) { // If we are in a special case we don't want to skip - if (!middleButtonInRepoMode && + if (!buttonSpecialMode && componentType is not GuiComponentType.ButtonMiddle && componentType is not GuiComponentType.Scanlines && componentType is not GuiComponentType.FrameBackground) continue; } - if (!GameEngine.GuiDialogManager.TextureExists( - texture, - out var origin, - out var isNone, - middleButtonInRepoMode) - && !isNone) + var exists = GameEngine.GuiDialogManager.TextureExists( + texture, + out var origin, + out var isNone, + buttonSpecialMode); + + if (!exists && !isNone) { - if (origin == GuiTextureOrigin.MegaTexture && texture.Texture.Length > MtdFileConstants.MaxFileNameSize) { - AddError(VerificationError.Create(VerifierChain, VerifierErrorCodes.FilePathTooLong, + AddError(VerificationError.Create(this, VerifierErrorCodes.FilePathTooLong, $"The filename is too long. Max length is {MtdFileConstants.MaxFileNameSize} characters.", VerificationSeverity.Error, texture.Texture)); } else { - var message = $"Could not find GUI texture '{texture.Texture}' at location '{origin}'."; - - if (texture.Texture.Length > PGConstants.MaxMegEntryPathLength) - message += " The file name is too long."; - - AddError(VerificationError.Create(VerifierChain, VerifierErrorCodes.FileNotFound, - message, VerificationSeverity.Error, - [component, origin.ToString()], texture.Texture)); + AddNotFoundError(texture, component, origin); } } - - if (componentType is GuiComponentType.ButtonMiddle && origin is GuiTextureOrigin.Repository) - middleButtonInRepoMode = true; + + _cache?.TryAddEntry(texture.Texture, exists); } finally { - - if (componentType >= GuiComponentType.ButtonRightDisabled) - middleButtonInRepoMode = false; + if (!componentType.IsButton()) + buttonSpecialMode = false; } } } + private void AddNotFoundError(ComponentTextureEntry texture, string component, GuiTextureOrigin? origin) + { + var sb = new StringBuilder($"Could not find GUI texture '{texture.Texture}'"); + if (origin is not null) + sb.Append($" at location '{origin}'"); + sb.Append('.'); + + if (texture.Texture.Length > PGConstants.MaxMegEntryPathLength) + sb.Append(" The file name is too long."); + + AddError(VerificationError.Create(this, VerifierErrorCodes.FileNotFound, + sb.ToString(), VerificationSeverity.Error, + [component, origin.ToString()], texture.Texture)); + } + private IReadOnlyDictionary GetTextureEntriesForComponents(string component, out bool defined) { if (component == DefaultComponentIdentifier) diff --git a/src/ModVerify/Verifiers/IAlreadyVerifiedCache.cs b/src/ModVerify/Verifiers/IAlreadyVerifiedCache.cs deleted file mode 100644 index 1fe8ad5..0000000 --- a/src/ModVerify/Verifiers/IAlreadyVerifiedCache.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using PG.Commons.Hashing; - -namespace AET.ModVerify.Verifiers; - -public interface IAlreadyVerifiedCache -{ - public bool TryAddEntry(string entry); - public bool TryAddEntry(ReadOnlySpan entry); - public bool TryAddEntry(Crc32 checksum); -} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/IGameVerifierInfo.cs b/src/ModVerify/Verifiers/IGameVerifierInfo.cs index cba43d1..731d605 100644 --- a/src/ModVerify/Verifiers/IGameVerifierInfo.cs +++ b/src/ModVerify/Verifiers/IGameVerifierInfo.cs @@ -1,9 +1,13 @@ -namespace AET.ModVerify.Verifiers; +using System.Collections.Generic; + +namespace AET.ModVerify.Verifiers; public interface IGameVerifierInfo { IGameVerifierInfo? Parent { get; } + IReadOnlyList VerifierChain { get; } + string Name { get; } string FriendlyName { get; } diff --git a/src/ModVerify/Verifiers/NamedGameEntityVerifier.cs b/src/ModVerify/Verifiers/NamedGameEntityVerifier.cs new file mode 100644 index 0000000..18ec9c3 --- /dev/null +++ b/src/ModVerify/Verifiers/NamedGameEntityVerifier.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading; +using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers.Commons; +using AET.ModVerify.Verifiers.Utilities; +using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Files.XML.Data; + +namespace AET.ModVerify.Verifiers; + +public abstract partial class NamedGameEntityVerifier( + IStarWarsGameEngine gameEngine, + GameVerifySettings settings, + IServiceProvider serviceProvider) + : GameVerifier(null, gameEngine, settings, serviceProvider) + where T : NamedXmlObject +{ + public abstract IGameManager GameManager { get; } + + public abstract string EntityTypeName { get; } + + public sealed override void Verify(CancellationToken token) + { + OnProgress(0.0, $"Verifying GameManager for '{EntityTypeName}'"); + PreEntityVerify(token); + OnProgress(0.5, null); + + var numEntities = GameEngine.GameObjectTypeManager.Entries.Count; + double counter = 0; + var context = new string[1]; + foreach (var gameEntity in GameManager.Entries) + { + LogVerifyingEntityTypeName(Logger, EntityTypeName, gameEntity.Name); + var progress = 0.5 + ++counter / numEntities * 0.5; + OnProgress(progress, $"{EntityTypeName} - '{gameEntity.Name}'"); + context[0] = gameEntity.Name; + VerifyEntity(gameEntity, context, progress, token); + } + + PostEntityVerify(token); + } + + protected virtual void PostEntityVerify(CancellationToken token) + { + } + + protected abstract void VerifyEntity(T entity, string[] context, double progress, CancellationToken token); + + protected virtual void PreEntityVerify(CancellationToken token) + { + VerifyDuplicates(token); + } + + private void VerifyDuplicates(CancellationToken token) + { + LogCheckingEntityTypeForDuplicateEntries(Logger, EntityTypeName); + var context = IDuplicateVerificationContext.CreateForNamedXmlObjects(GameManager, EntityTypeName); + var verifier = new DuplicateVerifier(this, GameEngine, Settings, Services); + verifier.Verify(context, [], token); + foreach (var error in verifier.VerifyErrors) + AddError(error); + } + + [LoggerMessage(LogLevel.Trace, "Verifying {entityType} - '{name}'")] + static partial void LogVerifyingEntityTypeName(ILogger? logger, string entityType, string name); + + [LoggerMessage(LogLevel.Debug, "Checking {entityType} for duplicate entries")] + static partial void LogCheckingEntityTypeForDuplicateEntries(ILogger logger, string entityType); +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/ReferencedModelsVerifier.cs b/src/ModVerify/Verifiers/ReferencedModelsVerifier.cs deleted file mode 100644 index 4a5feb6..0000000 --- a/src/ModVerify/Verifiers/ReferencedModelsVerifier.cs +++ /dev/null @@ -1,63 +0,0 @@ -using AET.ModVerify.Settings; -using AET.ModVerify.Verifiers.Commons; -using PG.StarWarsGame.Engine; -using System; -using System.Linq; -using System.Threading; - -namespace AET.ModVerify.Verifiers; - -public sealed class ReferencedModelsVerifier( - IStarWarsGameEngine engine, - GameVerifySettings settings, - IServiceProvider serviceProvider) - : GameVerifier(null, engine, settings, serviceProvider) -{ - public override string FriendlyName => "Referenced Models"; - - public override void Verify(CancellationToken token) - { - var gameObjectEntries = GameEngine.GameObjectTypeManager.Entries.ToList(); - var hardcodedModels = FocHardcodedConstants.HardcodedModels.ToList(); - - var totalModelsCount = gameObjectEntries.Sum(x => x.Models.Count()) + hardcodedModels.Count; - - if (totalModelsCount == 0) - return; - - var counter = 0; - - var inner = new SingleModelVerifier(this, GameEngine, Settings, Services); - try - { - inner.Error += OnModelError; - - var context = new string[1]; - foreach (var gameObject in gameObjectEntries) - { - context[0] = $"GameObject: {gameObject.Name}"; - foreach (var model in gameObject.Models) - { - OnProgress((double)++counter / totalModelsCount, $"Model - '{model}'"); - inner.Verify(model, context, token); - } - } - - context[0] = "Hardcoded Model"; - foreach (var hardcodedModel in hardcodedModels) - { - OnProgress((double)++counter / totalModelsCount, $"Model - '{hardcodedModel}'"); - inner.Verify(hardcodedModel, context, token); - } - } - finally - { - inner.Error -= OnModelError; - } - } - - private void OnModelError(object sender, VerificationErrorEventArgs e) - { - AddError(e.Error); - } -} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/SfxEvents/SfxEventVerifier.Samples.cs b/src/ModVerify/Verifiers/SfxEvents/SfxEventVerifier.Samples.cs new file mode 100644 index 0000000..68aee32 --- /dev/null +++ b/src/ModVerify/Verifiers/SfxEvents/SfxEventVerifier.Samples.cs @@ -0,0 +1,114 @@ +using System; +using System.Buffers; +using System.Threading; +using AET.ModVerify.Verifiers.Commons; +using AnakinRaW.CommonUtilities.FileSystem.Normalization; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Engine.Audio.Sfx; +using PG.StarWarsGame.Engine.Localization; + +namespace AET.ModVerify.Verifiers.SfxEvents; + +public partial class SfxEventVerifier +{ + private void VerifySamples(SfxEvent sfxEvent, string[] context, CancellationToken token) + { + var isAmbient = IsAmbient2D(sfxEvent); + foreach (var codedSample in sfxEvent.AllSamples) + VerifySample(codedSample.AsSpan(), sfxEvent, context, isAmbient, token); + } + + private void VerifySample( + ReadOnlySpan sample, + SfxEvent sfxEvent, + string[] context, + bool isAmbient, + CancellationToken token) + { + char[]? pooledBuffer = null; + + var buffer = sample.Length < PGConstants.MaxMegEntryPathLength + ? stackalloc char[PGConstants.MaxMegEntryPathLength] + : pooledBuffer = ArrayPool.Shared.Rent(sample.Length); + + try + { + var length = PathNormalizer.Normalize(sample, buffer, SampleNormalizerOptions); + var sampleNameBuffer = buffer.Slice(0, length); + + if (sfxEvent.IsLocalized) + { + foreach (var language in _languagesToVerify) + { + VerifySampleLocalized(context, sampleNameBuffer, isAmbient, language, out var localized, token); + if (!localized) + { + // There is no reason to continue if we failed to localize the sample name, because the verification will fail anyway + // and we want to avoid multiple errors for the same sample. + return; + } + } + } + else + { + var audioInfo = new AudioFileInfo(sampleNameBuffer.ToString(), AudioFileType.Wav, isAmbient); + _audioFileVerifier.Verify(audioInfo, context, token); + } + } + finally + { + if (pooledBuffer is not null) + ArrayPool.Shared.Return(pooledBuffer); + } + } + + private void VerifySampleLocalized(string[] context, + ReadOnlySpan sample, + bool isAmbient, + LanguageType language, + out bool localized, + CancellationToken token) + { + char[]? pooledBuffer = null; + + var buffer = sample.Length < PGConstants.MaxMegEntryPathLength + ? stackalloc char[PGConstants.MaxMegEntryPathLength] + : pooledBuffer = ArrayPool.Shared.Rent(sample.Length); + try + { + var l = _languageManager.LocalizeFileName(sample, language, buffer, out localized); + var localizedName = buffer.Slice(0, l); + + var audioInfo = new AudioFileInfo(localizedName.ToString(), AudioFileType.Wav, isAmbient); + _audioFileVerifier.Verify(audioInfo, context, token); + } + finally + { + if (pooledBuffer is not null) + ArrayPool.Shared.Return(pooledBuffer); + } + } + + // Some heuristics whether a SFXEvent is most likely to be an ambient sound. + private bool IsAmbient2D(SfxEvent sfxEvent) + { + if (!sfxEvent.Is2D) + return false; + + if (sfxEvent.IsPreset) + return false; + + // If the event is located in SFXEventsAmbient.xml we simply assume it's an ambient sound. + var fileName = _fileSystem.Path.GetFileName(sfxEvent.Location.XmlFile).AsSpan(); + if (fileName.Equals("SFXEventsAmbient.xml".AsSpan(), StringComparison.OrdinalIgnoreCase)) + return true; + + if (string.IsNullOrEmpty(sfxEvent.UsePresetName)) + return false; + + if (sfxEvent.UsePresetName!.StartsWith("Preset_AMB_2D", StringComparison.OrdinalIgnoreCase)) + return true; + + return false; + } +} diff --git a/src/ModVerify/Verifiers/SfxEvents/SfxEventVerifier.XRef.cs b/src/ModVerify/Verifiers/SfxEvents/SfxEventVerifier.XRef.cs new file mode 100644 index 0000000..ffdb772 --- /dev/null +++ b/src/ModVerify/Verifiers/SfxEvents/SfxEventVerifier.XRef.cs @@ -0,0 +1,21 @@ +using AET.ModVerify.Reporting; +using PG.StarWarsGame.Engine.Audio.Sfx; + +namespace AET.ModVerify.Verifiers.SfxEvents; + +public partial class SfxEventVerifier +{ + private void VerifyPresetRef(SfxEvent sfxEvent, string[] context) + { + if (!string.IsNullOrEmpty(sfxEvent.UsePresetName) && sfxEvent.Preset is null) + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.MissingXRef, + $"Missing preset '{sfxEvent.UsePresetName}' for SFXEvent '{sfxEvent.Name}'.", + VerificationSeverity.Error, + [..context, "Preset"], + sfxEvent.UsePresetName)); + } + } +} diff --git a/src/ModVerify/Verifiers/SfxEvents/SfxEventVerifier.cs b/src/ModVerify/Verifiers/SfxEvents/SfxEventVerifier.cs new file mode 100644 index 0000000..c688634 --- /dev/null +++ b/src/ModVerify/Verifiers/SfxEvents/SfxEventVerifier.cs @@ -0,0 +1,81 @@ +using AET.ModVerify.Reporting; +using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers.Commons; +using AnakinRaW.CommonUtilities.FileSystem.Normalization; +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Engine.Audio.Sfx; +using PG.StarWarsGame.Engine.Localization; +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Threading; + +namespace AET.ModVerify.Verifiers.SfxEvents; + +public sealed partial class SfxEventVerifier : NamedGameEntityVerifier +{ + private static readonly PathNormalizeOptions SampleNormalizerOptions = new() + { + UnifyCase = UnifyCasingKind.UpperCaseForce, + UnifySeparatorKind = DirectorySeparatorKind.Windows, + UnifyDirectorySeparators = true + }; + + private readonly IGameLanguageManager _languageManager; + private readonly IFileSystem _fileSystem; + private readonly AudioFileVerifier _audioFileVerifier; + private readonly IReadOnlyCollection _languagesToVerify; + + public override IGameManager GameManager => GameEngine.SfxGameManager; + public override string FriendlyName => "SFX Events"; + public override string EntityTypeName => "SFXEvent"; + + public SfxEventVerifier( + IStarWarsGameEngine gameEngine, + GameVerifySettings settings, + IServiceProvider serviceProvider) + : base(gameEngine, settings, serviceProvider) + { + _languageManager = serviceProvider.GetRequiredService() + .GetLanguageManager(Repository.EngineType); + _fileSystem = serviceProvider.GetRequiredService(); + _audioFileVerifier = new AudioFileVerifier(this); + _languagesToVerify = GetLanguagesToVerify(); + } + + protected override void VerifyEntity(SfxEvent entity, string[] context, double progress, CancellationToken token) + { + if (entity.Name.Length >= PGConstants.MaxSFXEventName) + { + AddError(VerificationError.Create(this, VerifierErrorCodes.NameTooLong, + $"The SFXEvent name '{entity.Name}' is too long. Maximum length is {PGConstants.MaxSFXEventName}.", + VerificationSeverity.Critical, entity.Name)); + } + VerifyPresetRef(entity, context); + VerifySamples(entity, context, token); + } + + protected override void PostEntityVerify(CancellationToken token) + { + foreach (var sampleError in _audioFileVerifier.VerifyErrors) + AddError(sampleError); + } + + private IReadOnlyCollection GetLanguagesToVerify() + { + switch (Settings.LocalizationOption) + { + case VerifyLocalizationOption.English: + return [LanguageType.English]; + case VerifyLocalizationOption.CurrentSystem: + return [_languageManager.GetLanguagesFromUser()]; + case VerifyLocalizationOption.AllInstalled: + return [..GameEngine.InstalledLanguages]; + case VerifyLocalizationOption.All: + return [.._languageManager.SupportedLanguages]; + default: + throw new NotSupportedException($"{Settings.LocalizationOption} is not supported"); + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/Utilities/DuplicateVerificationContextExtensions.cs b/src/ModVerify/Verifiers/Utilities/DuplicateVerificationContextExtensions.cs new file mode 100644 index 0000000..cd81de6 --- /dev/null +++ b/src/ModVerify/Verifiers/Utilities/DuplicateVerificationContextExtensions.cs @@ -0,0 +1,22 @@ +using AET.ModVerify.Verifiers.Commons; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Files.MTD.Files; +using PG.StarWarsGame.Files.XML.Data; + +namespace AET.ModVerify.Verifiers.Utilities; + +public static class DuplicateVerificationContextExtensions +{ + extension(IDuplicateVerificationContext) + { + public static IDuplicateVerificationContext CreateForMtd(IMtdFile mtdFile) + { + return new MtdDuplicateVerificationContext(mtdFile); + } + + public static IDuplicateVerificationContext CreateForNamedXmlObjects(IGameManager gameManager, string databaseName) where T : NamedXmlObject + { + return new NamedXmlObjectDuplicateVerificationContext(databaseName, gameManager); + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/Utilities/GameVerifierInfoExtensions.cs b/src/ModVerify/Verifiers/Utilities/GameVerifierInfoExtensions.cs new file mode 100644 index 0000000..05787a8 --- /dev/null +++ b/src/ModVerify/Verifiers/Utilities/GameVerifierInfoExtensions.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace AET.ModVerify.Verifiers.Utilities; + +internal static class GameVerifierInfoExtensions +{ + public static IReadOnlyList GetVerifierChain(this IGameVerifierInfo verifier) + { + if (verifier.Parent is null) + return [verifier]; + + var parentChain = verifier.Parent.VerifierChain; + var result = new List(parentChain.Count + 1); + result.AddRange(parentChain); + result.Add(verifier); + return result; + } +} diff --git a/src/ModVerify/Verifiers/NameBasedEqualityComparer.cs b/src/ModVerify/Verifiers/Utilities/NameBasedEqualityComparer.cs similarity index 94% rename from src/ModVerify/Verifiers/NameBasedEqualityComparer.cs rename to src/ModVerify/Verifiers/Utilities/NameBasedEqualityComparer.cs index 01f396f..1a44506 100644 --- a/src/ModVerify/Verifiers/NameBasedEqualityComparer.cs +++ b/src/ModVerify/Verifiers/Utilities/NameBasedEqualityComparer.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace AET.ModVerify.Verifiers; +namespace AET.ModVerify.Verifiers.Utilities; internal sealed class NameBasedEqualityComparer : IEqualityComparer, IEqualityComparer { diff --git a/src/ModVerify/Verifiers/VerifierErrorCodes.cs b/src/ModVerify/Verifiers/VerifierErrorCodes.cs index 57d3209..04a900d 100644 --- a/src/ModVerify/Verifiers/VerifierErrorCodes.cs +++ b/src/ModVerify/Verifiers/VerifierErrorCodes.cs @@ -10,14 +10,19 @@ public static class VerifierErrorCodes public const string GenericExceptionErrorCode = "MV00"; - public const string FileCorrupt = "ENG00"; + public const string BinaryFileCorrupt = "BIN00"; public const string FileNotFound = "FILE00"; public const string FilePathTooLong = "FILE01"; public const string InvalidFilePath = "FILE02"; - public const string DuplicateFound = "DUP00"; + public const string Duplicate = "DUP00"; + public const string MissingXRef = "XREF00"; + + public const string NameTooLong = "NAME00"; + + public const string MissingPreset = "SFX00"; public const string SampleNotPCM = "WAV00"; public const string SampleNotMono = "WAV01"; @@ -36,4 +41,6 @@ public static class VerifierErrorCodes public const string XmlDataBeforeHeader = "XML08"; public const string XmlMissingNode = "XML09"; public const string XmlUnsupportedTag = "XML10"; + public const string XmlElementsInTag = "XML11"; + public const string XmlUnexceptedElementName = "XML12"; } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/SfxEvent.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/SfxEvent.cs index aed53f2..43f437d 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/SfxEvent.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/SfxEvent.cs @@ -1,11 +1,14 @@ using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; using System.Linq; using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.Xml; using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Data; namespace PG.StarWarsGame.Engine.Audio.Sfx; +[DebuggerDisplay("{Name}")] public sealed class SfxEvent : NamedXmlObject { private byte _minVolume = DefaultMinVolume; @@ -25,11 +28,11 @@ public sealed class SfxEvent : NamedXmlObject public const byte MaxPan2dValue = 100; public const byte MinPriorityValue = 1; public const byte MaxPriorityValue = 5; - public const byte MaxProbability = 100; - public const sbyte MinMaxInstances = 0; - public const sbyte InfinitivePlayCount = -1; - public const float MinLoopSeconds = 0.0f; - public const float MinVolumeSaturation = 0.0f; + public const byte MaxProbabilityValue = 100; + public const sbyte MinMaxInstancesValue = 0; + public const sbyte InfinitivePlayCountValue = -1; + public const float MinLoopSecondsValue = 0.0f; + public const float MinVolumeSaturationValue = 0.0f; // Default values which are not the default value of the type public const byte DefaultPriority = 3; @@ -45,6 +48,11 @@ public sealed class SfxEvent : NamedXmlObject public const byte DefaultMaxPan2d = 50; public const float DefaultVolumeSaturationDistance = 300.0f; + internal readonly List PreSamplesInternal = new(); + internal readonly List SamplesInternal = new(); + internal readonly List PostSamplesInternal = new(); + internal readonly List LocalizedTextIDsInternal = new(); + public bool IsPreset { get; internal set; } public bool Is3D { get; internal set; } = DefaultIs3d; @@ -69,14 +77,14 @@ public sealed class SfxEvent : NamedXmlObject public IEnumerable AllSamples => PreSamples.Concat(Samples).Concat(PostSamples); - public IReadOnlyList PreSamples { get; internal set; } = []; - - public IReadOnlyList Samples { get; internal set; } = []; + public IReadOnlyList PreSamples { get; } - public IReadOnlyList PostSamples { get; internal set; } = []; + public IReadOnlyList Samples { get; } - public IReadOnlyList LocalizedTextIDs { get; internal set; } = []; + public IReadOnlyList PostSamples { get; } + public IReadOnlyList LocalizedTextIDs { get; } + public byte Priority { get; internal set; } = DefaultPriority; public byte Probability { get; internal set; } = DefaultProbability; @@ -161,9 +169,13 @@ public uint MaxPostdelay internal SfxEvent(string name, Crc32 nameCrc, XmlLocationInfo location) : base(name, nameCrc, location) { + PreSamples = new ReadOnlyCollection(PreSamplesInternal); + Samples = new ReadOnlyCollection(SamplesInternal); + PostSamples = new ReadOnlyCollection(PostSamplesInternal); + LocalizedTextIDs = new ReadOnlyCollection(LocalizedTextIDsInternal); } - internal override void CoerceValues() + internal void FixupValues() { AdjustMinMaxValues(ref _minVolume, ref _maxVolume); AdjustMinMaxValues(ref _minPitch, ref _maxPitch); @@ -187,6 +199,8 @@ internal override void CoerceValues() */ public void ApplyPreset(SfxEvent preset) { + Preset = preset; + Is3D = preset.Is3D; Is2D = preset.Is2D; IsGui = preset.IsGui; @@ -194,13 +208,11 @@ public void ApplyPreset(SfxEvent preset) IsUnitResponseVo = preset.IsUnitResponseVo; IsAmbientVo = preset.IsAmbientVo; IsLocalized = preset.IsLocalized; - Preset = preset; - UsePresetName = preset.Name; PlaySequentially = preset.PlaySequentially; - PreSamples = preset.PreSamples; - Samples = preset.Samples; - PostSamples = preset.PostSamples; - LocalizedTextIDs = preset.LocalizedTextIDs; + SetList(PreSamplesInternal, preset.PreSamplesInternal); + SetList(SamplesInternal, preset.SamplesInternal); + SetList(PostSamplesInternal, preset.PostSamplesInternal); + SetList(LocalizedTextIDsInternal, preset.LocalizedTextIDsInternal); Priority = preset.Priority; Probability = preset.Probability; PlayCount = preset.PlayCount; @@ -219,6 +231,13 @@ public void ApplyPreset(SfxEvent preset) MaxPitch = preset.MaxPitch; MinPan2D = preset.MinPan2D; MaxPan2D = preset.MaxPan2D; + return; + + static void SetList(List target, List source) + { + target.Clear(); + target.AddRange(source); + } } private static void AdjustMinMaxValues(ref byte minValue, ref byte maxValue) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/SfxEventGameManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/SfxEventGameManager.cs index b037939..99f1c31 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/SfxEventGameManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/SfxEventGameManager.cs @@ -7,11 +7,14 @@ using PG.StarWarsGame.Engine.ErrorReporting; using PG.StarWarsGame.Engine.IO.Repositories; using PG.StarWarsGame.Engine.Localization; -using PG.StarWarsGame.Engine.Xml.Parsers; +using PG.StarWarsGame.Engine.Xml; namespace PG.StarWarsGame.Engine.Audio.Sfx; -internal class SfxEventGameManager(GameRepository repository, GameEngineErrorReporterWrapper errorReporter, IServiceProvider serviceProvider) +internal class SfxEventGameManager( + GameRepository repository, + GameEngineErrorReporterWrapper errorReporter, + IServiceProvider serviceProvider) : GameManagerBase(repository, errorReporter, serviceProvider), ISfxEventGameManager { public IEnumerable InstalledLanguages { get; private set; } = []; @@ -24,42 +27,20 @@ protected override async Task InitializeCoreAsync(CancellationToken token) Logger?.LogInformation("Parsing SFXEvents..."); - var contentParser = new XmlContainerContentParser(ServiceProvider, ErrorReporter); - contentParser.XmlParseError += OnParseError; - try - { - await Task.Run(() => contentParser.ParseEntriesFromFileListXml( - "DATA\\XML\\SFXEventFiles.XML", - GameRepository, - "DATA\\XML", - NamedEntries, - VerifyFilePathLength), - token); - } - finally - { - contentParser.XmlParseError -= OnParseError; - } - } - - private void OnParseError(object sender, XmlContainerParserErrorEventArgs e) - { - if (e.ErrorInXmlFileList || e.HasException) - { - e.Continue = false; - ErrorReporter.Report(new InitializationError + var contentParser = new PetroglyphStarWarsGameXmlParser(GameRepository, + new PetroglyphStarWarsGameXmlParseSettings { GameManager = ToString(), - Message = GetMessage(e) - }); - } - } - - private static string GetMessage(XmlContainerParserErrorEventArgs errorEventArgs) - { - if (errorEventArgs.HasException) - return $"Error while parsing SFXEvent XML file '{errorEventArgs.File}': {errorEventArgs.Exception.Message}"; - return "Could not find SFXEventFiles.xml"; + InvalidObjectXmlFailsInitialization = true, + InvalidFilesListXmlFailsInitialization = true + }, ServiceProvider, ErrorReporter); + + await Task.Run(() => contentParser.ParseEntriesFromFileListXml( + "DATA\\XML\\SFXEventFiles.XML", + "DATA\\XML", + NamedEntries, + VerifyFilePathLength), + token); } private void VerifyFilePathLength(string filePath) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager.cs index 0b8687e..a012a50 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager.cs @@ -1,27 +1,21 @@ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.CommandBar.Components; -using PG.StarWarsGame.Engine.CommandBar.Xml; using PG.StarWarsGame.Engine.ErrorReporting; using PG.StarWarsGame.Engine.GameConstants; using PG.StarWarsGame.Engine.IO.Repositories; using PG.StarWarsGame.Engine.Rendering; using PG.StarWarsGame.Engine.Rendering.Font; -using PG.StarWarsGame.Engine.Xml.Parsers; -using PG.StarWarsGame.Files.Binary; using PG.StarWarsGame.Files.MTD.Files; using PG.StarWarsGame.Files.MTD.Services; using System; using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using AnakinRaW.CommonUtilities.Collections; +using System.Numerics; +using PG.StarWarsGame.Engine.GameObjects; namespace PG.StarWarsGame.Engine.CommandBar; -internal class CommandBarGameManager( +internal partial class CommandBarGameManager( GameRepository repository, PGRender pgRender, IGameConstants gameConstants, @@ -33,16 +27,14 @@ internal class CommandBarGameManager( private readonly ICrc32HashingService _hashingService = serviceProvider.GetRequiredService(); private readonly IMtdFileService _mtdFileService = serviceProvider.GetRequiredService(); private readonly Dictionary _groups = new(); - private readonly PGRender _pgRender = pgRender; - private bool _megaTextureExists; private FontData? _defaultFont; public ICollection Components => Entries; public IReadOnlyDictionary Groups => _groups; - public FontData? DefaultFont + public FontData? DefaultFont { get { @@ -56,7 +48,7 @@ private set } } - public IMtdFile? MegaTextureFile + public IMtdFile? MtdFile { get { @@ -70,224 +62,39 @@ private set } } - protected override async Task InitializeCoreAsync(CancellationToken token) + public string? MegaTextureFileName { - Logger?.LogInformation("Creating command bar components..."); - - var contentParser = new XmlContainerContentParser(ServiceProvider, ErrorReporter); - contentParser.XmlParseError += OnParseError; - - var parsedCommandBarComponents = new FrugalValueListDictionary(); - - try - { - await Task.Run(() => contentParser.ParseEntriesFromFileListXml( - "DATA\\XML\\CommandBarComponentFiles.XML", - GameRepository, - ".\\DATA\\XML", - parsedCommandBarComponents, - VerifyFilePathLength), - token); - } - finally - { - contentParser.XmlParseError -= OnParseError; - } - - // Create Scene - // Create Camera - // Resize(true) - - foreach (var parsedCommandBarComponent in parsedCommandBarComponents.Values) + get { - var component = CommandBarBaseComponent.Create(parsedCommandBarComponent, ErrorReporter); - if (component is not null) - { - var crc = _hashingService.GetCrc32(component.Name, PGConstants.DefaultPGEncoding); - NamedEntries.Add(crc, component); - } + ThrowIfNotInitialized(); + return field; } - - SetComponentGroup(Components); - SetMegaTexture(); - SetDefaultFont(); - - LinkComponentsToShell(); - LinkComponentsWithActions(); - } - - private void LinkComponentsWithActions() - { - var nameLookup = SupportedCommandBarComponentData.GetComponentIdsForEngine(GameRepository.EngineType); - - foreach (var idPair in nameLookup) + private set { - var crc = _hashingService.GetCrc32(idPair.Value, PGConstants.DefaultPGEncoding); - if (NamedEntries.TryGetFirstValue(crc, out var component)) - component.Id = idPair.Key; + ThrowIfAlreadyInitialized(); + field = value; } } - private void LinkComponentsToShell() - { - if (!Groups.TryGetValue(CommandBarConstants.ShellGroupName, out var shellGroup)) - return; - var modelCache = new Dictionary(); - foreach (var component in Components) - { - if (component.Type == CommandBarComponentType.Shell) - continue; + public Vector3 CommandBarScale { get; } - foreach (var shellComponent in shellGroup.Components) - { - if (LinkToShell(component, shellComponent as CommandBarShellComponent, modelCache)) - break; - } - } + public Vector3 CommandBarOffset { get; internal set; } - foreach (var model in modelCache.Values) - model?.Dispose(); - } - - private bool LinkToShell( - CommandBarBaseComponent component, - CommandBarShellComponent? shell, - IDictionary modelCache) + public bool IconExists(GameObject gameObject) { - if (shell is null) - { - ErrorReporter.Assert( - EngineAssert.FromNullOrEmpty( - [component.Name], $"Cannot link component '{component}' because shell component is null.")); - return false; - } - - var componentName = component.Name; - if (string.IsNullOrEmpty(componentName)) - return false; - - var modelPath = shell.ModelPath; - if (string.IsNullOrEmpty(modelPath)) - return false; - - if (!modelCache.TryGetValue(shell.Name, out var model)) - { - model = _pgRender.LoadModelAndAnimations(modelPath.AsSpan(), null, true); - modelCache.Add(shell.Name, model); - } - - if (model is null) - { - ErrorReporter.Assert( - EngineAssert.FromNullOrEmpty( - [$"component='{component.Name}'", $"shell='{shell.Name}'"], - $"Cannot link component '{componentName}' to shell '{shell.Name}' because model '{modelPath}' could not be loaded.")); - return false; - } + ThrowIfNotInitialized(); + if (gameObject == null) + throw new ArgumentNullException(nameof(gameObject)); - if (!model.IsModel) - { - ErrorReporter.Assert( - EngineAssert.FromNullOrEmpty( - [$"component='{component.Name}'", $"shell='{shell.Name}'"], - $"Cannot link component '{componentName}' to shell '{shell.Name}' because the loaded file '{modelPath}' is not a model.")); + if (MtdFile is null) return false; - } - var boneIndex = model.IndexOfBone(componentName); - - if (boneIndex == -1) + if (string.IsNullOrEmpty(gameObject.IconName)) return false; - component.Bone = boneIndex; - component.ParentShell = shell; - return true; - } - private void SetDefaultFont() - { - // The code is only triggered iff at least one Text CommandbarBar component existed - if (Components.FirstOrDefault(x => x is CommandBarTextComponent or CommandBarTextButtonComponent) is null) - return; + var crc = _hashingService.GetCrc32Upper(gameObject.IconName, PGConstants.DefaultPGEncoding); - if (_defaultFont is null) - { - // TODO: From GameConstants - 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}")); - DefaultFont = font; - } - } - - private void SetMegaTexture() - { - // The code is only triggered iff at least one Shell CommandbarBar component existed - if (Components.FirstOrDefault(x => x is CommandBarShellComponent) is null) - return; - // Note: The tag is not used by the engine - var mtdPath = FileSystem.Path.Combine("DATA\\ART\\TEXTURES", $"{CommandBarConstants.MegaTextureBaseName}.mtd"); - using var megaTexture = GameRepository.TryOpenFile(mtdPath); - - try - { - MegaTextureFile = megaTexture is null ? null : _mtdFileService.Load(megaTexture); - } - catch (BinaryCorruptedException e) - { - var message = $"Failed to load MTD file '{mtdPath}': {e.Message}"; - Logger?.LogError(e, message); - ErrorReporter.Assert(EngineAssert.Create(EngineAssertKind.CorruptBinary, mtdPath, [], message)); - } - _megaTextureExists = GameRepository.TextureRepository.FileExists($"{CommandBarConstants.MegaTextureBaseName}.tga"); - } - - private void SetComponentGroup(IEnumerable components) - { - var groupData = components - .Where(x => !string.IsNullOrEmpty(x.XmlData.Group)) - .GroupBy(x => x.XmlData.Group!, StringComparer.Ordinal); - - foreach (var grouping in groupData) - { - var group = new CommandBarComponentGroup(grouping.Key, grouping); - _groups.Add(grouping.Key, group); - foreach (var component in grouping) - component.Group = group; - } - } - - private void OnParseError(object sender, XmlContainerParserErrorEventArgs e) - { - if (e.ErrorInXmlFileList || e.HasException) - { - e.Continue = false; - ErrorReporter.Report(new InitializationError - { - GameManager = ToString(), - Message = GetMessage(e) - }); - } - } - - private static string GetMessage(XmlContainerParserErrorEventArgs errorEventArgs) - { - if (errorEventArgs.HasException) - return $"Error while parsing CommandBar XML file '{errorEventArgs.File}': {errorEventArgs.Exception.Message}"; - return "Could not find CommandBarComponentFiles.xml"; - } - - private void VerifyFilePathLength(string filePath) - { - if (filePath.Length > PGConstants.MaxCommandBarDatabaseFileName) - { - ErrorReporter.Report(new InitializationError - { - GameManager = ToString(), - Message = $"CommandBar file '{filePath}' is longer than {PGConstants.MaxCommandBarDatabaseFileName} characters." - }); - } + return MtdFile.Content.Contains(crc); } -} +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager_Initialization.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager_Initialization.cs new file mode 100644 index 0000000..4835ed3 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager_Initialization.cs @@ -0,0 +1,243 @@ +using AnakinRaW.CommonUtilities.Collections; +using Microsoft.Extensions.Logging; +using PG.Commons.Hashing; +using PG.StarWarsGame.Engine.CommandBar.Components; +using PG.StarWarsGame.Engine.CommandBar.Xml; +using PG.StarWarsGame.Engine.ErrorReporting; +using PG.StarWarsGame.Engine.Rendering; +using PG.StarWarsGame.Engine.Xml; +using PG.StarWarsGame.Files.Binary; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Threading; +using System.Threading.Tasks; + +namespace PG.StarWarsGame.Engine.CommandBar; + +internal partial class CommandBarGameManager +{ + protected override async Task InitializeCoreAsync(CancellationToken token) + { + Logger?.LogInformation("Creating command bar components..."); + + var contentParser = new PetroglyphStarWarsGameXmlParser(GameRepository, new PetroglyphStarWarsGameXmlParseSettings + { + GameManager = ToString(), + InvalidObjectXmlFailsInitialization = true, + InvalidFilesListXmlFailsInitialization = true + }, ServiceProvider, ErrorReporter); + + var parsedCommandBarComponents = new FrugalValueListDictionary(); + + await Task.Run(() => contentParser.ParseEntriesFromFileListXml( + ".\\Data\\XML\\CommandBarComponentFiles.xml", + ".\\DATA\\XML\\", + parsedCommandBarComponents, + VerifyFilePathLength), + token); + + CommandBarOffset = new Vector3(-0.5f, -0.5f, 0.0f); + + // Create Scene + // Create Camera + // Resize(force: true) + + foreach (var parsedCommandBarComponent in parsedCommandBarComponents.Values) + { + var component = CommandBarBaseComponent.Create(parsedCommandBarComponent, ErrorReporter); + if (component is not null) + { + var crc = _hashingService.GetCrc32(component.Name, PGConstants.DefaultPGEncoding); + NamedEntries.Add(crc, component); + } + + if (component is CommandBarShellComponent shellComponent) + SetModelTransform(shellComponent); + } + + SetComponentGroup(Components); + SetMegaTexture(); + SetDefaultFont(); + + LinkComponentsToShell(); + LinkComponentsWithActions(); + + + // CommandBarClass::Set_Encyclopedia_Delay_Time(this); + // CommandBarClass::Find_Neighbors(this); + + // CommandBarClass::Load_Hero_Particles(this); + // CommandBarClass::Load_Corruption_Particle(this); + } + + private void SetModelTransform(CommandBarShellComponent shellComponent) + { + if (string.IsNullOrEmpty(shellComponent.ModelPath) || + !GameRepository.ModelRepository.FileExists(shellComponent.ModelPath)) + return; + shellComponent.SetOffsetAndScale(CommandBarOffset, CommandBarScale); + } + + private void LinkComponentsWithActions() + { + var nameLookup = SupportedCommandBarComponentData.GetComponentIdsForEngine(GameRepository.EngineType); + foreach (var idPair in nameLookup) + { + // The engine does not uppercase the name here + var crc = _hashingService.GetCrc32(idPair.Value, PGConstants.DefaultPGEncoding); + if (NamedEntries.TryGetFirstValue(crc, out var component)) + { + // NB: Currently we do not have "action" + // but we keep the original method name 'LinkComponentsWithActions' + component.Id = idPair.Key; + } + } + } + + private void LinkComponentsToShell() + { + if (!Groups.TryGetValue(CommandBarConstants.ShellGroupName, out var shellGroup)) + return; + + var modelCache = new Dictionary(); + foreach (var component in Components) + { + if (component.Type == CommandBarComponentType.Shell) + continue; + + foreach (var shellComponent in shellGroup.Components) + { + if (LinkToShell(component, shellComponent as CommandBarShellComponent, modelCache)) + break; + } + } + + foreach (var model in modelCache.Values) + model?.Dispose(); + } + + private bool LinkToShell( + CommandBarBaseComponent component, + CommandBarShellComponent? shell, + IDictionary modelCache) + { + if (shell is null) + { + ErrorReporter.Assert( + EngineAssert.FromNullOrEmpty( + [component.Name], $"Cannot link component '{component}' because shell component is null.")); + return false; + } + + var componentName = component.Name; + if (string.IsNullOrEmpty(componentName)) + return false; + + var modelPath = shell.ModelPath; + if (string.IsNullOrEmpty(modelPath)) + return false; + + if (!modelCache.TryGetValue(shell.Name, out var model)) + { + model = pgRender.LoadModelAndAnimations(modelPath.AsSpan(), null, true); + modelCache.Add(shell.Name, model); + } + + if (model is null) + { + ErrorReporter.Assert( + EngineAssert.FromNullOrEmpty( + [$"component='{component.Name}'", $"shell='{shell.Name}'"], + $"Cannot link component '{componentName}' to shell '{shell.Name}' because model '{modelPath}' could not be loaded.")); + return false; + } + + if (!model.IsModel) + { + ErrorReporter.Assert( + EngineAssert.FromNullOrEmpty( + [$"component='{component.Name}'", $"shell='{shell.Name}'"], + $"Cannot link component '{componentName}' to shell '{shell.Name}' because the loaded file '{modelPath}' is not a model.")); + return false; + } + + var boneIndex = model.IndexOfBone(componentName); + + if (boneIndex == -1) + return false; + component.Bone = boneIndex; + component.ParentShell = shell; + return true; + } + + private void SetDefaultFont() + { + // The code is only triggered iff at least one Text CommandbarBar component existed + if (Components.FirstOrDefault(x => x is CommandBarTextComponent or CommandBarTextButtonComponent) is null) + return; + + if (_defaultFont is null) + { + // TODO: From GameConstants + 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}")); + DefaultFont = font; + } + } + + private void SetMegaTexture() + { + // The code is only triggered iff at least one Shell CommandbarBar component existed + if (Components.FirstOrDefault(x => x is CommandBarShellComponent) is null) + return; + // Note: The tag is not used by the engine + var mtdPath = FileSystem.Path.Combine("DATA\\ART\\TEXTURES", $"{CommandBarConstants.MegaTextureBaseName}.mtd"); + using var megaTexture = GameRepository.TryOpenFile(mtdPath); + + try + { + MtdFile = megaTexture is null ? null : _mtdFileService.Load(megaTexture); + } + catch (BinaryCorruptedException e) + { + var message = $"Failed to load MTD file '{mtdPath}': {e.Message}"; + Logger?.LogError(e, message); + ErrorReporter.Assert(EngineAssert.Create(EngineAssertKind.CorruptBinary, mtdPath, [], message)); + } + + GameRepository.TextureRepository.FileExists($"{CommandBarConstants.MegaTextureBaseName}.tga", false, out _, out var actualFilePath); + MegaTextureFileName = FileSystem.Path.GetFileName(actualFilePath); + } + + private void SetComponentGroup(IEnumerable components) + { + var groupData = components + .Where(x => !string.IsNullOrEmpty(x.XmlData.Group)) + .GroupBy(x => x.XmlData.Group!, StringComparer.Ordinal); + + foreach (var grouping in groupData) + { + var group = new CommandBarComponentGroup(grouping.Key, grouping); + _groups.Add(grouping.Key, group); + foreach (var component in grouping) + component.Group = group; + } + } + + private void VerifyFilePathLength(string filePath) + { + if (filePath.Length > PGConstants.MaxCommandBarDatabaseFileName) + { + ErrorReporter.Report(new InitializationError + { + GameManager = ToString(), + Message = $"CommandBar file '{filePath}' is longer than {PGConstants.MaxCommandBarDatabaseFileName} characters." + }); + } + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/Components/CommandBarShellComponent.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/Components/CommandBarShellComponent.cs index 799040e..f649d80 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/Components/CommandBarShellComponent.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/Components/CommandBarShellComponent.cs @@ -1,4 +1,7 @@ using PG.StarWarsGame.Engine.CommandBar.Xml; +using System.Numerics; +using PG.StarWarsGame.Engine.Rendering; +using PG.StarWarsGame.Engine.Utilities; namespace PG.StarWarsGame.Engine.CommandBar.Components; @@ -10,10 +13,33 @@ public class CommandBarShellComponent : CommandBarBaseComponent public string? ModelPath { get; } + public Matrix3x4 ModelTransform { get; internal set; } = Matrix3x4.Identity; + public CommandBarShellComponent(CommandBarComponentData xmlData) : base(xmlData) { ModelName = xmlData.ModelName; if (!string.IsNullOrEmpty(ModelName)) ModelPath = $"DATA\\ART\\MODELS\\{ModelName}"; } + + internal void SetOffsetAndScale(Vector3 offset, Vector3 scale) + { + var newOffset = new Vector3(0.0f, 0.0f, 0.0f); + var newScale = new Vector3(1.0f, 1.0f, 1.0f); + if (XmlData.ModelOffsetX) + newOffset.X = offset.X; + if (XmlData.ModelOffsetY) + newOffset.Y = offset.Y; + if (XmlData.ScaleModelX) + newScale.X = scale.X; + if (XmlData.ScaleModelY) + newScale.Y = scale.Y; + + newOffset.X = PGMath.Floor(newOffset.X) + 0.5f; + newOffset.Y = PGMath.Floor(newOffset.Y) + 0.5f; + + ModelTransform = Matrix3x4.Identity + * Matrix3x4.Scale(newScale) + * Matrix3x4.CreateTranslation(newOffset); + } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/ICommandBarGameManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/ICommandBarGameManager.cs index 665d217..6c2749d 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/ICommandBarGameManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/ICommandBarGameManager.cs @@ -1,14 +1,19 @@ using System.Collections.Generic; using PG.StarWarsGame.Engine.CommandBar.Components; +using PG.StarWarsGame.Engine.GameObjects; using PG.StarWarsGame.Files.MTD.Files; namespace PG.StarWarsGame.Engine.CommandBar; public interface ICommandBarGameManager : IGameManager { - IMtdFile? MegaTextureFile { get; } + IMtdFile? MtdFile { get; } + + string? MegaTextureFileName { get; } ICollection Components { get; } IReadOnlyDictionary Groups { get; } + + bool IconExists(GameObject gameObject); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/SupportedCommandBarComponentData.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/SupportedCommandBarComponentData.cs index 7b6eeb6..388599c 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/SupportedCommandBarComponentData.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/SupportedCommandBarComponentData.cs @@ -5,6 +5,8 @@ namespace PG.StarWarsGame.Engine.CommandBar; public static class SupportedCommandBarComponentData { + // Unfortunately we cannot use EnumConversionDictionary, because EaW and use different enum values + // for the same components, so we need to maintain separate dictionaries for each engine. public static IReadOnlyDictionary GetComponentIdsForEngine(GameEngineType engineType) { return engineType switch diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/Xml/CommandBarComponentData.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/Xml/CommandBarComponentData.cs index 057745e..659130a 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/Xml/CommandBarComponentData.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/Xml/CommandBarComponentData.cs @@ -1,15 +1,14 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Numerics; using PG.Commons.Hashing; using PG.Commons.Numerics; -using PG.StarWarsGame.Engine.Xml; using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Data; namespace PG.StarWarsGame.Engine.CommandBar.Xml; -public sealed class CommandBarComponentData(string name, Crc32 crc, XmlLocationInfo location) : NamedXmlObject(name, crc, location) +public sealed class CommandBarComponentData : NamedXmlObject { public const float DefaultScale = 1.0f; public const float DefaultBlinkRate = 0.2f; @@ -17,18 +16,31 @@ public sealed class CommandBarComponentData(string name, Crc32 crc, XmlLocationI public static readonly Vector2 DefaultOffsetWidescreenValue = new(9.9999998e17f, 9.9999998e17f); public static readonly Vector4Int WhiteColor = new(255, 255, 255, 255); - public IReadOnlyList SelectedTextureNames { get; internal set; } = []; - public IReadOnlyList BlankTextureNames { get; internal set; } = []; - public IReadOnlyList IconAlternateTextureNames { get; internal set; } = []; - public IReadOnlyList MouseOverTextureNames { get; internal set; } = []; - public IReadOnlyList BarTextureNames { get; internal set; } = []; - public IReadOnlyList BarOverlayNames { get; internal set; } = []; - public IReadOnlyList AlternateFontNames { get; internal set; } = []; - public IReadOnlyList TooltipTexts { get; internal set; } = []; - public IReadOnlyList LowerEffectTextureNames { get; internal set; } = []; - public IReadOnlyList UpperEffectTextureNames { get; internal set; } = []; - public IReadOnlyList OverlayTextureNames { get; internal set; } = []; - public IReadOnlyList Overlay2TextureNames { get; internal set; } = []; + internal readonly List SelectedTextureNamesInternal = []; + internal readonly List BlankTextureNamesInternal = []; + internal readonly List IconAlternateTextureNamesInternal = []; + internal readonly List MouseOverTextureNamesInternal = []; + internal readonly List BarTextureNamesInternal = []; + internal readonly List BarOverlayNamesInternal = []; + internal readonly List AlternateFontNamesInternal = []; + internal readonly List TooltipTextsInternal = []; + internal readonly List LowerEffectTextureNamesInternal = []; + internal readonly List UpperEffectTextureNamesInternal = []; + internal readonly List OverlayTextureNamesInternal = []; + internal readonly List Overlay2TextureNamesInternal = []; + + public IReadOnlyList SelectedTextureNames { get; } + public IReadOnlyList BlankTextureNames { get; } + public IReadOnlyList IconAlternateTextureNames { get; } + public IReadOnlyList MouseOverTextureNames { get; } + public IReadOnlyList BarTextureNames { get; } + public IReadOnlyList BarOverlayNames { get; } + public IReadOnlyList AlternateFontNames { get; } + public IReadOnlyList TooltipTexts { get; } + public IReadOnlyList LowerEffectTextureNames { get; } + public IReadOnlyList UpperEffectTextureNames { get; } + public IReadOnlyList OverlayTextureNames { get; } + public IReadOnlyList Overlay2TextureNames { get; } public string? IconTextureName { get; internal set; } public string? DisabledTextureName { get; internal set; } @@ -128,22 +140,26 @@ public sealed class CommandBarComponentData(string name, Crc32 crc, XmlLocationI public Vector4Int? TextColor2 { get; internal set; } public Vector4Int? MaxBarColor { get; internal set; } = WhiteColor; - internal override void CoerceValues() + public CommandBarComponentData(string name, Crc32 crc, XmlLocationInfo location) : base(name, crc, location) { - base.CoerceValues(); - if (AlternateFontNames.Count == 0) - return; - var newFontNames = new string[AlternateFontNames.Count]; - for (var i = 0; i < AlternateFontNames.Count; i++) - { - var current = AlternateFontNames[i]; + SelectedTextureNames = new ReadOnlyCollection(SelectedTextureNamesInternal); + BlankTextureNames = new ReadOnlyCollection(BlankTextureNamesInternal); + IconAlternateTextureNames = new ReadOnlyCollection(IconAlternateTextureNamesInternal); + MouseOverTextureNames = new ReadOnlyCollection(MouseOverTextureNamesInternal); + BarTextureNames = new ReadOnlyCollection(BarTextureNamesInternal); + BarOverlayNames = new ReadOnlyCollection(BarOverlayNamesInternal); + AlternateFontNames = new ReadOnlyCollection(AlternateFontNamesInternal); + TooltipTexts = new ReadOnlyCollection(TooltipTextsInternal); + LowerEffectTextureNames = new ReadOnlyCollection(LowerEffectTextureNamesInternal); + UpperEffectTextureNames = new ReadOnlyCollection(UpperEffectTextureNamesInternal); + OverlayTextureNames = new ReadOnlyCollection(OverlayTextureNamesInternal); + Overlay2TextureNames = new ReadOnlyCollection(Overlay2TextureNamesInternal); + } - if (current.AsSpan().IndexOf('_') != -1) - newFontNames[i] = current.Replace('_', ' '); - else - newFontNames[i] = current; - } - AlternateFontNames = new ReadOnlyCollection(newFontNames); + internal void FixupValues() + { + for (var i = 0; i < AlternateFontNamesInternal.Count; i++) + AlternateFontNamesInternal[i] = AlternateFontNamesInternal[i].Replace('_', ' '); } } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Commons/MultiNameReferenceList.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Commons/MultiNameReferenceList.cs new file mode 100644 index 0000000..ff828a4 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Commons/MultiNameReferenceList.cs @@ -0,0 +1,57 @@ +using System.Collections; +using System.Collections.Generic; + +namespace PG.StarWarsGame.Engine.Commons; + +public class MultiNameReferenceList : IReadOnlyList +{ + private bool _replace; + private readonly List _list = []; + + public string this[int index] => _list[index]; + + public int Count => _list.Count; + + public MultiNameReferenceList() + { + } + + public MultiNameReferenceList(MultiNameReferenceList list) + { + foreach (var name in list) + _list.Add(name); + + _replace = true; + } + + internal void AddRange(IEnumerable names) + { + if (_replace) + Clear(); + _replace = false; + _list.AddRange(names); + } + + internal void Add(string name) + { + if (_replace) + Clear(); + _replace = false; + _list.Add(name); + } + + internal void Clear() + { + _list.Clear(); + } + + public IEnumerator GetEnumerator() + { + return _list.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/EngineAssert.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/EngineAssert.cs index 5cc4c49..38a07a9 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/EngineAssert.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/EngineAssert.cs @@ -9,7 +9,7 @@ namespace PG.StarWarsGame.Engine.ErrorReporting; public sealed class EngineAssert { - private static readonly string ThisNameSpace = typeof(EngineAssert).Namespace!; + private static readonly string ThisNamespace = typeof(EngineAssert).Namespace!; private const string NullLiteral = "NULL"; public string Value { get; } @@ -66,7 +66,7 @@ internal static EngineAssert Create(EngineAssertKind kind, object? value, IEnume { var frame = trace.GetFrame(i); var method = frame.GetMethod(); - if (method.DeclaringType is null || method.DeclaringType.Namespace?.Equals(ThisNameSpace) == false) + if (method.DeclaringType is null || method.DeclaringType.Namespace?.Equals(ThisNamespace) == false) return frame; } return null; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/EngineAssertKind.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/EngineAssertKind.cs index e3a2d69..f2d1ccd 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/EngineAssertKind.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/EngineAssertKind.cs @@ -7,4 +7,5 @@ public enum EngineAssertKind InvalidValue, CorruptBinary, FileNotFound, + DuplicateEntry } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/GameEngineErrorReporterWrapper.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/GameEngineErrorReporterWrapper.cs index a636aba..a8280cc 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/GameEngineErrorReporterWrapper.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/GameEngineErrorReporterWrapper.cs @@ -1,50 +1,26 @@ using System; using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; namespace PG.StarWarsGame.Engine.ErrorReporting; -internal sealed class GameEngineErrorReporterWrapper : XmlErrorReporter, IGameEngineErrorReporter +internal sealed class GameEngineErrorReporterWrapper(IGameEngineErrorReporter? errorReporter) + : XmlErrorReporter, IGameEngineErrorReporter { internal event EventHandler? InitializationError; - private readonly IGameEngineErrorReporter? _errorReporter; - - public GameEngineErrorReporterWrapper(IGameEngineErrorReporter? errorReporter) - { - if (errorReporter is null) - return; - _errorReporter = errorReporter; - } - - public void Report(XmlError error) + public override void Report(XmlError error) { - _errorReporter?.Report(error); + errorReporter?.Report(error); } public void Report(InitializationError error) { InitializationError?.Invoke(this, error); - _errorReporter?.Report(error); + errorReporter?.Report(error); } public void Assert(EngineAssert assert) { - _errorReporter?.Assert(assert); - } - - public override void Report(IPetroglyphXmlParser parser, XmlParseErrorEventArgs error) - { - if (_errorReporter is null) - return; - - Report(new XmlError - { - FileLocation = error.Location, - Parser = parser, - Message = error.Message, - ErrorKind = error.ErrorKind, - Element = error.Element - }); + errorReporter?.Assert(assert); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/IGameEngineErrorReporter.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/IGameEngineErrorReporter.cs index 1294fd1..933d3b5 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/IGameEngineErrorReporter.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/IGameEngineErrorReporter.cs @@ -1,9 +1,9 @@ -namespace PG.StarWarsGame.Engine.ErrorReporting; +using PG.StarWarsGame.Files.XML.ErrorHandling; -public interface IGameEngineErrorReporter -{ - void Report(XmlError error); +namespace PG.StarWarsGame.Engine.ErrorReporting; +public interface IGameEngineErrorReporter : IXmlParserErrorReporter +{ void Report(InitializationError error); void Assert(EngineAssert assert); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/XmlError.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/XmlError.cs deleted file mode 100644 index 83e6e9f..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/XmlError.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Xml.Linq; -using PG.StarWarsGame.Files.XML; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Engine.ErrorReporting; - -public sealed class XmlError -{ - public required XmlLocationInfo FileLocation { get; init; } - - public required IPetroglyphXmlParser Parser { get; init; } - - public XElement? Element { get; init; } - - public required XmlParseErrorKind ErrorKind { get; init; } - - public required string Message { get; init; } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/FocHardcodedConstants.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/FocHardcodedConstants.cs deleted file mode 100644 index d7eb4af..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/FocHardcodedConstants.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; - -namespace PG.StarWarsGame.Engine; - -public static class FocHardcodedConstants -{ - ///

- /// These models / particles are hardcoded into StarWarsG.exe. - /// - public static IList HardcodedModels { get; } = new List - { - "i_tutorial_arrow.alo", - "p_hero_empire_fx.alo", - "i_tactical_corrupt.alo", - "p_icon_corrupt.alo", - "w_planet_select_neutral.alo", - "i_game_arrow.alo", - "i_galactic_radar.alo", - "W_TextScroll.alo" - }; -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/GameConstants.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/GameConstants.cs index 1c36061..3874e07 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/GameConstants.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/GameConstants.cs @@ -6,7 +6,10 @@ namespace PG.StarWarsGame.Engine.GameConstants; -internal class GameConstants(GameRepository repository, GameEngineErrorReporterWrapper errorReporter, IServiceProvider serviceProvider) +internal class GameConstants( + GameRepository repository, + GameEngineErrorReporterWrapper errorReporter, + IServiceProvider serviceProvider) : GameManagerBase(repository, errorReporter, serviceProvider), IGameConstants { protected override Task InitializeCoreAsync(CancellationToken token) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/GameConstantsXml.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/GameConstantsXml.cs index e98ed52..5eae395 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/GameConstantsXml.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/GameConstantsXml.cs @@ -1,3 +1,6 @@ -namespace PG.StarWarsGame.Engine.GameConstants; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Data; -public class GameConstantsXml; \ No newline at end of file +namespace PG.StarWarsGame.Engine.GameConstants; + +public class GameConstantsXml(XmlLocationInfo location) : XmlObject(location); \ 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 607121d..c3dd55d 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameManagerBase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameManagerBase.cs @@ -12,7 +12,10 @@ namespace PG.StarWarsGame.Engine; -internal abstract class GameManagerBase(GameRepository repository, GameEngineErrorReporterWrapper errorReporter, IServiceProvider serviceProvider) +internal abstract class GameManagerBase( + GameRepository repository, + GameEngineErrorReporterWrapper errorReporter, + IServiceProvider serviceProvider) : GameManagerBase(repository, errorReporter, serviceProvider), IGameManager { protected readonly FrugalValueListDictionary NamedEntries = new(); @@ -40,11 +43,17 @@ internal abstract class GameManagerBase protected readonly GameEngineErrorReporterWrapper ErrorReporter; public bool IsInitialized => _initialized; + + public GameEngineType EngineType { get; } - protected GameManagerBase(GameRepository repository, GameEngineErrorReporterWrapper errorReporter, IServiceProvider serviceProvider) + protected GameManagerBase( + GameRepository repository, + GameEngineErrorReporterWrapper errorReporter, + IServiceProvider serviceProvider) { GameRepository = repository ?? throw new ArgumentNullException(nameof(repository)); ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + EngineType = repository.EngineType; Logger = serviceProvider.GetService()?.CreateLogger(GetType()); FileSystem = serviceProvider.GetRequiredService(); ErrorReporter = errorReporter ?? throw new ArgumentNullException(nameof(errorReporter)); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObject.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObject.cs index de7ed01..89823df 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObject.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObject.cs @@ -1,26 +1,32 @@ -using System; +using PG.Commons.Hashing; +using PG.StarWarsGame.Engine.Commons; +using PG.StarWarsGame.Engine.Utilities; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Data; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.Xml; -using PG.StarWarsGame.Files.XML; +using System.Diagnostics; namespace PG.StarWarsGame.Engine.GameObjects; +[DebuggerDisplay("{Name} ({ClassificationName})")] public sealed class GameObject : NamedXmlObject { - internal GameObject(string type, string name, Crc32 nameCrc, GameObjectType estimatedType, XmlLocationInfo location) - : base(name, nameCrc, location) - { - Type = type ?? throw new ArgumentNullException(nameof(type)); - EstimatedType = estimatedType; - LandTerrainModelMapping = new ReadOnlyDictionary(InternalLandTerrainModelMapping); - } + internal readonly List<(string terrain, string model)> InternalLandTerrainModelMapping = []; + + internal int Id { get; } + + public int Index { get; } - public string Type { get; } + public string ClassificationName { get; } - public GameObjectType EstimatedType { get; } + public string? VariantOfExistingTypeName { get; internal set; } + public GameObject? VariantOfExistingType { get; internal set; } + + public bool IsLoadingComplete { get; internal set; } + public string? GalacticModel { get; internal set; } public string? DestroyedGalacticModel { get; internal set; } @@ -29,8 +35,6 @@ internal GameObject(string type, string name, Crc32 nameCrc, GameObjectType esti public string? SpaceModel { get; internal set; } - public string? TacticalModel { get; internal set; } - public string? GalacticFleetOverrideModel { get; internal set; } public string? GuiModel { get; internal set; } @@ -39,46 +43,57 @@ internal GameObject(string type, string name, Crc32 nameCrc, GameObjectType esti public string? LandAnimOverrideModel { get; internal set; } - public string? XxxSpaceModeModel { get; internal set; } + public string SpaceAnimOverrideModel { get; internal set; } public string? DamagedSmokeAssetModel { get; internal set; } - public IReadOnlyDictionary LandTerrainModelMapping { get; } + public IReadOnlyList<(string terrain, string model)> LandTerrainModelMappingValues { get; } + + public string? IconName { get; internal set; } + + public MultiNameReferenceList GroundCompanyUnits { get; internal set; } = []; - internal Dictionary InternalLandTerrainModelMapping { get; } = new(StringComparer.OrdinalIgnoreCase); + internal GameObject( + string name, + string classification, + Crc32 nameCrc, + int index, + XmlLocationInfo location) + : base(name, nameCrc, location) + { + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(index), "Index must be greater than 0."); + Index = index; + Id = (int)nameCrc; + ClassificationName = classification ?? throw new ArgumentNullException(nameof(classification)); + LandTerrainModelMappingValues = new ReadOnlyCollection<(string, string)>(InternalLandTerrainModelMapping); + } - /// - /// Gets all model files (including particles) the game object references. - /// - public IEnumerable Models + internal void ApplyBaseType(GameObject baseType) { - get - { - var models = new HashSet(StringComparer.OrdinalIgnoreCase); - AddNotEmpty(models, GalacticModel); - AddNotEmpty(models, DestroyedGalacticModel); - AddNotEmpty(models, LandModel); - AddNotEmpty(models, SpaceModel); - AddNotEmpty(models, TacticalModel); - AddNotEmpty(models, GalacticFleetOverrideModel); - AddNotEmpty(models, GuiModel); - AddNotEmpty(models, ModelName); - AddNotEmpty(models, LandAnimOverrideModel, s => s.EndsWith(".alo", StringComparison.OrdinalIgnoreCase)); - AddNotEmpty(models, XxxSpaceModeModel); - AddNotEmpty(models, DamagedSmokeAssetModel); - foreach (var model in InternalLandTerrainModelMapping.Values) - models.Add(model); - - return models; - } + // The following properties must not be inherited from the base type: + // ID, CRC, Name, Location, IsLoadingComplete, ClassificationName and VariantOfExistingType[Name], LuaScript + + // TODO + GalacticModel = baseType.GalacticModel; + DestroyedGalacticModel = baseType.DestroyedGalacticModel; + LandModel = baseType.LandModel; + SpaceModel = baseType.SpaceModel; + GalacticFleetOverrideModel = baseType.GalacticFleetOverrideModel; + GuiModel = baseType.GuiModel; + ModelName = baseType.ModelName; + LandAnimOverrideModel = baseType.LandAnimOverrideModel; + SpaceAnimOverrideModel = baseType.SpaceAnimOverrideModel; + DamagedSmokeAssetModel = baseType.DamagedSmokeAssetModel; + InternalLandTerrainModelMapping.ClearAddRange(baseType.InternalLandTerrainModelMapping); + IconName = baseType.IconName; + GroundCompanyUnits = new(baseType.GroundCompanyUnits); } - private static void AddNotEmpty(ISet set, string? value, Predicate? predicate = null) + internal void PostLoadFixup() { - if (value is null) - return; - if (predicate is null || predicate(value)) - set.Add(value); + // This method is different than the fixup that is performed after parsing the object. + // IDK why there are two separate fixups. This fixup performs more value coercions } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectType.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectType.cs deleted file mode 100644 index 0b84517..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectType.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace PG.StarWarsGame.Engine.GameObjects; - -public enum GameObjectType -{ - Unknown = 0, - CinematicObject = 1, - Container = 2, - GenericHeroUnit = 3, - GroundBase = 4, - GroundBuildable = 5, - GroundCompany = 6, - GroundInfantry = 7, - GroundStructure = 8, - GroundVehicle = 9, - HeroCompany = 10, - HeroUnit = 11, - IndigenousUnit = 12, - LandBombingUnit = 13, - LandPrimarySkydome = 14, - LandSecondarySkydome = 15, - Marker = 16, - MiscObject = 17, - MobileDefenseUnit = 18, - MultiplayerStructureMarker = 19, - Particle = 20, - Planet = 21, - Projectile = 22, - Prop = 23, - ScriptMarker = 24, - SecondaryStructure = 25, - SlaveCompany = 26, - SlaveUnit = 27, - SpaceBuildable = 28, - SpacePrimarySkydome = 29, - SpaceProp = 30, - SpaceSecondarySkydome = 31, - SpaceUnit = 32, - SpecialEffect = 33, - SpecialStructure = 34, - Squadron = 35, - StarBase = 36, - TechBuilding = 37, - TransportUnit = 38, - UniqueUint = 39, - UpgradeUnit = 40 -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectTypeGameManager.Initialization.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectTypeGameManager.Initialization.cs new file mode 100644 index 0000000..9b1437a --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectTypeGameManager.Initialization.cs @@ -0,0 +1,166 @@ +using System; +using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine.ErrorReporting; +using PG.StarWarsGame.Engine.Xml; +using PG.StarWarsGame.Engine.Xml.Parsers; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AnakinRaW.CommonUtilities.FileSystem; + +namespace PG.StarWarsGame.Engine.GameObjects; + +internal partial class GameObjectTypeGameManager +{ + protected override async Task InitializeCoreAsync(CancellationToken token) + { + Logger?.LogInformation("Parsing GameObjects..."); + await Task.Run(ParseGameObjectDatabases, token); + } + + private void ParseGameObjectDatabases() + { + var gameParser = new PetroglyphStarWarsGameXmlParser(GameRepository, + new PetroglyphStarWarsGameXmlParseSettings + { + GameManager = ToString(), + InvalidFilesListXmlFailsInitialization = true, + InvalidObjectXmlFailsInitialization = false, + }, ServiceProvider, ErrorReporter); + + var xmlFileList = gameParser.ParseFileList(@"DATA\XML\GAMEOBJECTFILES.XML").Files + .Select(x => FileSystem.Path.Combine(@".\DATA\XML\", x)) + .Where(VerifyFilePathLength) + .ToList(); + + var gameObjectFileParser = new GameObjectFileParser(EngineType, ServiceProvider, ErrorReporter); + + var allLoaded = false; + + // This also acts a guard against infinite loops in case of unexpected circular dependencies or + // when a unit declares itself as its own + for (var passNumber = 0; !allLoaded && passNumber < 10; passNumber++) + { + Logger?.LogDebug("***** Parsing game object types - pass {PassNumber} *****", passNumber); + + if (passNumber != 0) + gameObjectFileParser.OverlayLoad = true; + + foreach (var gameObjectXmlFile in xmlFileList) + { + if (passNumber == 0) + { + try + { + gameObjectFileParser.GameObjectParsed += OnGameObjectParsed!; + ParseSingleGameObjectFile(gameObjectXmlFile, gameParser, gameObjectFileParser); + } + finally + { + gameObjectFileParser.GameObjectParsed -= OnGameObjectParsed!; + } + } + else + { + foreach (var gameObject in _gameObjects) + { + if (!gameObject.IsLoadingComplete && IsSameFile(gameObject.Location.XmlFile, gameObjectXmlFile)) + ParseSingleGameObjectFile(gameObjectXmlFile, gameParser, gameObjectFileParser); + } + } + } + + + PostLoadFixup(); + //SFXEventReferenceClass::Static_Post_Load_Fixup(); + //SpeechEventReferenceClass::Static_Post_Load_Fixup(); + //MusicEventReferenceClass::Static_Post_Load_Fixup(); + //FactionReferenceClass::Static_Post_Load_Fixup(); + //... + + allLoaded = true; + + foreach (var gameObject in _gameObjects) + { + gameObject.PostLoadFixup(); + if (!gameObject.IsLoadingComplete) + allLoaded = false; + } + } + + // TODO: The Engine is now asserting some SFX files of all types + } + + private void PostLoadFixup() + { + // In the engine, this is the so-called static post load fixup. + foreach (var gameObject in _gameObjects) + { + var baseTypeName = gameObject.VariantOfExistingTypeName; + if (string.IsNullOrEmpty(baseTypeName) || baseTypeName.Equals("None", StringComparison.OrdinalIgnoreCase)) + continue; + + // At this point the engine would assert, if the base type was not found. + // We do not, because the game reports this error for every iteration of the loading loop, + // which would bloat error logs. + var baseNameHash = _hashingService.GetCrc32Upper(baseTypeName, PGConstants.DefaultPGEncoding); + NamedEntries.TryGetFirstValue(baseNameHash, out var baseType); + gameObject.VariantOfExistingType = baseType; + } + } + + private bool IsSameFile(string filePathA, string filePathB) + { + return FileSystem.Path.AreEqual(filePathA, filePathB); + } + + private void OnGameObjectParsed(object sender, GameObjectParsedEventArgs e) + { + if (!e.Unique) + { + var entries = NamedEntries.GetValues(e.GameObject.Crc32) + .Select(x => x.Name); + ErrorReporter.Assert(EngineAssert.Create( + EngineAssertKind.DuplicateEntry, + e.GameObject.Crc32, entries, + $"Error: Game object type {e.GameObject.Name} is defined multiple times.")); + } + + if (NamedEntries.ValueCount >= 0x10000) + { + ErrorReporter.Assert( + EngineAssert.Create( + EngineAssertKind.ValueOutOfRange, + NamedEntries.ValueCount, + [ToString()], + "Too many game object types defined.")); + } + + _gameObjects.Add(e.GameObject); + } + + private void ParseSingleGameObjectFile( + string file, + PetroglyphStarWarsGameXmlParser gameParser, + GameObjectFileParser gameObjectFileParser) + { + gameParser.ParseObjectsFromContainerFile(file, gameObjectFileParser, NamedEntries); + } + + private bool VerifyFilePathLength(string filePath) + { + if (filePath.Length > PGConstants.MaxGameObjectDatabaseFileName) + { + // Technically this is an assert in the engine, but in Release Mode, the game CTDs. + // Thus, we rank this as an initialization error. + ErrorReporter.Report(new InitializationError + { + GameManager = ToString(), + Message = $"Game object file '{filePath}' is longer than {PGConstants.MaxGameObjectDatabaseFileName} characters." + }); + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectTypeGameManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectTypeGameManager.cs index 407d9c5..14aba19 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectTypeGameManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectTypeGameManager.cs @@ -1,39 +1,85 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; +using PG.Commons.Hashing; using PG.StarWarsGame.Engine.ErrorReporting; using PG.StarWarsGame.Engine.IO.Repositories; -using PG.StarWarsGame.Engine.Xml.Parsers; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Microsoft.Extensions.DependencyInjection; namespace PG.StarWarsGame.Engine.GameObjects; -internal class GameObjectTypeGameManager(GameRepository repository, GameEngineErrorReporterWrapper errorReporter, IServiceProvider serviceProvider) - : GameManagerBase(repository, errorReporter, serviceProvider), IGameObjectTypeGameManager +internal partial class GameObjectTypeGameManager : GameManagerBase, IGameObjectTypeGameManager { - protected override async Task InitializeCoreAsync(CancellationToken token) - { - Logger?.LogInformation("Parsing GameObjects..."); - - var contentParser = new XmlContainerContentParser(ServiceProvider, ErrorReporter); + private readonly List _gameObjects; + private readonly ICrc32HashingService _hashingService; - await Task.Run(() => contentParser.ParseEntriesFromFileListXml( - "DATA\\XML\\GAMEOBJECTFILES.XML", - GameRepository, - ".\\DATA\\XML", - NamedEntries, - VerifyFilePathLength), token); + public GameObjectTypeGameManager( + GameRepository repository, + GameEngineErrorReporterWrapper errorReporter, + IServiceProvider serviceProvider) + : base(repository, errorReporter, serviceProvider) + { + _hashingService = serviceProvider.GetRequiredService(); + _gameObjects = []; + GameObjects = new ReadOnlyCollection(_gameObjects); + } + + public IReadOnlyList GameObjects + { + get + { + ThrowIfNotInitialized(); + return field; + } } - private void VerifyFilePathLength(string filePath) + public IEnumerable GetModels(GameObject gameObject) { - if (filePath.Length > PGConstants.MaxGameObjectDatabaseFileName) + var models = new HashSet(StringComparer.OrdinalIgnoreCase); + AddNotEmpty(gameObject.GalacticModel); + AddNotEmpty(gameObject.DestroyedGalacticModel); + AddNotEmpty(gameObject.LandModel); + AddNotEmpty(gameObject.SpaceModel); + AddNotEmpty(gameObject.GalacticFleetOverrideModel); + AddNotEmpty(gameObject.GuiModel); + AddNotEmpty(gameObject.ModelName); + AddNotEmpty(gameObject.LandAnimOverrideModel); + AddNotEmpty(gameObject.SpaceAnimOverrideModel); + AddNotEmpty(gameObject.DamagedSmokeAssetModel); + AddTerrainMappingModels(); + return models; + + + void AddNotEmpty(string? value, Predicate? predicate = null) + { + if (string.IsNullOrEmpty(value)) + return; + if (predicate is null || predicate(value)) + models.Add(value); + } + + + void AddTerrainMappingModels() { - ErrorReporter.Report(new InitializationError + var visitedEnvTypes = new HashSet(); + foreach (var mapping in gameObject.LandTerrainModelMappingValues) { - GameManager = ToString(), - Message = $"Game object file '{filePath}' is longer than {PGConstants.MaxGameObjectDatabaseFileName} characters." - }); + if (MapEnvironmentTypeConversion.ConversionDictionary.TryStringToEnum(mapping.terrain, out var envType)) + { + // The engine only uses the first model for each environment type. + if (visitedEnvTypes.Add(envType)) + AddNotEmpty(mapping.model); + } + } } } + + public GameObject? FindObjectType(string? name) + { + if (string.IsNullOrEmpty(name)) + return null; + var nameCrc = _hashingService.GetCrc32Upper(name, PGConstants.DefaultPGEncoding); + NamedEntries.TryGetFirstValue(nameCrc, out var gameObject); + return gameObject; + } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/IGameObjectTypeGameManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/IGameObjectTypeGameManager.cs index f31273f..ef72db3 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/IGameObjectTypeGameManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/IGameObjectTypeGameManager.cs @@ -1,3 +1,25 @@ -namespace PG.StarWarsGame.Engine.GameObjects; +using System.Collections.Generic; -public interface IGameObjectTypeGameManager : IGameManager; \ No newline at end of file +namespace PG.StarWarsGame.Engine.GameObjects; + +public interface IGameObjectTypeGameManager : IGameManager +{ + // List represent XML load order + IReadOnlyList GameObjects { get; } + + /// + /// Retrieves the collection of model and particle names directly associated with the specified . + /// + /// + /// Death Clones, Projectiles, Hardpoints, etc. are not included in the returned collection. + /// + /// + /// The for which to retrieve the associated model names. + /// + /// + /// An containing the names of the models associated with the specified . + /// + IEnumerable GetModels(GameObject gameObject); + + GameObject? FindObjectType(string name); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/MapEnvironmentType.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/MapEnvironmentType.cs new file mode 100644 index 0000000..2faf565 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/MapEnvironmentType.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using PG.StarWarsGame.Engine.Xml; + +namespace PG.StarWarsGame.Engine.GameObjects; + +// TODO: Not sure, this is the correct namespace + +public enum MapEnvironmentType +{ + Temperate = 0x0, + Arctic = 0x1, + Desert = 0x2, + Forest = 0x3, + Swamp = 0x4, + Volcanic = 0x5, + Urban = 0x6, + Space = 0x7, +} + +// TODO: To separate GameManager that holds these Conversion instances +public static class MapEnvironmentTypeConversion +{ + public static readonly EnumConversionDictionary ConversionDictionary = new([ + new KeyValuePair("Temperate", MapEnvironmentType.Temperate), + new KeyValuePair("Arctic", MapEnvironmentType.Arctic), + new KeyValuePair("Desert", MapEnvironmentType.Desert), + new KeyValuePair("Forest", MapEnvironmentType.Forest), + new KeyValuePair("Swamp", MapEnvironmentType.Swamp), + new KeyValuePair("Volcanic", MapEnvironmentType.Volcanic), + new KeyValuePair("Urban", MapEnvironmentType.Urban), + new KeyValuePair("Space", MapEnvironmentType.Space) + ]); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/ComponentTextureEntry.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/ComponentTextureEntry.cs index f1ffb04..6d188e7 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/ComponentTextureEntry.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/ComponentTextureEntry.cs @@ -1,5 +1,8 @@ -namespace PG.StarWarsGame.Engine.GuiDialog; +using System.Diagnostics; +namespace PG.StarWarsGame.Engine.GuiDialog; + +[DebuggerDisplay("Type:{ComponentType}, Texture: {Texture}")] public readonly struct ComponentTextureEntry(GuiComponentType componentType, string texture, bool isOverride) { public string Texture { get; } = texture; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/ExtensionMethods.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/ExtensionMethods.cs new file mode 100644 index 0000000..dff28af --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/ExtensionMethods.cs @@ -0,0 +1,22 @@ +namespace PG.StarWarsGame.Engine.GuiDialog; + +public static class ExtensionMethods +{ + extension(GuiComponentType componentType) + { + public bool IsButton() + { + return componentType <= GuiComponentType.ButtonRightDisabled; + } + + public bool SupportsSpecialTextureMode() + { + return componentType is GuiComponentType.ButtonMiddle + or GuiComponentType.ButtonMiddleMouseOver + or GuiComponentType.ButtonMiddlePressed + or GuiComponentType.ButtonMiddleDisabled + or GuiComponentType.Scanlines + or GuiComponentType.FrameBackground; + } + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager.cs index 879c16f..d6b4ca9 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using PG.Commons.Hashing; @@ -13,21 +14,33 @@ namespace PG.StarWarsGame.Engine.GuiDialog; -internal partial class GuiDialogGameManager(GameRepository repository, GameEngineErrorReporterWrapper errorReporter, IServiceProvider serviceProvider) +internal partial class GuiDialogGameManager( + GameRepository repository, + GameEngineErrorReporterWrapper errorReporter, + IServiceProvider serviceProvider) : GameManagerBase(repository, errorReporter, serviceProvider), IGuiDialogManager { private readonly IMtdFileService _mtdFileService = serviceProvider.GetRequiredService(); private readonly ICrc32HashingService _hashingService = serviceProvider.GetRequiredService(); - // Unlike other strings for this game, the component's (aka gadget) name is case-sensitive. - private readonly Dictionary> _perComponentTextures = new(StringComparer.Ordinal); - private readonly Dictionary _defaultTextures = new(); - private ReadOnlyDictionary _defaultTexturesRo = null!; - private bool _megaTextureExists; private string? _megaTextureFileName; - + [field: MaybeNull, AllowNull] + private ReadOnlyDictionary> PerComponentTextures + { + get + { + ThrowIfNotInitialized(); + return field!; + } + set + { + ThrowIfAlreadyInitialized(); + field = value; + } + } + public IMtdFile? MtdFile { get @@ -61,7 +74,7 @@ public IReadOnlyCollection Components get { ThrowIfNotInitialized(); - return _perComponentTextures.Keys; + return PerComponentTextures.Keys!; } } @@ -70,14 +83,19 @@ public IReadOnlyDictionary DefaultTextu get { ThrowIfNotInitialized(); - return _defaultTexturesRo; + return field!; + } + internal set + { + ThrowIfAlreadyInitialized(); + field = value; } } public IReadOnlyDictionary GetTextureEntries(string component, out bool componentExist) { - if (!_perComponentTextures.TryGetValue(component, out var textures)) + if (!PerComponentTextures.TryGetValue(component, out var textures)) { Logger?.LogDebug("The component '{Component}' has no overrides. Using default textures.", component); componentExist = false; @@ -85,15 +103,15 @@ public IReadOnlyDictionary GetTextureEn } componentExist = true; - return new ReadOnlyDictionary(textures); + return textures; } public bool TryGetTextureEntry(string component, GuiComponentType key, out ComponentTextureEntry texture) { - if (!_perComponentTextures.TryGetValue(component, out var textures)) + if (!PerComponentTextures.TryGetValue(component, out var textures)) { Logger?.LogDebug("The component '{Component}' has no overrides. Using default textures.", component); - textures = _defaultTextures; + textures = DefaultTextureEntries; } return textures.TryGetValue(key, out texture); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager_Initialization.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager_Initialization.cs index c3814ea..2bac738 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager_Initialization.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager_Initialization.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading; @@ -8,13 +7,13 @@ using Microsoft.Extensions.Logging; using PG.StarWarsGame.Engine.ErrorReporting; using PG.StarWarsGame.Engine.GuiDialog.Xml; -using PG.StarWarsGame.Engine.Xml.Parsers.File; -using PG.StarWarsGame.Engine.Xml.Tags; +using PG.StarWarsGame.Engine.Xml; +using PG.StarWarsGame.Engine.Xml.Parsers; using PG.StarWarsGame.Files.Binary; namespace PG.StarWarsGame.Engine.GuiDialog; -partial class GuiDialogGameManager +internal partial class GuiDialogGameManager { public const int MegaTextureMaxFilePathLength = 255; @@ -22,24 +21,14 @@ protected override Task InitializeCoreAsync(CancellationToken token) { return Task.Run(() => { - var guiDialogParser = new GuiDialogParser(ServiceProvider, ErrorReporter); - - _defaultTexturesRo = new ReadOnlyDictionary(_defaultTextures); - - Logger?.LogInformation("Parsing GuiDialogs..."); - using var fileStream = GameRepository.TryOpenFile("DATA\\XML\\GUIDIALOGS.XML"); - - if (fileStream is null) - { - ErrorReporter.Report(new InitializationError + var engineParser = new PetroglyphStarWarsGameXmlParser(GameRepository, + new PetroglyphStarWarsGameXmlParseSettings { GameManager = ToString(), - Message = "Unable to find GuiDialogs.xml" - }); - return; - } + InvalidObjectXmlFailsInitialization = true + }, ServiceProvider, ErrorReporter); - var guiDialogs = guiDialogParser.ParseFile(fileStream); + var guiDialogs = engineParser.ParseFile("DATA\\XML\\GUIDIALOGS.XML", new GuiDialogParser(ServiceProvider, ErrorReporter)); if (guiDialogs is null) { ErrorReporter.Report(new InitializationError @@ -50,10 +39,8 @@ protected override Task InitializeCoreAsync(CancellationToken token) return; } - GuiDialogsXml = guiDialogs; - InitializeTextures(guiDialogs.TextureData); - + GuiDialogsXml = guiDialogs; }, token); } @@ -62,7 +49,8 @@ private void InitializeTextures(GuiDialogsXmlTextureData textureData) InitializeMegaTextures(textureData); var textures = textureData.Textures; - + + IReadOnlyDictionary defaultTextures; if (textures.Count == 0) { ErrorReporter.Report(new InitializationError @@ -70,47 +58,70 @@ private void InitializeTextures(GuiDialogsXmlTextureData textureData) GameManager = ToString(), Message = "No Textures defined in GuiDialogs.xml" }); + + defaultTextures = new ReadOnlyDictionary( + new Dictionary()); } else { - var defaultCandidate = textures.First(); - // Regardless of its name, the game treats the first entry as default. - var defaultTextures = InitializeComponentTextures(defaultCandidate, true, out var invalidKeys); - foreach (var entry in defaultTextures) - _defaultTextures.Add(entry.Key, entry.Value); + var defaultCandidate = textures.First(); + defaultTextures = InitializeComponentTextures(defaultCandidate, null, out var invalidKeys); + ReportInvalidComponent(in invalidKeys); } + var perComponentTextures = new Dictionary>(); + foreach (var componentTextureData in textures.Skip(1)) { // The game only uses the *first* entry. - if (_perComponentTextures.ContainsKey(componentTextureData.Component)) + if (perComponentTextures.ContainsKey(componentTextureData.Component)) continue; - _perComponentTextures.Add(componentTextureData.Component, InitializeComponentTextures(componentTextureData, false, out var invalidKeys)); + perComponentTextures.Add( + componentTextureData.Component, + InitializeComponentTextures(componentTextureData, defaultTextures, out var invalidKeys)); + ReportInvalidComponent(in invalidKeys); } + + DefaultTextureEntries = defaultTextures; + PerComponentTextures = + new ReadOnlyDictionary>( + perComponentTextures); } - private Dictionary InitializeComponentTextures(XmlComponentTextureData textureData, bool isDefaultComponent, out FrugalList invalidKeys) + private ReadOnlyDictionary InitializeComponentTextures( + XmlComponentTextureData textureData, + IReadOnlyDictionary? defaultTextures, + out FrugalList invalidKeys) { - invalidKeys = new FrugalList(); + invalidKeys = []; var result = new Dictionary(); + var isDefaultComponent = defaultTextures is null; + if (!isDefaultComponent) { - // This assumes that _defaultTextures is already filled - foreach (var key in _defaultTextures.Keys) - result.Add(key, _defaultTextures[key]); + foreach (var key in defaultTextures!.Keys) + result.Add(key, defaultTextures[key]); } - + if (textureData.Textures.Count == 0) + { + ErrorReporter.Report(new InitializationError + { + GameManager = ToString(), + Message = $"No Textures defined for component '{textureData.Component}' in GuiDialogs.xml" + }); + } + foreach (var keyText in textureData.Textures.Keys) { - if (!ComponentTextureKeyExtensions.TryConvertToKey(keyText.AsSpan(), out var key)) + if (!GuiDialogParser.ComponentTypeDictionary.TryStringToEnum(keyText, out var key)) { invalidKeys.Add(keyText); continue; @@ -119,8 +130,8 @@ private Dictionary InitializeComponentT var textureValue = textureData.Textures.GetLastValue(keyText); result[key] = new ComponentTextureEntry(key, textureValue, !isDefaultComponent); } - - return result; + + return new ReadOnlyDictionary(result); } private void InitializeMegaTextures(GuiDialogsXmlTextureData guiDialogs) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/GuiDialogsXml.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/GuiDialogsXml.cs index 72cc5a0..dbaaed6 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/GuiDialogsXml.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/GuiDialogsXml.cs @@ -1,5 +1,5 @@ -using PG.StarWarsGame.Engine.Xml; -using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Data; namespace PG.StarWarsGame.Engine.GuiDialog.Xml; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/GuiDialogsXmlTextureData.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/GuiDialogsXmlTextureData.cs index 6496417..c5e56b8 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/GuiDialogsXmlTextureData.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/GuiDialogsXmlTextureData.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; -using PG.StarWarsGame.Engine.Xml; using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Data; namespace PG.StarWarsGame.Engine.GuiDialog.Xml; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/XmlComponentTextureData.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/XmlComponentTextureData.cs index f102457..6b91547 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/XmlComponentTextureData.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/XmlComponentTextureData.cs @@ -1,7 +1,7 @@ using System; using AnakinRaW.CommonUtilities.Collections; -using PG.StarWarsGame.Engine.Xml; using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Data; namespace PG.StarWarsGame.Engine.GuiDialog.Xml; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/HardcodedEngineAssets.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/HardcodedEngineAssets.cs new file mode 100644 index 0000000..92a7cdf --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/HardcodedEngineAssets.cs @@ -0,0 +1,302 @@ +using System; +using System.Collections.Generic; + +namespace PG.StarWarsGame.Engine; + +public static class HardcodedEngineAssets +{ + /// + /// These models / particles are hardcoded into StarWarsG.exe. + /// + public static IList HardcodedFocModels { get; } = new List + { + "i_tutorial_arrow.alo", + "p_hero_empire_fx.alo", + "i_tactical_corrupt.alo", + "p_icon_corrupt.alo", + "w_planet_select_neutral.alo", + "i_game_arrow.alo", + "i_galactic_radar.alo", + "W_TextScroll.alo" + }; + + + /// + /// These models / particles are hardcoded into StarWarsG.exe. + /// + public static IList HardcodedEawModels { get; } = new List + { + "i_tutorial_arrow.alo", + "p_hero_empire_fx.alo", + "w_planet_select_neutral.alo", + "i_game_arrow.alo", + "i_galactic_radar.alo", + "W_TextScroll.alo" + }; + + public static IList HardcodedFocTextures { get; } = new List + { + "splash.tga", + "SPLASH_E3.tga", + "i_button_temporary.tga", + "i_attention00.tga", + "i_tree_arrow_right.tga", + "load_overlay.tga", + "i_pa_weather_sun.tga", + "i_pa_weather_fire.tga", + "i_pa_weather_snow.tga", + "i_pa_weather_rain.tga", + "i_pa_weather_wind.tga", + "Menuback_Overlay.TGA", + "i_button_space_station.tga", + "i_button_ground_base.tga", + "i_button_space_unitcount.tga", + "i_button_ground_unitcount.tga", + "i_dialogue_blank.tga", + "checked_box.tga", + "Galactic_Back.tga", + "Generic_Space.tga", + "Generic_Land.tga", + "red_dot.tga", + "yellow_dot.tga", + "green_dot.tga", + "i_tree_arrow_2right.tga", + "i_tree_arrow_3right.tga", + "i_tree_arrow_down.tga", + "Generic_Flat_B.tga", + "w_generic_white.tga", + "w_shadow_blob.tga", + "w_light_blob.tga", + "p_particle_master.tga", + "missing_texture_xxx.tga", + "D_master_decal.tga", + "p_particle_depth_master.tga", + "MT_CommandBar.tga", + "MT_CommandBarCompressed.dds", + "DEFAULTPOINTER_00.TGA", + "tractor_beam00.tga", + "i_button_unknown.tga", + "i_icon_stealth.tga", + "e_line2.tga", + "e_line.tga", + "i_bar_icons_space.tga", + "i_bar_icons_land.tga", + "e_topbar2.tga", + "e_topbar.tga", + "e_against_frame.tga", + "missing.tga", + "i_encyclopedia_border.tga", + "i_radar_default_blip.tga", + "select_overlay.tga", + "i_icon_raid.tga", + "maptemp.tga", + "shield_range.tga", + "W_galaxy_line.tga", + "W_galaxy_line_alpha.tga", + "W_galaxy_dot.tga", + "i_sa_defend_mode.tga", + "i_sa_deploy.tga", + "i_sa_interdict.tga", + "i_sa_barrage_area.tga", + "i_sa_capture_vehicles.tga", + "i_sa_spread_out.tga", + "i_sa_power_to_engines.tga", + "i_sa_rocket_attack.tga", + "i_sa_power_to_weapons.tga", + "i_sa_tractor_beam.tga", + "i_sa_fire_energy_weapon.tga", + "i_sa_missile_jammer.tga", + "i_sa_evasive_maneuvers.tga", + "i_sa_all_ships_concentrate_fire.tga", + "i_sa_sprint.tga", + "i_sa_stim_pack.tga", + "i_sa_s_foil_mode.tga", + "i_sa_maximum_firepower.tga", + "i_sa_swap_weapons.tga", + "i_sa_full_salvo.tga", + "i_sa_force_cloak.tga", + "i_sa_sensor_jamming.tga", + "i_sa_force_crush.tga", + "i_sa_force_push.tga", + "i_sa_force_lighting.tga", + "i_sa_flame_thrower.tga", + "i_sa_jetpack_jump.tga", + "i_sa_force_protect.tga", + "i_sa_hack_turret.tga", + "i_sa_repair_vehicle.tga", + "i_sa_sticky_bomb.tga", + "i_sa_electronic_scramble.tga", + "i_sa_area_heal.tga", + "i_sa_join_me.tga", + "i_sa_tow_cable_attack.tga", + "i_sa_sensor_ping.tga", + "i_sa_cover_me.tga", + "i_sa_Harmonic_bomb.tga", + "i_sa_drop_bomb.tga", + "i_sa_weaken_enemy.tga", + "i_sa_drain_life.tga", + "i_sa_blast.tga", + "i_sa_shield_flare.tga", + "i_sa_deploy_squad.tga", + "i_sa_stun.tga", + "i_sa_contaminate.tga", + "i_sa_berserker.tga", + "i_sa_force_sight.tga", + "i_sa_saber_throw.tga", + "i_sa_laser_defense.tga", + "i_sa_force_confuse.tga", + "i_sa_leech_shields.tga", + "i_sa_tactical_bribe.tga", + "i_sa_cluster_bomb.tga", + "i_sa_place_remote_bomb.tga", + "i_sa_detonate_remote_bomb.tga", + "i_sa_infection.tga", + "i_sa_proximity_mines.tga", + "i_sa_buzz_droids.tga", + "i_sa_summon.tga", + "i_sa_corrupt_systems.tga", + "i_sa_hunt.tga", + "i_sa_lure.tga", + "i_sa_self_destruct.tga", + "i_sa_deploy_stormtroopers.tga", + "i_sa_ion_cannon_shot.tga", + "i_sa_lucky_shot.tga", + "lightning_default.tga", + "Mon_Mothma.tga", + "Tarkin.tga", + "W_Cable.tga", + "i_information00.tga", + "W_Laser_Pill.tga", + "lensflare.tga", + "W_Space_FOW_Grid.tga", + "W_Space_Reinforce_FOW_Grid.tga", + }; + + public static IList HardcodedEawTextures { get; } = new List + { + "splash.tga", + "i_button_temporary.tga", + "i_attention00.tga", + "i_tree_arrow_right.tga", + "load_overlay.tga", + "i_pa_weather_sun.tga", + "i_pa_weather_fire.tga", + "i_pa_weather_snow.tga", + "i_pa_weather_rain.tga", + "i_pa_weather_wind.tga", + "Menuback_Overlay.TGA", + "i_button_space_station.tga", + "i_button_ground_base.tga", + "i_button_space_unitcount.tga", + "i_button_ground_unitcount.tga", + "i_dialogue_blank.tga", + "checked_box.tga", + "Galactic_Back.tga", + "Generic_Space.tga", + "Generic_Land.tga", + "red_dot.tga", + "yellow_dot.tga", + "green_dot.tga", + "i_tree_arrow_2right.tga", + "i_tree_arrow_3right.tga", + "i_tree_arrow_down.tga", + "Generic_Flat_B.tga", + "w_generic_white.tga", + "w_shadow_blob.tga", + "w_light_blob.tga", + "p_particle_master.tga", + "missing_texture_xxx.tga", + "D_master_decal.tga", + "p_particle_depth_master.tga", + "MT_CommandBar.tga", + "MT_CommandBarCompressed.dds", + "DEFAULTPOINTER_00.TGA", + "tractor_beam00.tga", + "i_button_unknown.tga", + "i_icon_stealth.tga", + "e_line2.tga", + "e_line.tga", + "i_bar_icons_space.tga", + "i_bar_icons_land.tga", + "e_topbar2.tga", + "e_topbar.tga", + "e_against_frame.tga", + "missing.tga", + "i_encyclopedia_border.tga", + "i_radar_default_blip.tga", + "select_overlay.tga", + "i_icon_raid.tga", + "W_galaxy_line.tga", + "W_galaxy_line_alpha.tga", + "W_galaxy_dot.tga", + "i_sa_defend_mode.tga", + "i_sa_deploy.tga", + "i_sa_interdict.tga", + "i_sa_barrage_area.tga", + "i_sa_capture_vehicles.tga", + "i_sa_spread_out.tga", + "i_sa_power_to_engines.tga", + "i_sa_rocket_attack.tga", + "i_sa_power_to_weapons.tga", + "i_sa_tractor_beam.tga", + "i_sa_fire_energy_weapon.tga", + "i_sa_missile_jammer.tga", + "i_sa_evasive_maneuvers.tga", + "i_sa_all_ships_concentrate_fire.tga", + "i_sa_sprint.tga", + "i_sa_s_foil_mode.tga", + "i_sa_maximum_firepower.tga", + "i_sa_force_crush.tga", + "i_sa_force_push.tga", + "i_sa_force_lighting.tga", + "i_sa_flame_thrower.tga", + "i_sa_jetpack_jump.tga", + "i_sa_force_protect.tga", + "i_sa_hack_turret.tga", + "i_sa_repair_vehicle.tga", + "i_sa_sticky_bomb.tga", + "i_sa_electronic_scramble.tga", + "i_sa_area_heal.tga", + "i_sa_join_me.tga", + "i_sa_tow_cable_attack.tga", + "i_sa_sensor_ping.tga", + "i_sa_cover_me.tga", + "i_sa_Harmonic_bomb.tga", + "i_sa_drop_bomb.tga", + "i_sa_weaken_enemy.tga", + "i_sa_hunt.tga", + "i_sa_lure.tga", + "i_sa_self_destruct.tga", + "i_sa_deploy_stormtroopers.tga", + "i_sa_ion_cannon_shot.tga", + "i_sa_lucky_shot.tga", + "lightning_default.tga", + "Mon_Mothma.tga", + "Tarkin.tga", + "W_Cable.tga", + "i_information00.tga", + "W_Laser_Pill.tga", + "W_Space_FOW_Grid.tga", + "W_Space_Reinforce_FOW_Grid.tga", + }; + + public static IList GetHardcodedModels(GameEngineType engine) + { + return engine switch + { + GameEngineType.Eaw => HardcodedEawModels, + GameEngineType.Foc => HardcodedFocModels, + _ => throw new ArgumentOutOfRangeException(nameof(engine), engine, null) + }; + } + + public static IList GetHardcodedTextures(GameEngineType engine) + { + return engine switch + { + GameEngineType.Eaw => HardcodedEawTextures, + GameEngineType.Foc => HardcodedFocTextures, + _ => throw new ArgumentOutOfRangeException(nameof(engine), engine, null) + }; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/IRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/IRepository.cs index 4ca45db..89c2d86 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/IRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/IRepository.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.IO; namespace PG.StarWarsGame.Engine.IO; @@ -9,6 +10,7 @@ public interface IRepository Stream OpenFile(ReadOnlySpan filePath, bool megFileOnly = false); bool FileExists(string filePath, bool megFileOnly = false); + bool FileExists(string filePath, bool megFileOnly, out bool inMeg, [NotNullWhen(true)] out string? actualFilePath); bool FileExists(ReadOnlySpan filePath, bool megFileOnly = false); bool FileExists(ReadOnlySpan filePath, bool megFileOnly, out bool pathTooLong); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/MultiPassRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/MultiPassRepository.cs index 0fce4d3..fd95e5b 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/MultiPassRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/MultiPassRepository.cs @@ -1,9 +1,10 @@ -using System; -using System.IO; -using System.IO.Abstractions; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using PG.StarWarsGame.Engine.IO.Repositories; using PG.StarWarsGame.Engine.Utilities; +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Abstractions; namespace PG.StarWarsGame.Engine.IO; @@ -30,6 +31,22 @@ public bool FileExists(string filePath, bool megFileOnly = false) return FileExists(filePath.AsSpan(), megFileOnly); } + public bool FileExists(string filePath, bool megFileOnly, out bool inMeg, [NotNullWhen(true)] out string? actualFilePath) + { + var multiPassSb = new ValueStringBuilder(stackalloc char[PGConstants.MaxMegEntryPathLength]); + var destinationSb = new ValueStringBuilder(stackalloc char[PGConstants.MaxMegEntryPathLength]); + var result = MultiPassAction(filePath, ref multiPassSb, ref destinationSb, megFileOnly); + var fileFound = result.FileFound; + inMeg = result.InMeg; + if (!fileFound) + actualFilePath = null; + else + actualFilePath = result.InMeg ? result.MegDataEntryReference.Path : result.FilePath.ToString(); + multiPassSb.Dispose(); + destinationSb.Dispose(); + return fileFound; + } + public bool FileExists(ReadOnlySpan filePath, bool megFileOnly = false) { return FileExists(filePath, megFileOnly, out _); 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 7abd1b9..d35254d 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs @@ -1,14 +1,15 @@ -using System; +using AnakinRaW.CommonUtilities.FileSystem; +using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine.IO.Utilities; +using PG.StarWarsGame.Engine.Utilities; +using PG.StarWarsGame.Files.MEG.Binary; +using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using AnakinRaW.CommonUtilities.FileSystem; -using Microsoft.Extensions.Logging; -using PG.StarWarsGame.Engine.IO.Utilities; -using PG.StarWarsGame.Engine.Utilities; -using PG.StarWarsGame.Files.MEG.Binary; namespace PG.StarWarsGame.Engine.IO.Repositories; @@ -32,6 +33,20 @@ public bool FileExists(string filePath, bool megFileOnly = false) return FileExists(filePath.AsSpan(), megFileOnly); } + public bool FileExists(string filePath, bool megFileOnly, out bool inMeg, [NotNullWhen(true)] out string? actualFilePath) + { + var sb = new ValueStringBuilder(stackalloc char[PGConstants.MaxMegEntryPathLength]); + var fileFound = FindFile(filePath, ref sb, megFileOnly); + var fileExists = fileFound.FileFound; + inMeg = fileFound.InMeg; + if (!fileExists) + actualFilePath = null; + else + actualFilePath = fileFound.InMeg ? fileFound.MegDataEntryReference.Path : fileFound.FilePath.ToString(); + sb.Dispose(); + return fileExists; + } + public bool FileExists(ReadOnlySpan filePath, bool megFileOnly = false) { return FileExists(filePath, megFileOnly, out _); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj index 47b3a7f..8d201e4 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 @@
- - - - + + + + diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj.DotSettings b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj.DotSettings index 57b1fa3..89ac95d 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj.DotSettings +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj.DotSettings @@ -4,4 +4,6 @@ True True True - True \ No newline at end of file + True + True + True \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PGConstants.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/PGConstants.cs index b7a9218..a53709c 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PGConstants.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PGConstants.cs @@ -15,8 +15,11 @@ public static class PGConstants public const int MaxSFXEventDatabaseFileName = 259; public const int MaxSFXEventName = 255; public const int MaxGameObjectDatabaseFileName = 127; - public const int MaxCommandBarDatabaseFileName = 259; + public const int MaxCommandBarDatabaseFileName = 271; public const int MaxCommandBarComponentName = 255; + // The actual engine's buffer size that holds name. + public const int MaxCommandBarComponentNameBuffer = 263; + public const int MaxGameObjectTypeName = 127; public const int MaxGuiDialogMegaTextureFileName = 255; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs index 0132d10..73bd755 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs @@ -12,7 +12,7 @@ public static void ContributeServices(IServiceCollection serviceCollection) // Singletons serviceCollection.AddSingleton(sp => new GameRepositoryFactory(sp)); serviceCollection.AddSingleton(sp => new GameLanguageManagerProvider(sp)); - serviceCollection.AddSingleton(sp => new PetroglyphXmlFileParserFactory(sp)); + serviceCollection.AddSingleton(sp => new XmlObjectParserFactory(sp)); serviceCollection.AddSingleton(sp => new PetroglyphStarWarsGameEngineService(sp)); } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs index 02a8d9f..42a2416 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs @@ -51,6 +51,7 @@ public async Task InitializeAsync( void OnInitializationError(object sender, InitializationError e) { + _logger?.LogWarning("Engine initialization failed for {Manager}: {Message}", e.GameManager, e.Message); if (cancelOnInitializationError) cts.Cancel(); } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/EawModelAnimationTypes.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/EawModelAnimationTypes.cs deleted file mode 100644 index f1a89c3..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/EawModelAnimationTypes.cs +++ /dev/null @@ -1,109 +0,0 @@ -namespace PG.StarWarsGame.Engine.Rendering.Animations; - -public static class EawModelAnimationTypes -{ - public static readonly ModelAnimationType Idle = new(GameEngineType.Eaw, 0x0); - public static readonly ModelAnimationType SpaceIdle = new(GameEngineType.Eaw, 0x1); - public static readonly ModelAnimationType Move = new(GameEngineType.Eaw, 0x2); - public static readonly ModelAnimationType TurnLeft = new(GameEngineType.Eaw, 0x3); - public static readonly ModelAnimationType TurnRight = new(GameEngineType.Eaw, 0x4); - public static readonly ModelAnimationType Attack = new(GameEngineType.Eaw, 0x5); - public static readonly ModelAnimationType AttackIdle = new(GameEngineType.Eaw, 0x6); - public static readonly ModelAnimationType Die = new(GameEngineType.Eaw, 0x7); - public static readonly ModelAnimationType Rotate = new(GameEngineType.Eaw, 0x8); - public static readonly ModelAnimationType SpecialA = new(GameEngineType.Eaw, 0x9); - public static readonly ModelAnimationType SpecialB = new(GameEngineType.Eaw, 0xa); - public static readonly ModelAnimationType SpecialC = new(GameEngineType.Eaw, 0xb); - public static readonly ModelAnimationType TransitionToLeftTurn = new(GameEngineType.Eaw, 0xc); - public static readonly ModelAnimationType TransitionFromLeftTurn = new(GameEngineType.Eaw, 0xd); - public static readonly ModelAnimationType TransitionToRightTurn = new(GameEngineType.Eaw, 0xe); - public static readonly ModelAnimationType TransitionFromRightTurn = new(GameEngineType.Eaw, 0xf); - public static readonly ModelAnimationType TransitionToMove = new(GameEngineType.Eaw, 0x10); - public static readonly ModelAnimationType TransitionFromMoveFrame0 = new(GameEngineType.Eaw, 0x11); - public static readonly ModelAnimationType TransitionFromMoveFrame40 = new(GameEngineType.Eaw, 0x12); - public static readonly ModelAnimationType TransitionFromMoveFrame80 = new(GameEngineType.Eaw, 0x13); - public static readonly ModelAnimationType TransitionFromMoveFrame120 = new(GameEngineType.Eaw, 0x14); - public static readonly ModelAnimationType TurnLeftHalf = new(GameEngineType.Eaw, 0x15); - public static readonly ModelAnimationType TurnLeftQuarter = new(GameEngineType.Eaw, 0x16); - public static readonly ModelAnimationType TurnRightHalf = new(GameEngineType.Eaw, 0x17); - public static readonly ModelAnimationType TurnRightQuarter = new(GameEngineType.Eaw, 0x18); - public static readonly ModelAnimationType Deploy = new(GameEngineType.Eaw, 0x19); - public static readonly ModelAnimationType Undeploy = new(GameEngineType.Eaw, 0x1a); - public static readonly ModelAnimationType Cinematic = new(GameEngineType.Eaw, 0x1b); - public static readonly ModelAnimationType BlockBlaster = new(GameEngineType.Eaw, 0x1c); - public static readonly ModelAnimationType RedirectBlaster = new(GameEngineType.Eaw, 0x1d); - public static readonly ModelAnimationType IdleBlockBlaster = new(GameEngineType.Eaw, 0x1e); - public static readonly ModelAnimationType ForceWhirlwindAttack = new(GameEngineType.Eaw, 0x1f); - public static readonly ModelAnimationType ForceWhirlwindDie = new(GameEngineType.Eaw, 0x20); - public static readonly ModelAnimationType ForceTelekinesisAttack = new(GameEngineType.Eaw, 0x21); - public static readonly ModelAnimationType ForceTelekinesisHold = new(GameEngineType.Eaw, 0x22); - public static readonly ModelAnimationType ForceTelekinesisRelease = new(GameEngineType.Eaw, 0x23); - public static readonly ModelAnimationType ForceTelekinesisDie = new(GameEngineType.Eaw, 0x24); - public static readonly ModelAnimationType EarthquakeAttack = new(GameEngineType.Eaw, 0x25); - public static readonly ModelAnimationType EarthquakeHold = new(GameEngineType.Eaw, 0x26); - public static readonly ModelAnimationType EarthquakeRelease = new(GameEngineType.Eaw, 0x27); - public static readonly ModelAnimationType ForceLightningAttack = new(GameEngineType.Eaw, 0x28); - public static readonly ModelAnimationType ForceLightningDie = new(GameEngineType.Eaw, 0x29); - public static readonly ModelAnimationType ForceRun = new(GameEngineType.Eaw, 0x2a); - public static readonly ModelAnimationType TransportLanding = new(GameEngineType.Eaw, 0x2b); - public static readonly ModelAnimationType TransportLeaving = new(GameEngineType.Eaw, 0x2c); - public static readonly ModelAnimationType FlameAttack = new(GameEngineType.Eaw, 0x2d); - public static readonly ModelAnimationType Demolition = new(GameEngineType.Eaw, 0x2e); - public static readonly ModelAnimationType BombToss = new(GameEngineType.Eaw, 0x2f); - public static readonly ModelAnimationType Jump = new(GameEngineType.Eaw, 0x30); - public static readonly ModelAnimationType FlyIdle = new(GameEngineType.Eaw, 0x31); - public static readonly ModelAnimationType FlyLand = new(GameEngineType.Eaw, 0x32); - public static readonly ModelAnimationType LandIdle = new(GameEngineType.Eaw, 0x33); - public static readonly ModelAnimationType Land = new(GameEngineType.Eaw, 0x34); - public static readonly ModelAnimationType HcWin = new(GameEngineType.Eaw, 0x35); - public static readonly ModelAnimationType HcLose = new(GameEngineType.Eaw, 0x36); - public static readonly ModelAnimationType HcDraw = new(GameEngineType.Eaw, 0x37); - public static readonly ModelAnimationType ShieldOn = new(GameEngineType.Eaw, 0x38); - public static readonly ModelAnimationType ShieldOff = new(GameEngineType.Eaw, 0x39); - public static readonly ModelAnimationType CableAttackDie = new(GameEngineType.Eaw, 0x3a); - public static readonly ModelAnimationType DeployedCableAttackDie = new(GameEngineType.Eaw, 0x3b); - public static readonly ModelAnimationType DeployedDie = new(GameEngineType.Eaw, 0x3c); - public static readonly ModelAnimationType RunAroundOnFire = new(GameEngineType.Eaw, 0x3d); - public static readonly ModelAnimationType FireDie = new(GameEngineType.Eaw, 0x3e); - public static readonly ModelAnimationType PoundAttack = new(GameEngineType.Eaw, 0x3f); - public static readonly ModelAnimationType EatAttack = new(GameEngineType.Eaw, 0x40); - public static readonly ModelAnimationType EatDie = new(GameEngineType.Eaw, 0x41); - public static readonly ModelAnimationType MoveWalk = new(GameEngineType.Eaw, 0x42); - public static readonly ModelAnimationType MoveCrouch = new(GameEngineType.Eaw, 0x43); - public static readonly ModelAnimationType StructureOpen = new(GameEngineType.Eaw, 0x44); - public static readonly ModelAnimationType StructureHold = new(GameEngineType.Eaw, 0x45); - public static readonly ModelAnimationType StructureClose = new(GameEngineType.Eaw, 0x46); - public static readonly ModelAnimationType IdleCrouch = new(GameEngineType.Eaw, 0x47); - public static readonly ModelAnimationType TurnLeftCrouch = new(GameEngineType.Eaw, 0x48); - public static readonly ModelAnimationType TurnRightCrouch = new(GameEngineType.Eaw, 0x49); - public static readonly ModelAnimationType Build = new(GameEngineType.Eaw, 0x4a); - public static readonly ModelAnimationType TransitionOwnership = new(GameEngineType.Eaw, 0x4b); - public static readonly ModelAnimationType SelfDestruct = new(GameEngineType.Eaw, 0x4c); - public static readonly ModelAnimationType Attention = new(GameEngineType.Eaw, 0x4d); - public static readonly ModelAnimationType Celebrate = new(GameEngineType.Eaw, 0x4e); - public static readonly ModelAnimationType FlinchLeft = new(GameEngineType.Eaw, 0x4f); - public static readonly ModelAnimationType FlinchRight = new(GameEngineType.Eaw, 0x50); - public static readonly ModelAnimationType FlinchFront = new(GameEngineType.Eaw, 0x51); - public static readonly ModelAnimationType FlinchBack = new(GameEngineType.Eaw, 0x52); - public static readonly ModelAnimationType AttackFlinchLeft = new(GameEngineType.Eaw, 0x53); - public static readonly ModelAnimationType AttackFlinchRight = new(GameEngineType.Eaw, 0x54); - public static readonly ModelAnimationType AttackFlinchFront = new(GameEngineType.Eaw, 0x55); - public static readonly ModelAnimationType AttackFlinchBack = new(GameEngineType.Eaw, 0x56); - public static readonly ModelAnimationType Talk = new(GameEngineType.Eaw, 0x57); - public static readonly ModelAnimationType TalkGesture = new(GameEngineType.Eaw, 0x58); - public static readonly ModelAnimationType TalkQuestion = new(GameEngineType.Eaw, 0x59); - public static readonly ModelAnimationType Hacking = new(GameEngineType.Eaw, 0x5a); - public static readonly ModelAnimationType Repairing = new(GameEngineType.Eaw, 0x5b); - public static readonly ModelAnimationType Choke = new(GameEngineType.Eaw, 0x5c); - public static readonly ModelAnimationType ChokeDie = new(GameEngineType.Eaw, 0x5d); - public static readonly ModelAnimationType DropTroopers = new(GameEngineType.Eaw, 0x5e); - public static readonly ModelAnimationType RopeSlide = new(GameEngineType.Eaw, 0x5f); - public static readonly ModelAnimationType RopeLand = new(GameEngineType.Eaw, 0x60); - public static readonly ModelAnimationType RopeDrop = new(GameEngineType.Eaw, 0x61); - public static readonly ModelAnimationType RopeLift = new(GameEngineType.Eaw, 0x62); - public static readonly ModelAnimationType Alarm = new(GameEngineType.Eaw, 0x63); - public static readonly ModelAnimationType Warning = new(GameEngineType.Eaw, 0x64); - public static readonly ModelAnimationType Crushed = new(GameEngineType.Eaw, 0x65); - public static readonly ModelAnimationType PowerDown = new(GameEngineType.Eaw, 0x66); - public static readonly ModelAnimationType PowerUp = new(GameEngineType.Eaw, 0x67); -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/FocModelAnimationTypes.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/FocModelAnimationTypes.cs deleted file mode 100644 index 743491a..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/FocModelAnimationTypes.cs +++ /dev/null @@ -1,124 +0,0 @@ -namespace PG.StarWarsGame.Engine.Rendering.Animations; - -public static class FocModelAnimationTypes -{ - public static readonly ModelAnimationType Idle = new(GameEngineType.Foc, 0x0); - public static readonly ModelAnimationType SpaceIdle = new(GameEngineType.Foc, 0x1); - public static readonly ModelAnimationType Move = new(GameEngineType.Foc, 0x2); - public static readonly ModelAnimationType TurnLeft = new(GameEngineType.Foc, 0x3); - public static readonly ModelAnimationType TurnRight = new(GameEngineType.Foc, 0x4); - public static readonly ModelAnimationType Attack = new(GameEngineType.Foc, 0x5); - public static readonly ModelAnimationType AttackIdle = new(GameEngineType.Foc, 0x6); - public static readonly ModelAnimationType Die = new(GameEngineType.Foc, 0x7); - public static readonly ModelAnimationType Rotate = new(GameEngineType.Foc, 0x8); - public static readonly ModelAnimationType SpecialA = new(GameEngineType.Foc, 0x9); - public static readonly ModelAnimationType SpecialB = new(GameEngineType.Foc, 0xa); - public static readonly ModelAnimationType SpecialC = new(GameEngineType.Foc, 0xb); - public static readonly ModelAnimationType TransitionToLeftTurn = new(GameEngineType.Foc, 0xc); - public static readonly ModelAnimationType TransitionFromLeftTurn = new(GameEngineType.Foc, 0xd); - public static readonly ModelAnimationType TransitionToRightTurn = new(GameEngineType.Foc, 0xe); - public static readonly ModelAnimationType TransitionFromRightTurn = new(GameEngineType.Foc, 0xf); - public static readonly ModelAnimationType TransitionToMove = new(GameEngineType.Foc, 0x10); - public static readonly ModelAnimationType TransitionFromMoveFrame0 = new(GameEngineType.Foc, 0x11); - public static readonly ModelAnimationType TransitionFromMoveFrame40 = new(GameEngineType.Foc, 0x12); - public static readonly ModelAnimationType TransitionFromMoveFrame80 = new(GameEngineType.Foc, 0x13); - public static readonly ModelAnimationType TransitionFromMoveFrame120 = new(GameEngineType.Foc, 0x14); - public static readonly ModelAnimationType TurnLeftHalf = new(GameEngineType.Foc, 0x15); - public static readonly ModelAnimationType TurnLeftQuarter = new(GameEngineType.Foc, 0x16); - public static readonly ModelAnimationType TurnRightHalf = new(GameEngineType.Foc, 0x17); - public static readonly ModelAnimationType TurnRightQuarter = new(GameEngineType.Foc, 0x18); - public static readonly ModelAnimationType Deploy = new(GameEngineType.Foc, 0x19); - public static readonly ModelAnimationType Undeploy = new(GameEngineType.Foc, 0x1a); - public static readonly ModelAnimationType Cinematic = new(GameEngineType.Foc, 0x1b); - public static readonly ModelAnimationType BlockBlaster = new(GameEngineType.Foc, 0x1c); - public static readonly ModelAnimationType RedirectBlaster = new(GameEngineType.Foc, 0x1d); - public static readonly ModelAnimationType IdleBlockBlaster = new(GameEngineType.Foc, 0x1e); - public static readonly ModelAnimationType ForceWhirlwindAttack = new(GameEngineType.Foc, 0x1f); - public static readonly ModelAnimationType ForceWhirlwindDie = new(GameEngineType.Foc, 0x20); - public static readonly ModelAnimationType ForceTelekinesisAttack = new(GameEngineType.Foc, 0x21); - public static readonly ModelAnimationType ForceTelekinesisHold = new(GameEngineType.Foc, 0x22); - public static readonly ModelAnimationType ForceTelekinesisRelease = new(GameEngineType.Foc, 0x23); - public static readonly ModelAnimationType ForceTelekinesisDie = new(GameEngineType.Foc, 0x24); - public static readonly ModelAnimationType EarthquakeAttack = new(GameEngineType.Foc, 0x25); - public static readonly ModelAnimationType EarthquakeHold = new(GameEngineType.Foc, 0x26); - public static readonly ModelAnimationType EarthquakeRelease = new(GameEngineType.Foc, 0x27); - public static readonly ModelAnimationType ForceLightningAttack = new(GameEngineType.Foc, 0x28); - public static readonly ModelAnimationType ForceLightningDie = new(GameEngineType.Foc, 0x29); - public static readonly ModelAnimationType ForceRun = new(GameEngineType.Foc, 0x2a); - public static readonly ModelAnimationType TransportLanding = new(GameEngineType.Foc, 0x2b); - public static readonly ModelAnimationType TransportLeaving = new(GameEngineType.Foc, 0x2c); - public static readonly ModelAnimationType FlameAttack = new(GameEngineType.Foc, 0x2d); - public static readonly ModelAnimationType Demolition = new(GameEngineType.Foc, 0x2e); - public static readonly ModelAnimationType BombToss = new(GameEngineType.Foc, 0x2f); - public static readonly ModelAnimationType Jump = new(GameEngineType.Foc, 0x30); - public static readonly ModelAnimationType FlyIdle = new(GameEngineType.Foc, 0x31); - public static readonly ModelAnimationType FlyLand = new(GameEngineType.Foc, 0x32); - public static readonly ModelAnimationType LandIdle = new(GameEngineType.Foc, 0x33); - public static readonly ModelAnimationType Land = new(GameEngineType.Foc, 0x34); - public static readonly ModelAnimationType HcWin = new(GameEngineType.Foc, 0x35); - public static readonly ModelAnimationType HcLose = new(GameEngineType.Foc, 0x36); - public static readonly ModelAnimationType HcDraw = new(GameEngineType.Foc, 0x37); - public static readonly ModelAnimationType ShieldOn = new(GameEngineType.Foc, 0x38); - public static readonly ModelAnimationType ShieldOff = new(GameEngineType.Foc, 0x39); - public static readonly ModelAnimationType CableAttackDie = new(GameEngineType.Foc, 0x3a); - public static readonly ModelAnimationType DeployedCableAttackDie = new(GameEngineType.Foc, 0x3b); - public static readonly ModelAnimationType DeployedDie = new(GameEngineType.Foc, 0x3c); - public static readonly ModelAnimationType RunAroundOnFire = new(GameEngineType.Foc, 0x3d); - public static readonly ModelAnimationType FireDie = new(GameEngineType.Foc, 0x3e); - public static readonly ModelAnimationType PoundAttack = new(GameEngineType.Foc, 0x3f); - public static readonly ModelAnimationType EatAttack = new(GameEngineType.Foc, 0x40); - public static readonly ModelAnimationType EatDie = new(GameEngineType.Foc, 0x41); - public static readonly ModelAnimationType MoveWalk = new(GameEngineType.Foc, 0x42); - public static readonly ModelAnimationType MoveCrouch = new(GameEngineType.Foc, 0x43); - public static readonly ModelAnimationType StructureOpen = new(GameEngineType.Foc, 0x44); - public static readonly ModelAnimationType StructureHold = new(GameEngineType.Foc, 0x45); - public static readonly ModelAnimationType StructureClose = new(GameEngineType.Foc, 0x46); - public static readonly ModelAnimationType IdleCrouch = new(GameEngineType.Foc, 0x47); - public static readonly ModelAnimationType TurnLeftCrouch = new(GameEngineType.Foc, 0x48); - public static readonly ModelAnimationType TurnRightCrouch = new(GameEngineType.Foc, 0x49); - public static readonly ModelAnimationType Build = new(GameEngineType.Foc, 0x4a); - public static readonly ModelAnimationType TransitionOwnership = new(GameEngineType.Foc, 0x4b); - public static readonly ModelAnimationType SelfDestruct = new(GameEngineType.Foc, 0x4c); - public static readonly ModelAnimationType Attention = new(GameEngineType.Foc, 0x4d); - public static readonly ModelAnimationType Celebrate = new(GameEngineType.Foc, 0x4e); - public static readonly ModelAnimationType FlinchLeft = new(GameEngineType.Foc, 0x4f); - public static readonly ModelAnimationType FlinchRight = new(GameEngineType.Foc, 0x50); - public static readonly ModelAnimationType FlinchFront = new(GameEngineType.Foc, 0x51); - public static readonly ModelAnimationType FlinchBack = new(GameEngineType.Foc, 0x52); - public static readonly ModelAnimationType AttackFlinchLeft = new(GameEngineType.Foc, 0x53); - public static readonly ModelAnimationType AttackFlinchRight = new(GameEngineType.Foc, 0x54); - public static readonly ModelAnimationType AttackFlinchFront = new(GameEngineType.Foc, 0x55); - public static readonly ModelAnimationType AttackFlinchBack = new(GameEngineType.Foc, 0x56); - public static readonly ModelAnimationType Talk = new(GameEngineType.Foc, 0x57); - public static readonly ModelAnimationType TalkGesture = new(GameEngineType.Foc, 0x58); - public static readonly ModelAnimationType TalkQuestion = new(GameEngineType.Foc, 0x59); - public static readonly ModelAnimationType Hacking = new(GameEngineType.Foc, 0x5a); - public static readonly ModelAnimationType Repairing = new(GameEngineType.Foc, 0x5b); - public static readonly ModelAnimationType Choke = new(GameEngineType.Foc, 0x5c); - public static readonly ModelAnimationType ChokeDie = new(GameEngineType.Foc, 0x5d); - public static readonly ModelAnimationType DropTroopers = new(GameEngineType.Foc, 0x5e); - public static readonly ModelAnimationType RopeSlide = new(GameEngineType.Foc, 0x5f); - public static readonly ModelAnimationType RopeLand = new(GameEngineType.Foc, 0x60); - public static readonly ModelAnimationType RopeDrop = new(GameEngineType.Foc, 0x61); - public static readonly ModelAnimationType RopeLift = new(GameEngineType.Foc, 0x62); - public static readonly ModelAnimationType Alarm = new(GameEngineType.Foc, 0x63); - public static readonly ModelAnimationType Warning = new(GameEngineType.Foc, 0x64); - public static readonly ModelAnimationType Crushed = new(GameEngineType.Foc, 0x65); - public static readonly ModelAnimationType PowerDown = new(GameEngineType.Foc, 0x66); - public static readonly ModelAnimationType PowerUp = new(GameEngineType.Foc, 0x67); - public static readonly ModelAnimationType SpinMove = new(GameEngineType.Foc, 0x68); - public static readonly ModelAnimationType ForceRevealBegin = new(GameEngineType.Foc, 0x69); - public static readonly ModelAnimationType ForceRevealLoop = new(GameEngineType.Foc, 0x6a); - public static readonly ModelAnimationType ForceRevealEnd = new(GameEngineType.Foc, 0x6b); - public static readonly ModelAnimationType SaberThrow = new(GameEngineType.Foc, 0x6c); - public static readonly ModelAnimationType SaberControl = new(GameEngineType.Foc, 0x6d); - public static readonly ModelAnimationType SaberCatch = new(GameEngineType.Foc, 0x6e); - public static readonly ModelAnimationType SaberSpin = new(GameEngineType.Foc, 0x6f); - public static readonly ModelAnimationType ContaminateAttack = new(GameEngineType.Foc, 0x70); - public static readonly ModelAnimationType ContaminateLoop = new(GameEngineType.Foc, 0x71); - public static readonly ModelAnimationType ContaminateRelease = new(GameEngineType.Foc, 0x72); - public static readonly ModelAnimationType DeployedWalk = new(GameEngineType.Foc, 0x73); - public static readonly ModelAnimationType PadBuild = new(GameEngineType.Foc, 0x74); - public static readonly ModelAnimationType PadSell = new(GameEngineType.Foc, 0x75); - public static readonly ModelAnimationType Heal = new(GameEngineType.Foc, 0x76); -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/ModelAnimationType.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/ModelAnimationType.cs index 867ba08..31ecb22 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/ModelAnimationType.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/ModelAnimationType.cs @@ -1,47 +1,127 @@ -using System; +namespace PG.StarWarsGame.Engine.Rendering.Animations; -namespace PG.StarWarsGame.Engine.Rendering.Animations; - -public readonly struct ModelAnimationType : IEquatable +public enum ModelAnimationType { - public GameEngineType TargetEngine { get; } - - public int Value { get; } - - internal ModelAnimationType(GameEngineType engine, int value) - { - TargetEngine = engine; - Value = value; - } - - public override string ToString() - { - var nameLookup = SupportedModelAnimationTypes.GetAnimationTypesForEngine(TargetEngine); - return $"'{nameLookup[this]}' ({Value})"; - } - - public bool Equals(ModelAnimationType other) - { - return TargetEngine == other.TargetEngine && Value == other.Value; - } - - public override bool Equals(object? obj) - { - return obj is ModelAnimationType other && Equals(other); - } - - public override int GetHashCode() - { - return HashCode.Combine((int)TargetEngine, Value); - } - - public static bool operator ==(ModelAnimationType left, ModelAnimationType right) - { - return left.Equals(right); - } - - public static bool operator !=(ModelAnimationType left, ModelAnimationType right) - { - return !(left == right); - } -} \ No newline at end of file + Invalid = -1, + None = -1, + Idle = 0x0, + SpaceIdle = 0x1, + Move = 0x2, + TurnLeft = 0x3, + TurnRight = 0x4, + Attack = 0x5, + AttackIdle = 0x6, + Die = 0x7, + Rotate = 0x8, + SpecialA = 0x9, + SpecialB = 0xa, + SpecialC = 0xb, + TransitionToLeftTurn = 0xc, + TransitionFromLeftTurn = 0xd, + TransitionToRightTurn = 0xe, + TransitionFromRightTurn = 0xf, + TransitionToMove = 0x10, + TransitionFromMoveFrame0 = 0x11, + TransitionFromMoveFrame40 = 0x12, + TransitionFromMoveFrame80 = 0x13, + TransitionFromMoveFrame120 = 0x14, + TurnLeftHalf = 0x15, + TurnLeftQuarter = 0x16, + TurnRightHalf = 0x17, + TurnRightQuarter = 0x18, + Deploy = 0x19, + Undeploy = 0x1a, + Cinematic = 0x1b, + BlockBlaster = 0x1c, + RedirectBlaster = 0x1d, + IdleBlockBlaster = 0x1e, + ForceWhirlwindAttack = 0x1f, + ForceWhirlwindDie = 0x20, + ForceTelekinesisAttack = 0x21, + ForceTelekinesisHold = 0x22, + ForceTelekinesisRelease = 0x23, + ForceTelekinesisDie = 0x24, + EarthquakeAttack = 0x25, + EarthquakeHold = 0x26, + EarthquakeRelease = 0x27, + ForceLightningAttack = 0x28, + ForceLightningDie = 0x29, + ForceRun = 0x2a, + TransportLanding = 0x2b, + TransportLeaving = 0x2c, + FlameAttack = 0x2d, + Demolition = 0x2e, + BombToss = 0x2f, + Jump = 0x30, + FlyIdle = 0x31, + FlyLand = 0x32, + LandIdle = 0x33, + Land = 0x34, + HcWin = 0x35, + HcLose = 0x36, + HcDraw = 0x37, + ShieldOn = 0x38, + ShieldOff = 0x39, + CableAttackDie = 0x3a, + DeployedCableAttackDie = 0x3b, + DeployedDie = 0x3c, + RunAroundOnFire = 0x3d, + FireDie = 0x3e, + PoundAttack = 0x3f, + EatAttack = 0x40, + EatDie = 0x41, + MoveWalk = 0x42, + MoveCrouch = 0x43, + StructureOpen = 0x44, + StructureHold = 0x45, + StructureClose = 0x46, + IdleCrouch = 0x47, + TurnLeftCrouch = 0x48, + TurnRightCrouch = 0x49, + Build = 0x4a, + TransitionOwnership = 0x4b, + SelfDestruct = 0x4c, + Attention = 0x4d, + Celebrate = 0x4e, + FlinchLeft = 0x4f, + FlinchRight = 0x50, + FlinchFront = 0x51, + FlinchBack = 0x52, + AttackFlinchLeft = 0x53, + AttackFlinchRight = 0x54, + AttackFlinchFront = 0x55, + AttackFlinchBack = 0x56, + Talk = 0x57, + TalkGesture = 0x58, + TalkQuestion = 0x59, + Hacking = 0x5a, + Repairing = 0x5b, + Choke = 0x5c, + ChokeDie = 0x5d, + DropTroopers = 0x5e, + RopeSlide = 0x5f, + RopeLand = 0x60, + RopeDrop = 0x61, + RopeLift = 0x62, + Alarm = 0x63, + Warning = 0x64, + Crushed = 0x65, + PowerDown = 0x66, + PowerUp = 0x67, + // These are exclusive to FoC + SpinMove = 0x68, + ForceRevealBegin = 0x69, + ForceRevealLoop = 0x6a, + ForceRevealEnd = 0x6b, + SaberThrow = 0x6c, + SaberControl = 0x6d, + SaberCatch = 0x6e, + SaberSpin = 0x6f, + ContaminateAttack = 0x70, + ContaminateLoop = 0x71, + ContaminateRelease = 0x72, + DeployedWalk = 0x73, + PadBuild = 0x74, + PadSell = 0x75, + Heal = 0x76, +}; \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/SupportedModelAnimationTypes.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/SupportedModelAnimationTypes.cs index 2a7aa3f..302c2fb 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/SupportedModelAnimationTypes.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/SupportedModelAnimationTypes.cs @@ -1,11 +1,16 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; +using System.Linq; namespace PG.StarWarsGame.Engine.Rendering.Animations; public static class SupportedModelAnimationTypes { + // NB: The games uses two different string mappings for animations. + // The strings here are used to identify the animation file name. + // These strings have an underscore '_' as delimiter, + // while the animation names for the LUA functions (Play_Animation("ANIM NAME")) use a space ' ' as delimiter. + public static IReadOnlyDictionary GetAnimationTypesForEngine(GameEngineType engineType) { return engineType switch @@ -15,237 +20,135 @@ public static IReadOnlyDictionary GetAnimationTypesF _ => throw new NotSupportedException() }; } - - [SuppressMessage("ReSharper", "StringLiteralTypo")] - private static readonly Dictionary FocSupportedAnimations = new() - { - { FocModelAnimationTypes.Idle, "IDLE"}, - { FocModelAnimationTypes.SpaceIdle, "SPACE_IDLE"}, - { FocModelAnimationTypes.Move, "MOVE"}, - { FocModelAnimationTypes.TurnLeft, "TURNL"}, - { FocModelAnimationTypes.TurnRight, "TURNR"}, - { FocModelAnimationTypes.Attack, "ATTACK"}, - { FocModelAnimationTypes.AttackIdle, "ATTACKIDLE"}, - { FocModelAnimationTypes.Die, "DIE"}, - { FocModelAnimationTypes.Rotate, "ROTATE"}, - { FocModelAnimationTypes.SpecialA, "SPECIAL_A"}, - { FocModelAnimationTypes.SpecialB, "SPECIAL_B"}, - { FocModelAnimationTypes.SpecialC, "SPECIAL_C"}, - { FocModelAnimationTypes.TransitionToLeftTurn, "TURNL_BEGIN"}, - { FocModelAnimationTypes.TransitionFromLeftTurn, "TURNL_END"}, - { FocModelAnimationTypes.TransitionToRightTurn, "TURNR_BEGIN"}, - { FocModelAnimationTypes.TransitionFromRightTurn, "TURNR_END"}, - { FocModelAnimationTypes.TransitionToMove, "MOVESTART"}, - { FocModelAnimationTypes.TransitionFromMoveFrame0, "MOVE_ENDONE"}, - { FocModelAnimationTypes.TransitionFromMoveFrame40, "MOVE_ENDTWO"}, - { FocModelAnimationTypes.TransitionFromMoveFrame80, "MOVE_ENDTHREE"}, - { FocModelAnimationTypes.TransitionFromMoveFrame120, "MOVE_ENDFOUR"}, - { FocModelAnimationTypes.TurnLeftHalf, "TURNL_HALF"}, - { FocModelAnimationTypes.TurnLeftQuarter, "TURNL_QUARTER"}, - { FocModelAnimationTypes.TurnRightHalf, "TURNR_HALF"}, - { FocModelAnimationTypes.TurnRightQuarter, "TURNR_QUARTER"}, - { FocModelAnimationTypes.Deploy, "DEPLOY"}, - { FocModelAnimationTypes.Undeploy, "UNDEPLOY"}, - { FocModelAnimationTypes.Cinematic, "CINEMATIC"}, - { FocModelAnimationTypes.BlockBlaster, "BLOCK_BLASTER"}, - { FocModelAnimationTypes.RedirectBlaster, "REDIRECT_BLASTER"}, - { FocModelAnimationTypes.IdleBlockBlaster, "IDLE_BLOCKBLASTER"}, - { FocModelAnimationTypes.ForceWhirlwindAttack, "FW_ATTACK"}, - { FocModelAnimationTypes.ForceWhirlwindDie, "FW_DIE"}, - { FocModelAnimationTypes.ForceTelekinesisAttack, "FTK_ATTACK"}, - { FocModelAnimationTypes.ForceTelekinesisHold, "FTK_HOLD"}, - { FocModelAnimationTypes.ForceTelekinesisRelease, "FTK_RELEASE"}, - { FocModelAnimationTypes.ForceTelekinesisDie, "FTK_DIE"}, - { FocModelAnimationTypes.EarthquakeAttack, "FB_ATTACK"}, - { FocModelAnimationTypes.EarthquakeHold, "FB_HOLD"}, - { FocModelAnimationTypes.EarthquakeRelease, "FB_RELEASE"}, - { FocModelAnimationTypes.ForceLightningAttack, "FL_ATTACK"}, - { FocModelAnimationTypes.ForceLightningDie, "FL_DIE"}, - { FocModelAnimationTypes.ForceRun, "FORCE_RUN"}, - { FocModelAnimationTypes.TransportLanding, "LAND"}, - { FocModelAnimationTypes.TransportLeaving, "TAKEOFF"}, - { FocModelAnimationTypes.FlameAttack, "FLAME_ATTACK"}, - { FocModelAnimationTypes.Demolition, "DEMOLITION"}, - { FocModelAnimationTypes.BombToss, "BOMBTOSS"}, - { FocModelAnimationTypes.Jump, "JUMP"}, - { FocModelAnimationTypes.FlyIdle, "FLYIDLE"}, - { FocModelAnimationTypes.FlyLand, "FLYLAND"}, - { FocModelAnimationTypes.LandIdle, "FLYLANDIDLE"}, - { FocModelAnimationTypes.Land, "FLYLANDDROP"}, - { FocModelAnimationTypes.HcWin, "HC_WIN"}, - { FocModelAnimationTypes.HcLose, "HC_LOSE"}, - { FocModelAnimationTypes.HcDraw, "HC_DRAW"}, - { FocModelAnimationTypes.ShieldOn, "SHIELD_ON"}, - { FocModelAnimationTypes.ShieldOff, "SHIELD_OFF"}, - { FocModelAnimationTypes.CableAttackDie, "CA_DIE"}, - { FocModelAnimationTypes.DeployedCableAttackDie, "DEPLOYED_CA_DIE"}, - { FocModelAnimationTypes.DeployedDie, "DEPLOYED_DIE"}, - { FocModelAnimationTypes.RunAroundOnFire, "FIRE_MOVE"}, - { FocModelAnimationTypes.FireDie, "FIRE_DIE"}, - { FocModelAnimationTypes.PoundAttack, "POUND_ATTACK"}, - { FocModelAnimationTypes.EatAttack, "EAT_ATTACK"}, - { FocModelAnimationTypes.EatDie, "EATEN_DIE"}, - { FocModelAnimationTypes.MoveWalk, "WALKMOVE"}, - { FocModelAnimationTypes.MoveCrouch, "CROUCHMOVE"}, - { FocModelAnimationTypes.StructureOpen, "OPEN"}, - { FocModelAnimationTypes.StructureHold, "HOLD"}, - { FocModelAnimationTypes.StructureClose, "CLOSE"}, - { FocModelAnimationTypes.IdleCrouch, "CROUCHIDLE"}, - { FocModelAnimationTypes.TurnLeftCrouch, "CROUCHTURNL"}, - { FocModelAnimationTypes.TurnRightCrouch, "CROUCHTURNR"}, - { FocModelAnimationTypes.Build, "BUILD"}, - { FocModelAnimationTypes.TransitionOwnership, "TRANS"}, - { FocModelAnimationTypes.SelfDestruct, "SELF_DESTRUCT"}, - { FocModelAnimationTypes.Attention, "ATTENTION"}, - { FocModelAnimationTypes.Celebrate, "CELEBRATE"}, - { FocModelAnimationTypes.FlinchLeft, "FLINCHL"}, - { FocModelAnimationTypes.FlinchRight, "FLINCHR"}, - { FocModelAnimationTypes.FlinchFront, "FLINCHF"}, - { FocModelAnimationTypes.FlinchBack, "FLINCHB"}, - { FocModelAnimationTypes.AttackFlinchLeft, "ATTACKFLINCHL"}, - { FocModelAnimationTypes.AttackFlinchRight, "ATTACKFLINCHR"}, - { FocModelAnimationTypes.AttackFlinchFront, "ATTACKFLINCHF"}, - { FocModelAnimationTypes.AttackFlinchBack, "ATTACKFLINCHB"}, - { FocModelAnimationTypes.Talk, "TALK"}, - { FocModelAnimationTypes.TalkGesture, "TALKGESTURE"}, - { FocModelAnimationTypes.TalkQuestion, "TALKQUESTION"}, - { FocModelAnimationTypes.Hacking, "HACKING"}, - { FocModelAnimationTypes.Repairing, "REPAIRING"}, - { FocModelAnimationTypes.Choke, "CHOKE"}, - { FocModelAnimationTypes.ChokeDie, "CHOKEDEATH"}, - { FocModelAnimationTypes.DropTroopers, "TROOPDROP"}, - { FocModelAnimationTypes.RopeSlide, "ROPESLIDE"}, - { FocModelAnimationTypes.RopeLand, "ROPELAND"}, - { FocModelAnimationTypes.RopeDrop, "ROPE_DROP"}, - { FocModelAnimationTypes.RopeLift, "ROPE_LIFT"}, - { FocModelAnimationTypes.Alarm, "ALARM"}, - { FocModelAnimationTypes.Warning, "WARNING"}, - { FocModelAnimationTypes.Crushed, "CRUSHED"}, - { FocModelAnimationTypes.PowerDown, "POWERDOWN"}, - { FocModelAnimationTypes.PowerUp, "POWERUP"}, - { FocModelAnimationTypes.SpinMove, "SPINMOVE"}, - { FocModelAnimationTypes.ForceRevealBegin, "FORCE_REVEAL_BEGIN"}, - { FocModelAnimationTypes.ForceRevealLoop, "FORCE_REVEAL_LOOP"}, - { FocModelAnimationTypes.ForceRevealEnd, "FORCE_REVEAL_END"}, - { FocModelAnimationTypes.SaberThrow, "SWORD_THROW"}, - { FocModelAnimationTypes.SaberControl, "SWORD_CONTROL"}, - { FocModelAnimationTypes.SaberCatch, "SWORD_CATCH"}, - { FocModelAnimationTypes.SaberSpin, "SWORDSPIN"}, - { FocModelAnimationTypes.ContaminateAttack, "CONTAMINATE_ATTACK"}, - { FocModelAnimationTypes.ContaminateLoop, "CONTAMINATE_LOOP"}, - { FocModelAnimationTypes.ContaminateRelease, "CONTAMINATE_RELEASE"}, - { FocModelAnimationTypes.DeployedWalk, "WALK"}, - { FocModelAnimationTypes.PadBuild, "PAD_BUILD"}, - { FocModelAnimationTypes.PadSell, "PAD_SELL"}, - { FocModelAnimationTypes.Heal, "HEAL"}, - }; - - [SuppressMessage("ReSharper", "StringLiteralTypo")] + private static readonly Dictionary EawSupportedAnimations = new() { - { EawModelAnimationTypes.Idle, "IDLE"}, - { EawModelAnimationTypes.SpaceIdle, "SPACE_IDLE"}, - { EawModelAnimationTypes.Move, "MOVE"}, - { EawModelAnimationTypes.TurnLeft, "TURNL"}, - { EawModelAnimationTypes.TurnRight, "TURNR"}, - { EawModelAnimationTypes.Attack, "ATTACK"}, - { EawModelAnimationTypes.AttackIdle, "ATTACKIDLE"}, - { EawModelAnimationTypes.Die, "DIE"}, - { EawModelAnimationTypes.Rotate, "ROTATE"}, - { EawModelAnimationTypes.SpecialA, "SPECIAL_A"}, - { EawModelAnimationTypes.SpecialB, "SPECIAL_B"}, - { EawModelAnimationTypes.SpecialC, "SPECIAL_C"}, - { EawModelAnimationTypes.TransitionToLeftTurn, "TURNL_BEGIN"}, - { EawModelAnimationTypes.TransitionFromLeftTurn, "TURNL_END"}, - { EawModelAnimationTypes.TransitionToRightTurn, "TURNR_BEGIN"}, - { EawModelAnimationTypes.TransitionFromRightTurn, "TURNR_END"}, - { EawModelAnimationTypes.TransitionToMove, "MOVESTART"}, - { EawModelAnimationTypes.TransitionFromMoveFrame0, "MOVE_ENDONE"}, - { EawModelAnimationTypes.TransitionFromMoveFrame40, "MOVE_ENDTWO"}, - { EawModelAnimationTypes.TransitionFromMoveFrame80, "MOVE_ENDTHREE"}, - { EawModelAnimationTypes.TransitionFromMoveFrame120, "MOVE_ENDFOUR"}, - { EawModelAnimationTypes.TurnLeftHalf, "TURNL_HALF"}, - { EawModelAnimationTypes.TurnLeftQuarter, "TURNL_QUARTER"}, - { EawModelAnimationTypes.TurnRightHalf, "TURNR_HALF"}, - { EawModelAnimationTypes.TurnRightQuarter, "TURNR_QUARTER"}, - { EawModelAnimationTypes.Deploy, "DEPLOY"}, - { EawModelAnimationTypes.Undeploy, "UNDEPLOY"}, - { EawModelAnimationTypes.Cinematic, "CINEMATIC"}, - { EawModelAnimationTypes.BlockBlaster, "BLOCK_BLASTER"}, - { EawModelAnimationTypes.RedirectBlaster, "REDIRECT_BLASTER"}, - { EawModelAnimationTypes.IdleBlockBlaster, "IDLE_BLOCKBLASTER"}, - { EawModelAnimationTypes.ForceWhirlwindAttack, "FW_ATTACK"}, - { EawModelAnimationTypes.ForceWhirlwindDie, "FW_DIE"}, - { EawModelAnimationTypes.ForceTelekinesisAttack, "FTK_ATTACK"}, - { EawModelAnimationTypes.ForceTelekinesisHold, "FTK_HOLD"}, - { EawModelAnimationTypes.ForceTelekinesisRelease, "FTK_RELEASE"}, - { EawModelAnimationTypes.ForceTelekinesisDie, "FTK_DIE"}, - { EawModelAnimationTypes.EarthquakeAttack, "FB_ATTACK"}, - { EawModelAnimationTypes.EarthquakeHold, "FB_HOLD"}, - { EawModelAnimationTypes.EarthquakeRelease, "FB_RELEASE"}, - { EawModelAnimationTypes.ForceLightningAttack, "FL_ATTACK"}, - { EawModelAnimationTypes.ForceLightningDie, "FL_DIE"}, - { EawModelAnimationTypes.ForceRun, "FORCE_RUN"}, - { EawModelAnimationTypes.TransportLanding, "LAND"}, - { EawModelAnimationTypes.TransportLeaving, "TAKEOFF"}, - { EawModelAnimationTypes.FlameAttack, "FLAME_ATTACK"}, - { EawModelAnimationTypes.Demolition, "DEMOLITION"}, - { EawModelAnimationTypes.BombToss, "BOMBTOSS"}, - { EawModelAnimationTypes.Jump, "JUMP"}, - { EawModelAnimationTypes.FlyIdle, "FLYIDLE"}, - { EawModelAnimationTypes.FlyLand, "FLYLAND"}, - { EawModelAnimationTypes.LandIdle, "FLYLANDIDLE"}, - { EawModelAnimationTypes.Land, "FLYLANDDROP"}, - { EawModelAnimationTypes.HcWin, "HC_WIN"}, - { EawModelAnimationTypes.HcLose, "HC_LOSE"}, - { EawModelAnimationTypes.HcDraw, "HC_DRAW"}, - { EawModelAnimationTypes.ShieldOn, "SHIELD_ON"}, - { EawModelAnimationTypes.ShieldOff, "SHIELD_OFF"}, - { EawModelAnimationTypes.CableAttackDie, "CA_DIE"}, - { EawModelAnimationTypes.DeployedCableAttackDie, "DEPLOYED_CA_DIE"}, - { EawModelAnimationTypes.DeployedDie, "DEPLOYED_DIE"}, - { EawModelAnimationTypes.RunAroundOnFire, "FIRE_MOVE"}, - { EawModelAnimationTypes.FireDie, "FIRE_DIE"}, - { EawModelAnimationTypes.PoundAttack, "POUND_ATTACK"}, - { EawModelAnimationTypes.EatAttack, "EAT_ATTACK"}, - { EawModelAnimationTypes.EatDie, "EATEN_DIE"}, - { EawModelAnimationTypes.MoveWalk, "WALKMOVE"}, - { EawModelAnimationTypes.MoveCrouch, "CROUCHMOVE"}, - { EawModelAnimationTypes.StructureOpen, "OPEN"}, - { EawModelAnimationTypes.StructureHold, "HOLD"}, - { EawModelAnimationTypes.StructureClose, "CLOSE"}, - { EawModelAnimationTypes.IdleCrouch, "CROUCHIDLE"}, - { EawModelAnimationTypes.TurnLeftCrouch, "CROUCHTURNL"}, - { EawModelAnimationTypes.TurnRightCrouch, "CROUCHTURNR"}, - { EawModelAnimationTypes.Build, "BUILD"}, - { EawModelAnimationTypes.TransitionOwnership, "TRANS"}, - { EawModelAnimationTypes.SelfDestruct, "SELF_DESTRUCT"}, - { EawModelAnimationTypes.Attention, "ATTENTION"}, - { EawModelAnimationTypes.Celebrate, "CELEBRATE"}, - { EawModelAnimationTypes.FlinchLeft, "FLINCHL"}, - { EawModelAnimationTypes.FlinchRight, "FLINCHR"}, - { EawModelAnimationTypes.FlinchFront, "FLINCHF"}, - { EawModelAnimationTypes.FlinchBack, "FLINCHB"}, - { EawModelAnimationTypes.AttackFlinchLeft, "ATTACKFLINCHL"}, - { EawModelAnimationTypes.AttackFlinchRight, "ATTACKFLINCHR"}, - { EawModelAnimationTypes.AttackFlinchFront, "ATTACKFLINCHF"}, - { EawModelAnimationTypes.AttackFlinchBack, "ATTACKFLINCHB"}, - { EawModelAnimationTypes.Talk, "TALK"}, - { EawModelAnimationTypes.TalkGesture, "TALKGESTURE"}, - { EawModelAnimationTypes.TalkQuestion, "TALKQUESTION"}, - { EawModelAnimationTypes.Hacking, "HACKING"}, - { EawModelAnimationTypes.Repairing, "REPAIRING"}, - { EawModelAnimationTypes.Choke, "CHOKE"}, - { EawModelAnimationTypes.ChokeDie, "CHOKEDEATH"}, - { EawModelAnimationTypes.DropTroopers, "TROOPDROP"}, - { EawModelAnimationTypes.RopeSlide, "ROPESLIDE"}, - { EawModelAnimationTypes.RopeLand, "ROPELAND"}, - { EawModelAnimationTypes.RopeDrop, "ROPE_DROP"}, - { EawModelAnimationTypes.RopeLift, "ROPE_LIFT"}, - { EawModelAnimationTypes.Alarm, "ALARM"}, - { EawModelAnimationTypes.Warning, "WARNING"}, - { EawModelAnimationTypes.Crushed, "CRUSHED"}, - { EawModelAnimationTypes.PowerDown, "POWERDOWN"}, - { EawModelAnimationTypes.PowerUp, "POWERUP"} + { ModelAnimationType.Idle, "IDLE"}, + { ModelAnimationType.SpaceIdle, "SPACE_IDLE"}, + { ModelAnimationType.Move, "MOVE"}, + { ModelAnimationType.TurnLeft, "TURNL"}, + { ModelAnimationType.TurnRight, "TURNR"}, + { ModelAnimationType.Attack, "ATTACK"}, + { ModelAnimationType.AttackIdle, "ATTACKIDLE"}, + { ModelAnimationType.Die, "DIE"}, + { ModelAnimationType.Rotate, "ROTATE"}, + { ModelAnimationType.SpecialA, "SPECIAL_A"}, + { ModelAnimationType.SpecialB, "SPECIAL_B"}, + { ModelAnimationType.SpecialC, "SPECIAL_C"}, + { ModelAnimationType.TransitionToLeftTurn, "TURNL_BEGIN"}, + { ModelAnimationType.TransitionFromLeftTurn, "TURNL_END"}, + { ModelAnimationType.TransitionToRightTurn, "TURNR_BEGIN"}, + { ModelAnimationType.TransitionFromRightTurn, "TURNR_END"}, + { ModelAnimationType.TransitionToMove, "MOVESTART"}, + { ModelAnimationType.TransitionFromMoveFrame0, "MOVE_ENDONE"}, + { ModelAnimationType.TransitionFromMoveFrame40, "MOVE_ENDTWO"}, + { ModelAnimationType.TransitionFromMoveFrame80, "MOVE_ENDTHREE"}, + { ModelAnimationType.TransitionFromMoveFrame120, "MOVE_ENDFOUR"}, + { ModelAnimationType.TurnLeftHalf, "TURNL_HALF"}, + { ModelAnimationType.TurnLeftQuarter, "TURNL_QUARTER"}, + { ModelAnimationType.TurnRightHalf, "TURNR_HALF"}, + { ModelAnimationType.TurnRightQuarter, "TURNR_QUARTER"}, + { ModelAnimationType.Deploy, "DEPLOY"}, + { ModelAnimationType.Undeploy, "UNDEPLOY"}, + { ModelAnimationType.Cinematic, "CINEMATIC"}, + { ModelAnimationType.BlockBlaster, "BLOCK_BLASTER"}, + { ModelAnimationType.RedirectBlaster, "REDIRECT_BLASTER"}, + { ModelAnimationType.IdleBlockBlaster, "IDLE_BLOCKBLASTER"}, + { ModelAnimationType.ForceWhirlwindAttack, "FW_ATTACK"}, + { ModelAnimationType.ForceWhirlwindDie, "FW_DIE"}, + { ModelAnimationType.ForceTelekinesisAttack, "FTK_ATTACK"}, + { ModelAnimationType.ForceTelekinesisHold, "FTK_HOLD"}, + { ModelAnimationType.ForceTelekinesisRelease, "FTK_RELEASE"}, + { ModelAnimationType.ForceTelekinesisDie, "FTK_DIE"}, + { ModelAnimationType.EarthquakeAttack, "FB_ATTACK"}, + { ModelAnimationType.EarthquakeHold, "FB_HOLD"}, + { ModelAnimationType.EarthquakeRelease, "FB_RELEASE"}, + { ModelAnimationType.ForceLightningAttack, "FL_ATTACK"}, + { ModelAnimationType.ForceLightningDie, "FL_DIE"}, + { ModelAnimationType.ForceRun, "FORCE_RUN"}, + { ModelAnimationType.TransportLanding, "LAND"}, + { ModelAnimationType.TransportLeaving, "TAKEOFF"}, + { ModelAnimationType.FlameAttack, "FLAME_ATTACK"}, + { ModelAnimationType.Demolition, "DEMOLITION"}, + { ModelAnimationType.BombToss, "BOMBTOSS"}, + { ModelAnimationType.Jump, "JUMP"}, + { ModelAnimationType.FlyIdle, "FLYIDLE"}, + { ModelAnimationType.FlyLand, "FLYLAND"}, + { ModelAnimationType.LandIdle, "FLYLANDIDLE"}, + { ModelAnimationType.Land, "FLYLANDDROP"}, + { ModelAnimationType.HcWin, "HC_WIN"}, + { ModelAnimationType.HcLose, "HC_LOSE"}, + { ModelAnimationType.HcDraw, "HC_DRAW"}, + { ModelAnimationType.ShieldOn, "SHIELD_ON"}, + { ModelAnimationType.ShieldOff, "SHIELD_OFF"}, + { ModelAnimationType.CableAttackDie, "CA_DIE"}, + { ModelAnimationType.DeployedCableAttackDie, "DEPLOYED_CA_DIE"}, + { ModelAnimationType.DeployedDie, "DEPLOYED_DIE"}, + { ModelAnimationType.RunAroundOnFire, "FIRE_MOVE"}, + { ModelAnimationType.FireDie, "FIRE_DIE"}, + { ModelAnimationType.PoundAttack, "POUND_ATTACK"}, + { ModelAnimationType.EatAttack, "EAT_ATTACK"}, + { ModelAnimationType.EatDie, "EATEN_DIE"}, + { ModelAnimationType.MoveWalk, "WALKMOVE"}, + { ModelAnimationType.MoveCrouch, "CROUCHMOVE"}, + { ModelAnimationType.StructureOpen, "OPEN"}, + { ModelAnimationType.StructureHold, "HOLD"}, + { ModelAnimationType.StructureClose, "CLOSE"}, + { ModelAnimationType.IdleCrouch, "CROUCHIDLE"}, + { ModelAnimationType.TurnLeftCrouch, "CROUCHTURNL"}, + { ModelAnimationType.TurnRightCrouch, "CROUCHTURNR"}, + { ModelAnimationType.Build, "BUILD"}, + { ModelAnimationType.TransitionOwnership, "TRANS"}, + { ModelAnimationType.SelfDestruct, "SELF_DESTRUCT"}, + { ModelAnimationType.Attention, "ATTENTION"}, + { ModelAnimationType.Celebrate, "CELEBRATE"}, + { ModelAnimationType.FlinchLeft, "FLINCHL"}, + { ModelAnimationType.FlinchRight, "FLINCHR"}, + { ModelAnimationType.FlinchFront, "FLINCHF"}, + { ModelAnimationType.FlinchBack, "FLINCHB"}, + { ModelAnimationType.AttackFlinchLeft, "ATTACKFLINCHL"}, + { ModelAnimationType.AttackFlinchRight, "ATTACKFLINCHR"}, + { ModelAnimationType.AttackFlinchFront, "ATTACKFLINCHF"}, + { ModelAnimationType.AttackFlinchBack, "ATTACKFLINCHB"}, + { ModelAnimationType.Talk, "TALK"}, + { ModelAnimationType.TalkGesture, "TALKGESTURE"}, + { ModelAnimationType.TalkQuestion, "TALKQUESTION"}, + { ModelAnimationType.Hacking, "HACKING"}, + { ModelAnimationType.Repairing, "REPAIRING"}, + { ModelAnimationType.Choke, "CHOKE"}, + { ModelAnimationType.ChokeDie, "CHOKEDEATH"}, + { ModelAnimationType.DropTroopers, "TROOPDROP"}, + { ModelAnimationType.RopeSlide, "ROPESLIDE"}, + { ModelAnimationType.RopeLand, "ROPELAND"}, + { ModelAnimationType.RopeDrop, "ROPE_DROP"}, + { ModelAnimationType.RopeLift, "ROPE_LIFT"}, + { ModelAnimationType.Alarm, "ALARM"}, + { ModelAnimationType.Warning, "WARNING"}, + { ModelAnimationType.Crushed, "CRUSHED"}, + { ModelAnimationType.PowerDown, "POWERDOWN"}, + { ModelAnimationType.PowerUp, "POWERUP"} }; + + // ReSharper disable StringLiteralTypo + private static readonly Dictionary FocSupportedAnimations = + + EawSupportedAnimations.Concat(new Dictionary + { + { ModelAnimationType.SpinMove, "SPINMOVE" }, + { ModelAnimationType.ForceRevealBegin, "FORCE_REVEAL_BEGIN" }, + { ModelAnimationType.ForceRevealLoop, "FORCE_REVEAL_LOOP" }, + { ModelAnimationType.ForceRevealEnd, "FORCE_REVEAL_END" }, + { ModelAnimationType.SaberThrow, "SWORD_THROW" }, + { ModelAnimationType.SaberControl, "SWORD_CONTROL" }, + { ModelAnimationType.SaberCatch, "SWORD_CATCH" }, + { ModelAnimationType.SaberSpin, "SWORDSPIN" }, + { ModelAnimationType.ContaminateAttack, "CONTAMINATE_ATTACK" }, + { ModelAnimationType.ContaminateLoop, "CONTAMINATE_LOOP" }, + { ModelAnimationType.ContaminateRelease, "CONTAMINATE_RELEASE" }, + { ModelAnimationType.DeployedWalk, "WALK" }, + { ModelAnimationType.PadBuild, "PAD_BUILD" }, + { ModelAnimationType.PadSell, "PAD_SELL" }, + { ModelAnimationType.Heal, "HEAL" }, + }) + .ToDictionary(x => x.Key, x => x.Value); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/FontManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/FontManager.cs index 00ca4f2..75654f2 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/FontManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/FontManager.cs @@ -15,7 +15,10 @@ internal class FontManager : GameManagerBase, IFontManager private ISet _fontNames = null!; - public FontManager(GameRepository repository, GameEngineErrorReporterWrapper errorReporter, IServiceProvider serviceProvider) + public FontManager( + GameRepository repository, + GameEngineErrorReporterWrapper errorReporter, + IServiceProvider serviceProvider) : base(repository, errorReporter, serviceProvider) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -24,7 +27,14 @@ public FontManager(GameRepository repository, GameEngineErrorReporterWrapper err _fontManager = new NetFontManager(); } - public IReadOnlyCollection FontNames => [.._fontNames]; + public IReadOnlyCollection FontNames + { + get + { + ThrowIfNotInitialized(); + return [.._fontNames]; + } + } public FontData? CreateFont(string fontName, int size, bool bold, bool italic, bool staticSize, float stretchFactor) { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/WindowsFontManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/WindowsFontManager.cs index fe8f546..ef2344f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/WindowsFontManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/WindowsFontManager.cs @@ -28,7 +28,8 @@ public IEnumerable GetFontFamilies() return fonts; } - static IEnumerable<(Gdi32.ENUMLOGFONTEXDV lpelfe, Gdi32.ENUMTEXTMETRIC _, Gdi32.FontType __)> GetFonts(Gdi32.SafeHDC hdc) + private static IEnumerable<(Gdi32.ENUMLOGFONTEXDV lpelfe, Gdi32.ENUMTEXTMETRIC _, Gdi32.FontType __)> GetFonts( + Gdi32.SafeHDC hdc) { return Gdi32.EnumFontFamiliesEx(hdc, lfCharSet: CharacterSet.DEFAULT_CHARSET); } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Matrix3x4.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Matrix3x4.cs new file mode 100644 index 0000000..136f647 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Matrix3x4.cs @@ -0,0 +1,198 @@ +using System; +using System.Globalization; +using System.Numerics; + +namespace PG.StarWarsGame.Engine.Rendering; + +public struct Matrix3x4 : IEquatable +{ + public float M11; + public float M12; + public float M13; + public float M14; + public float M21; + public float M22; + public float M23; + public float M24; + public float M31; + public float M32; + public float M33; + public float M34; + + public static Matrix3x4 Identity { get; } = new( + 1f, 0.0f, 0.0f, 0.0f, + 0.0f, 1f, 0.0f, 0.0f, + 0.0f, 0.0f, 1f, 0.0f); + + /// Constructs a Matrix3x4 from the given components. + public Matrix3x4( + float m11, + float m12, + float m13, + float m14, + float m21, + float m22, + float m23, + float m24, + float m31, + float m32, + float m33, + float m34) + { + M11 = m11; + M12 = m12; + M13 = m13; + M14 = m14; + M21 = m21; + M22 = m22; + M23 = m23; + M24 = m24; + M31 = m31; + M32 = m32; + M33 = m33; + M34 = m34; + } + + public static Matrix3x4 Scale(Vector3 scale) + { + return Scale(scale.X, scale.Y, scale.Z); + } + + public static Matrix3x4 Scale(float xScale, float yScale, float zScale) + { + return new Matrix3x4 + { + M11 = xScale, M12 = 0, M13 = 0, M14 = 0, + M21 = 0, M22 = yScale, M23 = 0, M24 = 0, + M31 = 0, M32 = 0, M33 = zScale, M34 = 0 + }; + } + + public static Matrix3x4 CreateTranslation(Vector3 position) + { + return CreateTranslation(position.X, position.Y, position.Z); + } + + public static Matrix3x4 CreateTranslation(float xPosition, float yPosition, float zPosition) + { + return new Matrix3x4 + { + M11 = 1, M12 = 0, M13 = 0, M14 = xPosition, + M21 = 0, M22 = 1, M23 = 0, M24 = yPosition, + M31 = 0, M32 = 0, M33 = 1, M34 = zPosition + }; + } + + public static Matrix3x4 operator *(Matrix3x4 value1, Matrix3x4 value2) + { + Matrix3x4 matrix3x4; + matrix3x4.M11 = value1.M11 * value2.M11 + value1.M12 * value2.M21 + value1.M13 * value2.M31; + matrix3x4.M12 = value1.M11 * value2.M12 + value1.M12 * value2.M22 + value1.M13 * value2.M32; + matrix3x4.M13 = value1.M11 * value2.M13 + value1.M12 * value2.M23 + value1.M13 * value2.M33; + matrix3x4.M14 = value1.M11 * value2.M14 + value1.M12 * value2.M24 + value1.M13 * value2.M34 + value1.M14; + matrix3x4.M21 = value1.M21 * value2.M11 + value1.M22 * value2.M21 + value1.M23 * value2.M31; + matrix3x4.M22 = value1.M21 * value2.M12 + value1.M22 * value2.M22 + value1.M23 * value2.M32; + matrix3x4.M23 = value1.M21 * value2.M13 + value1.M22 * value2.M23 + value1.M23 * value2.M33; + matrix3x4.M24 = value1.M21 * value2.M14 + value1.M22 * value2.M24 + value1.M23 * value2.M34 + value1.M24; + matrix3x4.M31 = value1.M31 * value2.M11 + value1.M32 * value2.M21 + value1.M33 * value2.M31; + matrix3x4.M32 = value1.M31 * value2.M12 + value1.M32 * value2.M22 + value1.M33 * value2.M32; + matrix3x4.M33 = value1.M31 * value2.M13 + value1.M32 * value2.M23 + value1.M33 * value2.M33; + matrix3x4.M34 = value1.M31 * value2.M14 + value1.M32 * value2.M24 + value1.M33 * value2.M34 + value1.M34; + return matrix3x4; + } + + public static Matrix3x4 operator *(Matrix3x4 value1, float value2) + { + Matrix3x4 matrix3x4; + matrix3x4.M11 = value1.M11 * value2; + matrix3x4.M12 = value1.M12 * value2; + matrix3x4.M13 = value1.M13 * value2; + matrix3x4.M14 = value1.M14 * value2; + matrix3x4.M21 = value1.M21 * value2; + matrix3x4.M22 = value1.M22 * value2; + matrix3x4.M23 = value1.M23 * value2; + matrix3x4.M24 = value1.M24 * value2; + matrix3x4.M31 = value1.M31 * value2; + matrix3x4.M32 = value1.M32 * value2; + matrix3x4.M33 = value1.M33 * value2; + matrix3x4.M34 = value1.M34 * value2; + return matrix3x4; + } + + /// + /// Returns a boolean indicating whether the given two matrices are equal. + /// + /// The first matrix to compare. + /// The second matrix to compare. + /// True if the given matrices are equal; False otherwise. + public static bool operator ==(Matrix3x4 value1, Matrix3x4 value2) + { + return value1.M11 == (double)value2.M11 && value1.M22 == (double)value2.M22 && value1.M33 == (double)value2.M33 && + value1.M12 == (double)value2.M12 && value1.M13 == (double)value2.M13 && value1.M14 == (double)value2.M14 && + value1.M21 == (double)value2.M21 && value1.M23 == (double)value2.M23 && value1.M24 == (double)value2.M24 && + value1.M31 == (double)value2.M31 && value1.M32 == (double)value2.M32 && value1.M34 == (double)value2.M34; + } + + /// + /// Returns a boolean indicating whether the given two matrices are not equal. + /// + /// The first matrix to compare. + /// The second matrix to compare. + /// True if the given matrices are not equal; False if they are equal. + public static bool operator !=(Matrix3x4 value1, Matrix3x4 value2) + { + return value1.M11 != (double)value2.M11 || value1.M12 != (double)value2.M12 || value1.M13 != (double)value2.M13 || value1.M14 != (double)value2.M14 || + value1.M21 != (double)value2.M21 || value1.M22 != (double)value2.M22 || value1.M23 != (double)value2.M23 || value1.M24 != (double)value2.M24 || + value1.M31 != (double)value2.M31 || value1.M32 != (double)value2.M32 || value1.M33 != (double)value2.M33 || value1.M34 != (double)value2.M34; + } + + /// + /// Returns a boolean indicating whether this matrix instance is equal to the other given matrix. + /// + /// The matrix to compare this instance to. + /// True if the matrices are equal; False otherwise. + public bool Equals(Matrix3x4 other) + { + return M11 == (double)other.M11 && M22 == (double)other.M22 && M33 == (double)other.M33 && + M12 == (double)other.M12 && M13 == (double)other.M13 && M14 == (double)other.M14 && + M21 == (double)other.M21 && M23 == (double)other.M23 && M24 == (double)other.M24 && + M31 == (double)other.M31 && M32 == (double)other.M32 && M34 == (double)other.M34; + } + + /// + /// Returns a boolean indicating whether the given Object is equal to this matrix instance. + /// + /// The Object to compare against. + /// True if the Object is equal to this matrix; False otherwise. + public override bool Equals(object? obj) => obj is Matrix3x4 other && Equals(other); + + /// Returns a String representing this matrix instance. + /// The string representation. + public override string ToString() + { + var currentCulture = CultureInfo.CurrentCulture; + return string.Format(currentCulture, + "{{ {{M11:{0} M12:{1} M13:{2} M14:{3}}} {{M21:{4} M22:{5} M23:{6} M24:{7}}} {{M31:{8} M32:{9} M33:{10} M34:{11}}}", + M11.ToString(currentCulture), + M12.ToString(currentCulture), + M13.ToString(currentCulture), + M14.ToString(currentCulture), + M21.ToString(currentCulture), + M22.ToString(currentCulture), + M23.ToString(currentCulture), + M24.ToString(currentCulture), + M31.ToString(currentCulture), + M32.ToString(currentCulture), + M33.ToString(currentCulture), + M34.ToString(currentCulture)); + } + + /// Returns the hash code for this instance. + /// The hash code. + public override int GetHashCode() + { + return M11.GetHashCode() + M12.GetHashCode() + M13.GetHashCode() + M14.GetHashCode() + + M21.GetHashCode() + M22.GetHashCode() + M23.GetHashCode() + M24.GetHashCode() + + M31.GetHashCode() + M32.GetHashCode() + M33.GetHashCode() + M34.GetHashCode(); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/ExtensionMethods.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/ExtensionMethods.cs new file mode 100644 index 0000000..e67024f --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/ExtensionMethods.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace PG.StarWarsGame.Engine.Utilities; + +internal static class ExtensionMethods +{ + extension(List list) + { + public void ClearAddRange(IEnumerable items) + { + list.Clear(); + list.AddRange(items); + } + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/PGMath.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/PGMath.cs new file mode 100644 index 0000000..bacabc3 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/PGMath.cs @@ -0,0 +1,12 @@ +using System; + +namespace PG.StarWarsGame.Engine.Utilities; + +internal static class PGMath +{ +#if NETSTANDARD2_1_OR_GREATER || NET + public static float Floor(float value) => MathF.Floor(value); +#else + public static float Floor(float value) => (float)Math.Floor(value); +#endif +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/EnumConversionDictionary.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/EnumConversionDictionary.cs new file mode 100644 index 0000000..275fea8 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/EnumConversionDictionary.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace PG.StarWarsGame.Engine.Xml; + +public sealed class EnumConversionDictionary : IReadOnlyCollection> where T : struct, Enum +{ + // This is the value the engine would give you. + internal const string StringNotFoundDummy = "-BAD VALUE-"; + + // Most consumers call the method TryStringToEnum. Thus, we optimize the class for this case + // and accept performance penalties for the EnumToString case. + private readonly IReadOnlyDictionary _dictionary; + + public int Count => _dictionary.Count; + + public EnumConversionDictionary(IEnumerable> entries) + { + var dictionary = new Dictionary(); + var values = dictionary.Values; + foreach (var entry in entries) + { + if (values.Contains(entry.Value)) + throw new InvalidOperationException($"Enum value {entry.Value} already exists!"); + dictionary.Add(entry.Key.ToUpperInvariant(), entry.Value); + } + _dictionary = dictionary; + } + + public bool TryStringToEnum(string key, out T enumValue) + { + key = key.ToUpperInvariant(); + return _dictionary.TryGetValue(key, out enumValue); + } + + public string EnumToString(T enumValue) + { + foreach (var keyValuePair in _dictionary) + { + if (EqualityComparer.Default.Equals(enumValue, keyValuePair.Value)) + return keyValuePair.Key; + } + + return StringNotFoundDummy; + } + + public IEnumerator> GetEnumerator() + { + return _dictionary.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/IPetroglyphXmlFileParserFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/IPetroglyphXmlFileParserFactory.cs index 4532365..787535a 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/IPetroglyphXmlFileParserFactory.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/IPetroglyphXmlFileParserFactory.cs @@ -1,9 +1,10 @@ -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; +using PG.StarWarsGame.Files.XML.Data; +using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Engine.Xml; public interface IPetroglyphXmlFileParserFactory -{ - IPetroglyphXmlFileContainerParser CreateFileParser(IXmlParserErrorReporter? errorReporter) where T : notnull; +{ + NamedXmlObjectParser CreateNamedXmlObjectParser(GameEngineType engine, IXmlParserErrorReporter? errorReporter) + where T : NamedXmlObject; } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/NamedXmlObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/NamedXmlObjectParser.cs new file mode 100644 index 0000000..d397eb2 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/NamedXmlObjectParser.cs @@ -0,0 +1,74 @@ +using System; +using System.Xml.Linq; +using AnakinRaW.CommonUtilities.Collections; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PG.Commons.Hashing; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Data; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.Parsers; + +namespace PG.StarWarsGame.Engine.Xml; + +public abstract class NamedXmlObjectParser : + XmlObjectParserBase>, INamedXmlObjectParser + where T : NamedXmlObject +{ + protected abstract bool UpperCaseNameForCrc { get; } + protected abstract bool UpperCaseNameForObject { get; } + + protected readonly ICrc32HashingService HashingService; + + protected readonly ILogger? Logger; + + protected NamedXmlObjectParser( + GameEngineType engine, + XmlTagMapper tagMapper, + IXmlParserErrorReporter? errorReporter, + IServiceProvider serviceProvider) : base(engine, tagMapper, errorReporter) + { + HashingService = serviceProvider.GetRequiredService(); + Logger = serviceProvider.GetService()?.CreateLogger(GetType()); + } + + public T Parse(XElement element, IReadOnlyFrugalValueListDictionary parsedEntries, out Crc32 nameCrc) + { + var name = GetXmlObjectName(element, out nameCrc); + var namedXmlObject = CreateXmlObject(name, nameCrc, element, parsedEntries, XmlLocationInfo.FromElement(element)); + ParseObject(namedXmlObject, element, false, parsedEntries); + ValidateAndFixupValues(namedXmlObject, element, parsedEntries); + return namedXmlObject; + } + + protected abstract T CreateXmlObject( + string name, + Crc32 nameCrc, + XElement element, + IReadOnlyFrugalValueListDictionary parsedEntries, + XmlLocationInfo location); + + protected virtual Crc32 CreateNameCrc(string name) + { + return UpperCaseNameForCrc + ? HashingService.GetCrc32Upper(name.AsSpan(), XmlFileConstants.XmlEncoding) + : HashingService.GetCrc32(name.AsSpan(), XmlFileConstants.XmlEncoding); + } + + protected string GetXmlObjectName(XElement element, out Crc32 crc32) + { + GetNameAttributeValue(element, out var name, UpperCaseNameForObject); + crc32 = CreateNameCrc(name); + + if (crc32 == default) + { + ErrorReporter?.Report(new XmlError(this, element) + { + Message = $"Name for XML object of type {typeof(T).Name} cannot be empty.", + ErrorKind = XmlParseErrorKind.InvalidValue + }); + } + + return name; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/ParserNotFoundException.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/ParserNotFoundException.cs index 19bc797..fd4fba3 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/ParserNotFoundException.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/ParserNotFoundException.cs @@ -2,7 +2,7 @@ namespace PG.StarWarsGame.Engine.Xml; -public sealed class ParserNotFoundException : Exception +internal sealed class ParserNotFoundException : Exception { public override string Message { get; } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/CommandBarComponentParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/CommandBarComponentParser.cs deleted file mode 100644 index 297b474..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/CommandBarComponentParser.cs +++ /dev/null @@ -1,365 +0,0 @@ -using System; -using System.Collections.ObjectModel; -using System.Xml.Linq; -using AnakinRaW.CommonUtilities.Collections; -using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.CommandBar.Xml; -using PG.StarWarsGame.Engine.Xml.Tags; -using PG.StarWarsGame.Files.XML; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; - -public sealed class CommandBarComponentParser( - IReadOnlyFrugalValueListDictionary parsedElements, - IServiceProvider serviceProvider, - IXmlParserErrorReporter? errorReporter = null) - : XmlObjectParser(parsedElements, serviceProvider, errorReporter) -{ - public override CommandBarComponentData Parse(XElement element, out Crc32 crc32) - { - var name = GetXmlObjectName(element, out crc32, true); - var component = new CommandBarComponentData(name, crc32, XmlLocationInfo.FromElement(element)); - Parse(component, element, default); - ValidateValues(component, element); - component.CoerceValues(); - return component; - } - - protected override bool ParseTag(XElement tag, CommandBarComponentData componentData) - { - switch (tag.Name.LocalName) - { - case CommandBarComponentTags.SelectedTextureName: - componentData.SelectedTextureNames = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case CommandBarComponentTags.BlankTextureName: - componentData.BlankTextureNames = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case CommandBarComponentTags.IconAlternateTextureName: - componentData.IconAlternateTextureNames = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case CommandBarComponentTags.MouseOverTextureName: - componentData.MouseOverTextureNames = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case CommandBarComponentTags.BarTextureName: - componentData.BarTextureNames = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case CommandBarComponentTags.BarOverlayName: - componentData.BarOverlayNames = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case CommandBarComponentTags.AlternateFontName: - componentData.AlternateFontNames = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case CommandBarComponentTags.TooltipText: - componentData.TooltipTexts = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case CommandBarComponentTags.LowerEffectTextureName: - componentData.LowerEffectTextureNames = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case CommandBarComponentTags.UpperEffectTextureName: - componentData.UpperEffectTextureNames = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case CommandBarComponentTags.OverlayTextureName: - componentData.OverlayTextureNames = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case CommandBarComponentTags.Overlay2TextureName: - componentData.Overlay2TextureNames = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - - case CommandBarComponentTags.IconTextureName: - componentData.IconTextureName = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.DisabledTextureName: - componentData.DisabledTextureName = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.FlashTextureName: - componentData.FlashTextureName = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.BuildTextureName: - componentData.BuildTextureName = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.ModelName: - componentData.ModelName = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.BoneName: - componentData.BoneName = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.CursorTextureName: - componentData.CursorTextureName = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.FontName: - componentData.FontName = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.ClickSfx: - componentData.ClickSfx = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.MouseOverSfx: - componentData.MouseOverSfx = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.RightClickSfx: - componentData.RightClickSfx = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.Type: - componentData.Type = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.Group: - componentData.Group = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.AssociatedText: - componentData.AssociatedText = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - - case CommandBarComponentTags.DragAndDrop: - componentData.DragAndDrop = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.DragSelect: - componentData.DragSelect = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.Receptor: - componentData.Receptor = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.Toggle: - componentData.Toggle = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.Tab: - componentData.Tab = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.Hidden: - componentData.Hidden = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.ClearColor: - componentData.ClearColor = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.Disabled: - componentData.Disabled = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.SwapTexture: - componentData.SwapTexture = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.DrawAdditive: - componentData.DrawAdditive = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.Editable: - componentData.Editable = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.TextOutline: - componentData.TextOutline = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.Stackable: - componentData.Stackable = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.ModelOffsetX: - componentData.ModelOffsetX = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.ModelOffsetY: - componentData.ModelOffsetY = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.ScaleModelX: - componentData.ScaleModelX = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.ScaleModelY: - componentData.ScaleModelY = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.Collideable: - componentData.Collideable = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.TextEmboss: - componentData.TextEmboss = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.ShouldGhost: - componentData.ShouldGhost = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.GhostBaseOnly: - componentData.GhostBaseOnly = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.CrossFade: - componentData.CrossFade = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.LeftJustified: - componentData.LeftJustified = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.RightJustified: - componentData.RightJustified = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.NoShell: - componentData.NoShell = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.SnapDrag: - componentData.SnapDrag = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.SnapLocation: - componentData.SnapLocation = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.OffsetRender: - componentData.OffsetRender = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.BlinkFade: - componentData.BlinkFade = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.NoHiddenCollision: - componentData.NoHiddenCollision = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.ManualOffset: - componentData.ManualOffset = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.SelectedAlpha: - componentData.SelectedAlpha = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.PixelAlign: - componentData.PixelAlign = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.CanDragStack: - componentData.CanDragStack = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.CanAnimate: - componentData.CanAnimate = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.LoopAnim: - componentData.LoopAnim = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.SmoothBar: - componentData.SmoothBar = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.OutlinedBar: - componentData.OutlinedBar = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.DragBack: - componentData.DragBack = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.LowerEffectAdditive: - componentData.LowerEffectAdditive = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.UpperEffectAdditive: - componentData.UpperEffectAdditive = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.ClickShift: - componentData.ClickShift = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.TutorialScene: - componentData.TutorialScene = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.DialogScene: - componentData.DialogScene = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.ShouldRenderAtDragPos: - componentData.ShouldRenderAtDragPos = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.DisableDarken: - componentData.DisableDarken = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.AnimateBack: - componentData.AnimateBack = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.AnimateUpperEffect: - componentData.AnimateUpperEffect = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - - case CommandBarComponentTags.Size: - componentData.Size = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.TextOffset: - componentData.TextOffset = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.TextOffset2: - componentData.TextOffset2 = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.Offset: - componentData.Offset = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.DefaultOffset: - componentData.DefaultOffset = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.DefaultOffsetWidescreen: - componentData.DefaultOffsetWidescreen = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.IconOffset: - componentData.IconOffset = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.MouseOverOffset: - componentData.MouseOverOffset = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.DisabledOffset: - componentData.DisabledOffset = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.BuildDialOffset: - componentData.BuildDialOffset = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.BuildDial2Offset: - componentData.BuildDial2Offset = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.LowerEffectOffset: - componentData.LowerEffectOffset = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.UpperEffectOffset: - componentData.UpperEffectOffset = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.OverlayOffset: - componentData.OverlayOffset = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.Overlay2Offset: - componentData.Overlay2Offset = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - - case CommandBarComponentTags.MaxTextLength: - componentData.MaxTextLength = PetroglyphXmlUnsignedIntegerParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.FontPointSize: - componentData.FontPointSize = (int)PetroglyphXmlUnsignedIntegerParser.Instance.Parse(tag); - return true; - - case CommandBarComponentTags.Scale: - componentData.Scale = PetroglyphXmlFloatParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.BlinkRate: - componentData.BlinkRate = PetroglyphXmlFloatParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.MaxTextWidth: - componentData.MaxTextWidth = PetroglyphXmlFloatParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.BlinkDuration: - componentData.BlinkDuration = PetroglyphXmlFloatParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.ScaleDuration: - componentData.ScaleDuration = PetroglyphXmlFloatParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.AnimFps: - componentData.AnimFps = PetroglyphXmlFloatParser.Instance.Parse(tag); - return true; - - case CommandBarComponentTags.BaseLayer: - componentData.BaseLayer = (int)PetroglyphXmlUnsignedIntegerParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.MaxBarLevel: - componentData.MaxBarLevel = (int)PetroglyphXmlUnsignedIntegerParser.Instance.Parse(tag); - return true; - - case CommandBarComponentTags.Color: - componentData.Color = PetroglyphXmlRgbaColorParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.TextColor: - componentData.TextColor = PetroglyphXmlRgbaColorParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.TextColor2: - componentData.TextColor2 = PetroglyphXmlRgbaColorParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.MaxBarColor: - componentData.MaxBarColor = PetroglyphXmlRgbaColorParser.Instance.Parse(tag); - return true; - - default: return true; - } - } - - private void ValidateValues(CommandBarComponentData xmlData, XElement element) - { - if (xmlData.Name.Length > PGConstants.MaxCommandBarComponentName) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.TooLongData, - $"CommandbarComponent name '{xmlData.Name}' is too long.")); - } - } - - public override CommandBarComponentData Parse(XElement element) => throw new NotSupportedException(); -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs deleted file mode 100644 index 5cac3e2..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using System.Xml.Linq; -using AnakinRaW.CommonUtilities.Collections; -using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.GameObjects; -using PG.StarWarsGame.Files.XML; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; - -public static class GameObjectXmlTags -{ - public const string LandTerrainModelMapping = "Land_Terrain_Model_Mapping"; - public const string GalacticModelName = "Galactic_Model_Name"; - public const string DestroyedGalacticModelName = "Destroyed_Galactic_Model_Name"; - public const string LandModelName = "Land_Model_Name"; - public const string SpaceModelName = "Space_Model_Name"; - public const string ModelName = "Model_Name"; - public const string TacticalModelName = "Tactical_Model_Name"; - public const string GalacticFleetOverrideModelName = "Galactic_Fleet_Override_Model_Name"; - public const string GuiModelName = "GUI_Model_Name"; - public const string LandModelAnimOverrideName = "Land_Model_Anim_Override_Name"; - public const string XxxSpaceModelName = "xxxSpace_Model_Name"; - public const string DamagedSmokeAssetName = "Damaged_Smoke_Asset_Name"; -} - -public sealed class GameObjectParser( - IReadOnlyFrugalValueListDictionary parsedElements, - IServiceProvider serviceProvider, - IXmlParserErrorReporter? errorReporter = null) - : XmlObjectParser(parsedElements, serviceProvider, errorReporter) -{ - public override GameObject Parse(XElement element, out Crc32 crc32) - { - var name = GetXmlObjectName(element, out crc32, true); - var type = GetTagName(element); - var objectType = EstimateType(type); - var gameObject = new GameObject(type, name, crc32, objectType, XmlLocationInfo.FromElement(element)); - - Parse(gameObject, element, default); - - return gameObject; - } - - protected override bool ParseTag(XElement tag, GameObject xmlObject) - { - switch (tag.Name.LocalName) - { - case GameObjectXmlTags.LandTerrainModelMapping: - var mappingValue = CommaSeparatedStringKeyValueListParser.Instance.Parse(tag); - var dict = xmlObject.InternalLandTerrainModelMapping; - foreach (var keyValuePair in mappingValue) - { - if (!dict.ContainsKey(keyValuePair.key)) - dict.Add(keyValuePair.key, keyValuePair.value); - } - return true; - case GameObjectXmlTags.GalacticModelName: - xmlObject.GalacticModel = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case GameObjectXmlTags.DestroyedGalacticModelName: - xmlObject.DestroyedGalacticModel = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case GameObjectXmlTags.LandModelName: - xmlObject.LandModel = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case GameObjectXmlTags.SpaceModelName: - xmlObject.SpaceModel = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case GameObjectXmlTags.ModelName: - xmlObject.ModelName = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case GameObjectXmlTags.TacticalModelName: - xmlObject.TacticalModel = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case GameObjectXmlTags.GalacticFleetOverrideModelName: - xmlObject.GalacticFleetOverrideModel = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case GameObjectXmlTags.GuiModelName: - xmlObject.GuiModel = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case GameObjectXmlTags.LandModelAnimOverrideName: - xmlObject.LandAnimOverrideModel = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case GameObjectXmlTags.XxxSpaceModelName: - xmlObject.XxxSpaceModeModel = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case GameObjectXmlTags.DamagedSmokeAssetName: - xmlObject.DamagedSmokeAssetModel = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - default: return true; // TODO: Once parsing is complete, switch to false. - } - } - - private static GameObjectType EstimateType(string tagName) - { - if (tagName.StartsWith("Props_")) - return GameObjectType.Prop; - if (tagName.StartsWith("CIN_", StringComparison.OrdinalIgnoreCase)) - return GameObjectType.CinematicObject; - - return tagName switch - { - "Container" => GameObjectType.Container, - "GenericHeroUnit" => GameObjectType.GenericHeroUnit, - "GroundBase" => GameObjectType.GroundBase, - "GroundBuildable" => GameObjectType.GroundBuildable, - "GroundCompany" => GameObjectType.GroundCompany, - "GroundInfantry" => GameObjectType.GroundInfantry, - "GroundStructure" => GameObjectType.GroundStructure, - "GroundVehicle" => GameObjectType.GroundVehicle, - "HeroCompany" => GameObjectType.HeroCompany, - "HeroUnit" => GameObjectType.HeroUnit, - "Indigenous_Unit" => GameObjectType.IndigenousUnit, - "LandBombingUnit" => GameObjectType.LandBombingUnit, - "LandPrimarySkydome" => GameObjectType.LandPrimarySkydome, - "LandSecondarySkydome" => GameObjectType.LandSecondarySkydome, - "Marker" => GameObjectType.Marker, - "MiscObject" => GameObjectType.MiscObject, - "Mobile_Defense_Unit" => GameObjectType.MobileDefenseUnit, - "MultiplayerStructureMarker" => GameObjectType.MultiplayerStructureMarker, - "Particle" => GameObjectType.Particle, - "Planet" => GameObjectType.Planet, - "Projectile" => GameObjectType.Projectile, - "ScriptMarker" => GameObjectType.ScriptMarker, - "SecondaryStructure" => GameObjectType.SecondaryStructure, - "SlaveCompany" => GameObjectType.SlaveCompany, - "Slave_Unit" => GameObjectType.SlaveUnit, - "SpaceBuildable" => GameObjectType.SpaceBuildable, - "SpacePrimarySkydome" => GameObjectType.SpacePrimarySkydome, - "SpaceProp" => GameObjectType.SpaceProp, - "SpaceSecondarySkydome" => GameObjectType.SpaceSecondarySkydome, - "SpaceUnit" => GameObjectType.SpaceUnit, - "SpecialEffect" => GameObjectType.SpecialEffect, - "SpecialStructure" => GameObjectType.SpecialStructure, - "Squadron" => GameObjectType.Squadron, - "StarBase" => GameObjectType.StarBase, - "TechBuilding" => GameObjectType.TechBuilding, - "TransportUnit" => GameObjectType.TransportUnit, - "UniqueUnit" => GameObjectType.UniqueUint, - "UpgradeObject" => GameObjectType.UpgradeUnit, - _ => GameObjectType.Unknown - }; - } - - public override GameObject Parse(XElement element) => throw new NotSupportedException(); -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs deleted file mode 100644 index bc3439f..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs +++ /dev/null @@ -1,204 +0,0 @@ -using System; -using System.Collections.ObjectModel; -using System.Xml.Linq; -using AnakinRaW.CommonUtilities.Collections; -using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.Audio.Sfx; -using PG.StarWarsGame.Engine.Xml.Tags; -using PG.StarWarsGame.Files.XML; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; - -public sealed class SfxEventParser( - IReadOnlyFrugalValueListDictionary parsedElements, - IServiceProvider serviceProvider, - IXmlParserErrorReporter? errorReporter = null) - : XmlObjectParser(parsedElements, serviceProvider, errorReporter) -{ - public override SfxEvent Parse(XElement element, out Crc32 crc32) - { - var name = GetXmlObjectName(element, out crc32, true); - var sfxEvent = new SfxEvent(name, crc32, XmlLocationInfo.FromElement(element)); - Parse(sfxEvent, element, default); - ValidateValues(sfxEvent, element); - sfxEvent.CoerceValues(); - return sfxEvent; - } - - private void ValidateValues(SfxEvent sfxEvent, XElement element) - { - if (sfxEvent.Name.Length > PGConstants.MaxSFXEventName) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.TooLongData, - $"SFXEvent name '{sfxEvent.Name}' is too long.")); - } - - if (sfxEvent is { Is2D: true, Is3D: true }) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"SFXEvent '{sfxEvent.Name}' is defined as 2D and 3D.")); - } - - if (sfxEvent.MinVolume > sfxEvent.MaxVolume) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"{SfxEventXmlTags.MinVolume} should not be higher than {SfxEventXmlTags.MaxVolume} for SFXEvent '{sfxEvent.Name}'")); - } - - if (sfxEvent.MinPitch > sfxEvent.MaxPitch) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"{SfxEventXmlTags.MinPitch} should not be higher than {SfxEventXmlTags.MaxPitch} for SFXEvent '{sfxEvent.Name}'")); - } - - if (sfxEvent.MinPan2D > sfxEvent.MaxPan2D) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"{SfxEventXmlTags.MinPan2D} should not be higher than {SfxEventXmlTags.MaxPan2D} for SFXEvent '{sfxEvent.Name}'")); - } - - if (sfxEvent.MinPredelay > sfxEvent.MaxPredelay) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"{SfxEventXmlTags.MinPredelay} should not be higher than {SfxEventXmlTags.MaxPredelay} for SFXEvent '{sfxEvent.Name}'")); - } - - if (sfxEvent.MinPostdelay > sfxEvent.MaxPostdelay) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"{SfxEventXmlTags.MinPostdelay} should not be higher than {SfxEventXmlTags.MaxPostdelay} for SFXEvent '{sfxEvent.Name}'")); - } - } - - protected override bool ParseTag(XElement tag, SfxEvent sfxEvent) - { - switch (tag.Name.LocalName) - { - case SfxEventXmlTags.OverlapTest: - sfxEvent.OverlapTestName = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.ChainedSfxEvent: - sfxEvent.ChainedSfxEventName = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.UsePreset: - { - var presetName = PetroglyphXmlStringParser.Instance.Parse(tag); - var presetNameCrc = HashingService.GetCrc32Upper(presetName.AsSpan(), PGConstants.DefaultPGEncoding); - if (presetNameCrc != default && ParsedElements.TryGetFirstValue(presetNameCrc, out var preset)) - sfxEvent.ApplyPreset(preset); - else - { - OnParseError(new XmlParseErrorEventArgs(tag, - XmlParseErrorKind.MissingReference, $"Cannot to find preset '{presetName}' for SFXEvent '{sfxEvent.Name}'")); - } - return true; - } - - case SfxEventXmlTags.IsPreset: - sfxEvent.IsPreset = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.Is3D: - sfxEvent.Is3D = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.Is2D: - sfxEvent.Is2D = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.IsGui: - sfxEvent.IsGui = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.IsHudVo: - sfxEvent.IsHudVo = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.IsUnitResponseVo: - sfxEvent.IsUnitResponseVo = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.IsAmbientVo: - sfxEvent.IsAmbientVo = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.Localize: - sfxEvent.IsLocalized = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.PlaySequentially: - sfxEvent.PlaySequentially = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.KillsPreviousObjectSFX: - sfxEvent.KillsPreviousObjectsSfx = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - - case SfxEventXmlTags.Samples: - sfxEvent.Samples = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case SfxEventXmlTags.PreSamples: - sfxEvent.PreSamples = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case SfxEventXmlTags.PostSamples: - sfxEvent.PostSamples = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case SfxEventXmlTags.TextID: - sfxEvent.LocalizedTextIDs = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - - case SfxEventXmlTags.Priority: - sfxEvent.Priority = (byte)PetroglyphXmlIntegerParser.Instance.ParseWithRange(tag, SfxEvent.MinPriorityValue, SfxEvent.MaxPriorityValue); - return true; - case SfxEventXmlTags.MinPitch: - sfxEvent.MinPitch = (byte)PetroglyphXmlIntegerParser.Instance.ParseWithRange(tag, SfxEvent.MinPitchValue, SfxEvent.MaxPitchValue); - return true; - case SfxEventXmlTags.MaxPitch: - sfxEvent.MaxPitch = (byte)PetroglyphXmlIntegerParser.Instance.ParseWithRange(tag, SfxEvent.MinPitchValue, SfxEvent.MaxPitchValue); - return true; - case SfxEventXmlTags.MinPan2D: - sfxEvent.MinPan2D = (byte)PetroglyphXmlIntegerParser.Instance.ParseWithRange(tag, byte.MinValue, SfxEvent.MaxPan2dValue); - return true; - case SfxEventXmlTags.MaxPan2D: - sfxEvent.MaxPan2D = (byte)PetroglyphXmlIntegerParser.Instance.ParseWithRange(tag, byte.MinValue, SfxEvent.MaxPan2dValue); - return true; - case SfxEventXmlTags.PlayCount: - sfxEvent.PlayCount = (sbyte)PetroglyphXmlIntegerParser.Instance.ParseWithRange(tag, SfxEvent.InfinitivePlayCount, sbyte.MaxValue); - return true; - case SfxEventXmlTags.MaxInstances: - sfxEvent.MaxInstances = (sbyte)PetroglyphXmlIntegerParser.Instance.ParseWithRange(tag, SfxEvent.MinMaxInstances, sbyte.MaxValue); - return true; - - case SfxEventXmlTags.Probability: - sfxEvent.Probability = PetroglyphXmlMax100ByteParser.Instance.ParseWithRange(tag, byte.MinValue, SfxEvent.MaxProbability); - return true; - case SfxEventXmlTags.MinVolume: - sfxEvent.MinVolume = PetroglyphXmlMax100ByteParser.Instance.ParseWithRange(tag, byte.MinValue, SfxEvent.MaxVolumeValue); - return true; - case SfxEventXmlTags.MaxVolume: - sfxEvent.MaxVolume = PetroglyphXmlMax100ByteParser.Instance.ParseWithRange(tag, byte.MinValue, SfxEvent.MaxVolumeValue); - return true; - - case SfxEventXmlTags.MinPredelay: - sfxEvent.MinPredelay = PetroglyphXmlUnsignedIntegerParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.MaxPredelay: - sfxEvent.MaxPredelay = PetroglyphXmlUnsignedIntegerParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.MinPostdelay: - sfxEvent.MinPostdelay = PetroglyphXmlUnsignedIntegerParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.MaxPostdelay: - sfxEvent.MaxPostdelay = PetroglyphXmlUnsignedIntegerParser.Instance.Parse(tag); - return true; - - case SfxEventXmlTags.LoopFadeInSeconds: - sfxEvent.LoopFadeInSeconds = PetroglyphXmlFloatParser.Instance.ParseAtLeast(tag, SfxEvent.MinLoopSeconds); - return true; - case SfxEventXmlTags.LoopFadeOutSeconds: - sfxEvent.LoopFadeOutSeconds = PetroglyphXmlFloatParser.Instance.ParseAtLeast(tag, SfxEvent.MinLoopSeconds); - return true; - case SfxEventXmlTags.VolumeSaturationDistance: - // I think it was planned at some time to support -1.0 and >= 0.0, since you don't get a warning when -1.0 is coded - // but the Engine coerces anything < 0.0 to 0.0. - sfxEvent.VolumeSaturationDistance = PetroglyphXmlFloatParser.Instance.ParseAtLeast(tag, SfxEvent.MinVolumeSaturation); - return true; - default: return false; - } - } - - public override SfxEvent Parse(XElement element) => throw new NotSupportedException(); -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/CommandBarComponentFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/CommandBarComponentFileParser.cs deleted file mode 100644 index 763d244..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/CommandBarComponentFileParser.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Xml.Linq; -using AnakinRaW.CommonUtilities.Collections; -using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.CommandBar.Xml; -using PG.StarWarsGame.Engine.Xml.Parsers.Data; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Engine.Xml.Parsers.File; - -internal class CommandBarComponentFileParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) - : PetroglyphXmlFileContainerParser(serviceProvider, errorReporter) -{ - protected override void Parse(XElement element, IFrugalValueListDictionary parsedElements, string fileName) - { - var parser = new CommandBarComponentParser(parsedElements, ServiceProvider, ErrorReporter); - - if (!element.HasElements) - { - OnParseError(XmlParseErrorEventArgs.FromEmptyRoot(element)); - return; - } - - foreach (var xElement in element.Elements()) - { - var commandBarComponent = parser.Parse(xElement, out var nameCrc); - parsedElements.Add(nameCrc, commandBarComponent); - } - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileParser.cs deleted file mode 100644 index ffeafff..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileParser.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Xml.Linq; -using AnakinRaW.CommonUtilities.Collections; -using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.GameObjects; -using PG.StarWarsGame.Engine.Xml.Parsers.Data; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Engine.Xml.Parsers.File; - -internal class GameObjectFileParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) - : PetroglyphXmlFileContainerParser(serviceProvider, errorReporter) -{ - protected override void Parse(XElement element, IFrugalValueListDictionary parsedElements, string fileName) - { - var parser = new GameObjectParser(parsedElements, ServiceProvider, ErrorReporter); - - foreach (var xElement in element.Elements()) - { - var gameObject = parser.Parse(xElement, out var nameCrc); - parsedElements.Add(nameCrc, gameObject); - } - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GuiDialogParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GuiDialogParser.cs deleted file mode 100644 index 851aa9f..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GuiDialogParser.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Xml.Linq; -using AnakinRaW.CommonUtilities.Collections; -using PG.StarWarsGame.Engine.GuiDialog.Xml; -using PG.StarWarsGame.Files.XML; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Engine.Xml.Parsers.File; - -internal class GuiDialogParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : - PetroglyphXmlFileParser(serviceProvider, errorReporter) -{ - protected override GuiDialogsXml Parse(XElement element, string fileName) - { - var textures = ParseTextures(element.Element("Textures"), fileName); - return new GuiDialogsXml(textures, XmlLocationInfo.FromElement(element)); - } - - private GuiDialogsXmlTextureData ParseTextures(XElement? element, string fileName) - { - if (element is null) - { - OnParseError(new XmlParseErrorEventArgs(new XmlLocationInfo(fileName, null), XmlParseErrorKind.MissingNode, - "Expected node is missing.")); - return new GuiDialogsXmlTextureData([], new XmlLocationInfo(fileName, null)); - } - - var textures = new List(); - - GetAttributeValue(element, "File", out var megaTexture); - GetAttributeValue(element, "Compressed_File", out var compressedMegaTexture); - - foreach (var texture in element.Elements()) - textures.Add(ParseTexture(texture)); - - if (textures.Count == 0) - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.MissingNode, - "Textures must contain at least one child node.")); - - return new GuiDialogsXmlTextureData(textures, XmlLocationInfo.FromElement(element)) - { - MegaTexture = megaTexture, - CompressedMegaTexture = compressedMegaTexture - }; - } - - private XmlComponentTextureData ParseTexture(XElement texture) - { - var componentId = GetTagName(texture); - var textures = new FrugalValueListDictionary(); - - foreach (var entry in texture.Elements()) - textures.Add(entry.Name.ToString(), PetroglyphXmlStringParser.Instance.Parse(entry)); - - return new XmlComponentTextureData(componentId, textures, XmlLocationInfo.FromElement(texture)); - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs deleted file mode 100644 index 841d805..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Xml.Linq; -using AnakinRaW.CommonUtilities.Collections; -using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.Audio.Sfx; -using PG.StarWarsGame.Engine.Xml.Parsers.Data; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Engine.Xml.Parsers.File; - -internal class SfxEventFileParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) - : PetroglyphXmlFileContainerParser(serviceProvider, errorReporter) -{ - protected override void Parse(XElement element, IFrugalValueListDictionary parsedElements, string fileName) - { - var parser = new SfxEventParser(parsedElements, ServiceProvider, ErrorReporter); - - if (!element.HasElements) - { - OnParseError(XmlParseErrorEventArgs.FromEmptyRoot(element)); - return; - } - - foreach (var xElement in element.Elements()) - { - var sfxEvent = parser.Parse(xElement, out var nameCrc); - parsedElements.Add(nameCrc, sfxEvent); - } - - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameConstantsParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/FileObjects/GameConstantsParser.cs similarity index 50% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameConstantsParser.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/FileObjects/GameConstantsParser.cs index 64de5ec..4c7e0d9 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameConstantsParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/FileObjects/GameConstantsParser.cs @@ -1,16 +1,17 @@ using System; using System.Xml.Linq; using PG.StarWarsGame.Engine.GameConstants; +using PG.StarWarsGame.Files.XML; using PG.StarWarsGame.Files.XML.ErrorHandling; using PG.StarWarsGame.Files.XML.Parsers; -namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; +namespace PG.StarWarsGame.Engine.Xml.Parsers; internal class GameConstantsParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : - PetroglyphXmlFileParser(serviceProvider, errorReporter) + XmlFileParser(serviceProvider, errorReporter) { - protected override GameConstantsXml Parse(XElement element, string fileName) + protected override GameConstantsXml ParseRoot(XElement element, string fileName) { - return new GameConstantsXml(); + return new GameConstantsXml(new XmlLocationInfo(fileName, null)); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/FileObjects/GuiDialogParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/FileObjects/GuiDialogParser.cs new file mode 100644 index 0000000..152ec83 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/FileObjects/GuiDialogParser.cs @@ -0,0 +1,186 @@ +using AnakinRaW.CommonUtilities.Collections; +using PG.StarWarsGame.Engine.GuiDialog; +using PG.StarWarsGame.Engine.GuiDialog.Xml; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.Parsers; +using System; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace PG.StarWarsGame.Engine.Xml.Parsers; + +internal class GuiDialogParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : + XmlFileParser(serviceProvider, errorReporter) +{ + protected override GuiDialogsXml ParseRoot(XElement element, string fileName) + { + using var elementsEnumerator = element.Elements().GetEnumerator(); + + var texturesExist = elementsEnumerator.MoveNext(); + var textures = ParseTextures(texturesExist + ? elementsEnumerator.Current + : null!, + fileName); + + return new GuiDialogsXml(textures, XmlLocationInfo.FromElement(element)); + } + + private GuiDialogsXmlTextureData ParseTextures(XElement? element, string fileName) + { + if (element is null) + { + ErrorReporter?.Report(new XmlError(this, locationInfo: new XmlLocationInfo(fileName, null)) + { + ErrorKind = XmlParseErrorKind.MissingNode, + Message = "Unable to read textures for GUI." + }); + return new GuiDialogsXmlTextureData([], new XmlLocationInfo(fileName, null)); + } + + if (element.Name != "Textures") + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.UnexceptedElementName, + Message = "Unable to read textures for GUI." + }); + } + + var textures = new List(); + + GetAttributeValue(element, "File", out var megaTexture); + GetAttributeValue(element, "Compressed_File", out var compressedMegaTexture); + + foreach (var texture in element.Elements()) + textures.Add(ParseTexture(texture)); + + if (textures.Count == 0) + { + ErrorReporter?.Report(new XmlError(this, element) + { + Message = "Missing default texture specifications in GUI XML file!", + ErrorKind = XmlParseErrorKind.MissingNode + }); + } + + return new GuiDialogsXmlTextureData(textures, XmlLocationInfo.FromElement(element)) + { + MegaTexture = megaTexture, + CompressedMegaTexture = compressedMegaTexture + }; + } + + private XmlComponentTextureData ParseTexture(XElement texture) + { + var componentId = GetTagName(texture); + var textures = new FrugalValueListDictionary(); + + foreach (var entry in texture.Elements()) + textures.Add(entry.Name.ToString(), PetroglyphXmlStringParser.Instance.Parse(entry)); + + return new XmlComponentTextureData(componentId, textures, XmlLocationInfo.FromElement(texture)); + } + + + internal static readonly EnumConversionDictionary ComponentTypeDictionary = new([ + new("Button_Left", GuiComponentType.ButtonLeft), + new("Button_Middle", GuiComponentType.ButtonMiddle), + new("Button_Right", GuiComponentType.ButtonRight), + new("Button_Left_Mouse_Over", GuiComponentType.ButtonLeftMouseOver), + new("Button_Middle_Mouse_Over", GuiComponentType.ButtonMiddleMouseOver), + new("Button_Right_Mouse_Over", GuiComponentType.ButtonRightMouseOver), + new("Button_Left_Pressed", GuiComponentType.ButtonLeftPressed), + new("Button_Middle_Pressed", GuiComponentType.ButtonMiddlePressed), + new("Button_Right_Pressed", GuiComponentType.ButtonRightPressed), + new("Button_Left_Disabled", GuiComponentType.ButtonLeftDisabled), + new("Button_Middle_Disabled", GuiComponentType.ButtonMiddleDisabled), + new("Button_Right_Disabled", GuiComponentType.ButtonRightDisabled), + + new("Check_Off", GuiComponentType.CheckOff), + new("Check_On", GuiComponentType.CheckOn), + + new("Dial_Left", GuiComponentType.DialLeft), + new("Dial_Middle", GuiComponentType.DialMiddle), + new("Dial_Right", GuiComponentType.DialRight), + new("Dial_Plus", GuiComponentType.DialPlus), + new("Dial_Plus_Mouse_Over", GuiComponentType.DialPlusMouseOver), + new("Dial_Plus_Pressed", GuiComponentType.DialPlusPressed), + new("Dial_Minus", GuiComponentType.DialMinus), + new("Dial_Minus_Mouse_Over", GuiComponentType.DialMinusMouseOver), + new("Dial_Minus_Pressed", GuiComponentType.DialMinusPressed), + new("Dial_Tab", GuiComponentType.DialTab), + + new("Frame_Bottom", GuiComponentType.FrameBottom), + new("Frame_Bottom_Left", GuiComponentType.FrameBottomLeft), + new("Frame_Bottom_Right", GuiComponentType.FrameBottomRight), + new("Frame_Background", GuiComponentType.FrameBackground), + new("Frame_Left", GuiComponentType.FrameLeft), + new("Frame_Right", GuiComponentType.FrameRight), + new("Frame_Top", GuiComponentType.FrameTop), + new("Frame_Top_Left", GuiComponentType.FrameTopLeft), + new("Frame_Top_Right", GuiComponentType.FrameTopRight), + new("Frame_Top_Transition_Left", GuiComponentType.FrameTopTransitionLeft), + new("Frame_Top_Transition_Right", GuiComponentType.FrameTopTransitionRight), + new("Frame_Bottom_Transition_Left", GuiComponentType.FrameBottomTransitionLeft), + new("Frame_Bottom_Transition_Right", GuiComponentType.FrameBottomTransitionRight), + new("Frame_Left_Transition_Top", GuiComponentType.FrameLeftTransitionTop), + new("Frame_Left_Transition_Bottom", GuiComponentType.FrameLeftTransitionBottom), + new("Frame_Right_Transition_Top", GuiComponentType.FrameRightTransitionTop), + new("Frame_Right_Transition_Bottom", GuiComponentType.FrameRightTransitionBottom), + + new("Radio_Off", GuiComponentType.RadioOff), + new("Radio_On", GuiComponentType.RadioOn), + new("Radio_Disabled", GuiComponentType.RadioDisabled), + new("Radio_Mouse_Over", GuiComponentType.RadioMouseOver), + + new("Scroll_Down_Button", GuiComponentType.ScrollDownButton), + new("Scroll_Down_Button_Pressed", GuiComponentType.ScrollDownButtonPressed), + new("Scroll_Down_Button_Mouse_Over", GuiComponentType.ScrollDownButtonMouseOver), + new("Scroll_Down_Button_Disabled", GuiComponentType.ScrollDownButtonDisabled), + new("Scroll_Middle", GuiComponentType.ScrollMiddle), + new("Scroll_Middle_Disabled", GuiComponentType.ScrollMiddleDisabled), + new("Scroll_Tab", GuiComponentType.ScrollTab), + new("Scroll_Tab_Disabled", GuiComponentType.ScrollTabDisabled), + new("Scroll_Up_Button", GuiComponentType.ScrollUpButton), + new("Scroll_Up_Button_Pressed", GuiComponentType.ScrollUpButtonPressed), + new("Scroll_Up_Button_Mouse_Over", GuiComponentType.ScrollUpButtonMouseOver), + new("Scroll_Up_Button_Disabled", GuiComponentType.ScrollUpButtonDisabled), + + new("Trackbar_Scroll_Down_Button", GuiComponentType.TrackbarScrollDownButton), + new("Trackbar_Scroll_Down_Button_Pressed", GuiComponentType.TrackbarScrollDownButtonPressed), + new("Trackbar_Scroll_Down_Button_Mouse_Over", GuiComponentType.TrackbarScrollDownButtonMouseOver), + new("Trackbar_Scroll_Down_Button_Disabled", GuiComponentType.TrackbarScrollDownButtonDisabled), + new("Trackbar_Scroll_Middle", GuiComponentType.TrackbarScrollMiddle), + new("Trackbar_Scroll_Middle_Disabled", GuiComponentType.TrackbarScrollMiddleDisabled), + new("Trackbar_Scroll_Tab", GuiComponentType.TrackbarScrollTab), + new("Trackbar_Scroll_Tab_Disabled", GuiComponentType.TrackbarScrollTabDisabled), + new("Trackbar_Scroll_Up_Button", GuiComponentType.TrackbarScrollUpButton), + new("Trackbar_Scroll_Up_Button_Pressed", GuiComponentType.TrackbarScrollUpButtonPressed), + new("Trackbar_Scroll_Up_Button_Mouse_Over", GuiComponentType.TrackbarScrollUpButtonMouseOver), + new("Trackbar_Scroll_Up_Button_Disabled", GuiComponentType.TrackbarScrollUpButtonDisabled), + + new("Small_Frame_Bottom", GuiComponentType.SmallFrameBottom), + new("Small_Frame_Bottom_Left", GuiComponentType.SmallFrameBottomLeft), + new("Small_Frame_Bottom_Right", GuiComponentType.SmallFrameBottomRight), + new("Small_Frame_Left", GuiComponentType.SmallFrameMiddleLeft), + new("Small_Frame_Right", GuiComponentType.SmallFrameMiddleRight), + new("Small_Frame_Top", GuiComponentType.SmallFrameTop), + new("Small_Frame_Top_Left", GuiComponentType.SmallFrameTopLeft), + new("Small_Frame_Top_Right", GuiComponentType.SmallFrameTopRight), + new("Small_Frame_Background", GuiComponentType.SmallFrameBackground), + + new("Combo_Box_Popdown_Button", GuiComponentType.ComboboxPopdown), + new("Combo_Box_Popdown_Button_Pressed", GuiComponentType.ComboboxPopdownPressed), + new("Combo_Box_Popdown_Button_Mouse_Over", GuiComponentType.ComboboxPopdownMouseOver), + new("Combo_Box_Text_Box", GuiComponentType.ComboboxTextBox), + new("Combo_Box_Left_Cap", GuiComponentType.ComboboxLeftCap), + + new("Progress_Bar_Left", GuiComponentType.ProgressLeft), + new("Progress_Bar_Middle_Off", GuiComponentType.ProgressMiddleOff), + new("Progress_Bar_Middle_On", GuiComponentType.ProgressMiddleOn), + new("Progress_Bar_Right", GuiComponentType.ProgressRight), + + new("Scanlines", GuiComponentType.Scanlines), + ]); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/GameObjectFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/GameObjectFileParser.cs new file mode 100644 index 0000000..66efb71 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/GameObjectFileParser.cs @@ -0,0 +1,46 @@ +using AnakinRaW.CommonUtilities.Collections; +using PG.Commons.Hashing; +using PG.StarWarsGame.Engine.ErrorReporting; +using PG.StarWarsGame.Engine.GameObjects; +using PG.StarWarsGame.Files.XML.Parsers; +using System; +using System.IO; + +namespace PG.StarWarsGame.Engine.Xml.Parsers; + +internal class GameObjectFileParser( + GameEngineType engine, + IServiceProvider serviceProvider, + IGameEngineErrorReporter? errorReporter) + : PetroglyphXmlFileParserBase(serviceProvider, errorReporter), IXmlContainerFileParser +{ + public event EventHandler? GameObjectParsed; + + private readonly GameObjectParser _gameObjectParser = new(engine, serviceProvider, errorReporter); + + public INamedXmlObjectParser ElementParser => _gameObjectParser; + + public bool OverlayLoad + { + get; + set + { + field = value; + _gameObjectParser.OverlayLoad = value; + } + } + + public void ParseFile(Stream xmlStream, IFrugalValueListDictionary parsedEntries) + { + var root = GetRootElement(xmlStream, out _); + foreach (var xElement in root.Elements()) + { + var parsedElement = _gameObjectParser.Parse(xElement, parsedEntries, out var entryCrc); + if (!OverlayLoad) + { + parsedEntries.Add(entryCrc, parsedElement); + GameObjectParsed?.Invoke(this, new GameObjectParsedEventArgs(parsedElement, true)); + } + } + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/GameObjectParsedEventArgs.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/GameObjectParsedEventArgs.cs new file mode 100644 index 0000000..692c423 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/GameObjectParsedEventArgs.cs @@ -0,0 +1,17 @@ +using System; +using PG.StarWarsGame.Engine.GameObjects; + +namespace PG.StarWarsGame.Engine.Xml.Parsers; + +internal sealed class GameObjectParsedEventArgs : EventArgs +{ + public bool Unique { get; } + + public GameObject GameObject { get; } + + internal GameObjectParsedEventArgs(GameObject gameObject, bool unique) + { + Unique = unique; + GameObject = gameObject ?? throw new ArgumentNullException(nameof(gameObject)); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/NamedObjects/CommandBarComponentParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/NamedObjects/CommandBarComponentParser.cs new file mode 100644 index 0000000..ced9c3c --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/NamedObjects/CommandBarComponentParser.cs @@ -0,0 +1,584 @@ +using AnakinRaW.CommonUtilities.Collections; +using PG.StarWarsGame.Engine.CommandBar.Xml; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.Parsers; +using System; +using System.Xml.Linq; +using Crc32 = PG.Commons.Hashing.Crc32; + +namespace PG.StarWarsGame.Engine.Xml.Parsers; + +internal class CommandBarComponentParser( + GameEngineType engine, + IServiceProvider serviceProvider, + IXmlParserErrorReporter? errorReporter = null) + : NamedXmlObjectParser(engine, new CommandBarComponentDataXmlTagMapper(serviceProvider), errorReporter, serviceProvider) +{ + protected override bool UpperCaseNameForCrc => true; + protected override bool UpperCaseNameForObject => false; + + protected override CommandBarComponentData CreateXmlObject( + string name, + Crc32 nameCrc, + XElement element, + IReadOnlyFrugalValueListDictionary parsedEntries, + XmlLocationInfo location) + { + return new CommandBarComponentData(name, nameCrc, location); + } + + + protected override void ValidateAndFixupValues(CommandBarComponentData xmlData, XElement element, in IReadOnlyFrugalValueListDictionary parsedEntries) + { + if (xmlData.Name.Length > PGConstants.MaxCommandBarComponentName) + { + ErrorReporter?.Report(new XmlError(this, element) + { + Message = $"CommandbarComponent name '{xmlData.Name}' is too long.", + ErrorKind = XmlParseErrorKind.TooLongData + }); + } + + xmlData.FixupValues(); + } + + private sealed class CommandBarComponentDataXmlTagMapper(IServiceProvider serviceProvider) + : XmlTagMapper(serviceProvider) + { + protected override void BuildMappings() + { + AddMapping( + CommandBarComponentTags.SelectedTextureName, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.SelectedTextureNamesInternal, val, replace)); + AddMapping( + CommandBarComponentTags.BlankTextureName, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.BlankTextureNamesInternal, val, replace)); + AddMapping( + CommandBarComponentTags.IconTextureName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.IconTextureName = val); + AddMapping( + CommandBarComponentTags.IconAlternateTextureName, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.IconAlternateTextureNamesInternal, val, replace)); + AddMapping( + CommandBarComponentTags.MouseOverTextureName, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.MouseOverTextureNamesInternal, val, replace)); + AddMapping( + CommandBarComponentTags.DisabledTextureName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.DisabledTextureName = val); + AddMapping( + CommandBarComponentTags.FlashTextureName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.FlashTextureName = val); + AddMapping( + CommandBarComponentTags.BarTextureName, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.BarTextureNamesInternal, val, replace)); + AddMapping( + CommandBarComponentTags.BarOverlayName, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.BarOverlayNamesInternal, val, replace)); + AddMapping( + CommandBarComponentTags.BuildTextureName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.BuildTextureName = val); + AddMapping( + CommandBarComponentTags.ModelName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.ModelName = val); + AddMapping( + CommandBarComponentTags.BoneName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.BoneName = val); + AddMapping( + CommandBarComponentTags.CursorTextureName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.CursorTextureName = val); + AddMapping( + CommandBarComponentTags.FontName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.FontName = val); + AddMapping( + CommandBarComponentTags.AlternateFontName, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.AlternateFontNamesInternal, val, replace)); + AddMapping( + CommandBarComponentTags.TooltipText, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.TooltipTextsInternal, val, replace)); + AddMapping( + CommandBarComponentTags.ClickSfx, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.ClickSfx = val); + AddMapping( + CommandBarComponentTags.MouseOverSfx, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.MouseOverSfx = val); + AddMapping( + CommandBarComponentTags.LowerEffectTextureName, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.LowerEffectTextureNamesInternal, val, replace)); + AddMapping( + CommandBarComponentTags.UpperEffectTextureName, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.UpperEffectTextureNamesInternal, val, replace)); + AddMapping( + CommandBarComponentTags.OverlayTextureName, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.OverlayTextureNamesInternal, val, replace)); + AddMapping( + CommandBarComponentTags.Overlay2TextureName, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.Overlay2TextureNamesInternal, val, replace)); + AddMapping( + CommandBarComponentTags.RightClickSfx, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.RightClickSfx = val); + AddMapping( + CommandBarComponentTags.Type, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.Type = val); + AddMapping( + CommandBarComponentTags.Group, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.Group = val); + AddMapping( + CommandBarComponentTags.DragAndDrop, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.DragAndDrop = val); + AddMapping( + CommandBarComponentTags.DragSelect, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.DragSelect = val); + AddMapping( + CommandBarComponentTags.Receptor, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.Receptor = val); + AddMapping( + CommandBarComponentTags.Toggle, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.Toggle = val); + AddMapping( + CommandBarComponentTags.Tab, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.Tab = val); + AddMapping( + CommandBarComponentTags.AssociatedText, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.AssociatedText = val); + AddMapping( + CommandBarComponentTags.Hidden, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.Hidden = val); + AddMapping( + CommandBarComponentTags.Scale, + PetroglyphXmlFloatParser.Instance.Parse, + (obj, val) => obj.Scale = val); + AddMapping( + CommandBarComponentTags.Color, + PetroglyphXmlRgbaColorParser.Instance.Parse, + (obj, val) => obj.Color = val); + AddMapping( + CommandBarComponentTags.TextColor, + PetroglyphXmlRgbaColorParser.Instance.Parse, + (obj, val) => obj.TextColor = val); + AddMapping( + CommandBarComponentTags.TextColor2, + PetroglyphXmlRgbaColorParser.Instance.Parse, + (obj, val) => obj.TextColor2 = val); + AddMapping( + CommandBarComponentTags.Size, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.Size = val); + AddMapping( + CommandBarComponentTags.ClearColor, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.ClearColor = val); + AddMapping( + CommandBarComponentTags.Disabled, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.Disabled = val); + AddMapping( + CommandBarComponentTags.SwapTexture, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.SwapTexture = val); + AddMapping( + CommandBarComponentTags.BaseLayer, + x => (int)PetroglyphXmlUnsignedIntegerParser.Instance.Parse(x), + (obj, val) => obj.BaseLayer = val); + AddMapping( + CommandBarComponentTags.DrawAdditive, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.DrawAdditive = val); + AddMapping( + CommandBarComponentTags.TextOffset, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.TextOffset = val); + AddMapping( + CommandBarComponentTags.TextOffset2, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.TextOffset2 = val); + AddMapping( + CommandBarComponentTags.Offset, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.Offset = val); + AddMapping( + CommandBarComponentTags.DefaultOffset, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.DefaultOffset = val); + AddMapping( + CommandBarComponentTags.DefaultOffsetWidescreen, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.DefaultOffsetWidescreen = val); + AddMapping( + CommandBarComponentTags.IconOffset, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.IconOffset = val); + AddMapping( + CommandBarComponentTags.MouseOverOffset, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.MouseOverOffset = val); + AddMapping( + CommandBarComponentTags.DisabledOffset, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.DisabledOffset = val); + AddMapping( + CommandBarComponentTags.BuildDialOffset, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.BuildDialOffset = val); + AddMapping( + CommandBarComponentTags.BuildDial2Offset, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.BuildDial2Offset = val); + AddMapping( + CommandBarComponentTags.LowerEffectOffset, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.LowerEffectOffset = val); + AddMapping( + CommandBarComponentTags.UpperEffectOffset, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.UpperEffectOffset = val); + AddMapping( + CommandBarComponentTags.OverlayOffset, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.OverlayOffset = val); + AddMapping( + CommandBarComponentTags.Overlay2Offset, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.Overlay2Offset = val); + AddMapping( + CommandBarComponentTags.Editable, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.Editable = val); + AddMapping( + CommandBarComponentTags.MaxTextLength, + PetroglyphXmlUnsignedIntegerParser.Instance.Parse, + (obj, val) => obj.MaxTextLength = val); + AddMapping( + CommandBarComponentTags.BlinkRate, + PetroglyphXmlFloatParser.Instance.Parse, + (obj, val) => obj.BlinkRate = val); + AddMapping( + CommandBarComponentTags.FontPointSize, + x => (int)PetroglyphXmlUnsignedIntegerParser.Instance.Parse(x), + (obj, val) => obj.FontPointSize = val); + AddMapping( + CommandBarComponentTags.TextOutline, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.TextOutline = val); + AddMapping( + CommandBarComponentTags.MaxTextWidth, + PetroglyphXmlFloatParser.Instance.Parse, + (obj, val) => obj.MaxTextWidth = val); + AddMapping( + CommandBarComponentTags.Stackable, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.Stackable = val); + AddMapping( + CommandBarComponentTags.ModelOffsetX, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.ModelOffsetX = val); + AddMapping( + CommandBarComponentTags.ModelOffsetY, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.ModelOffsetY = val); + AddMapping( + CommandBarComponentTags.ScaleModelX, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.ScaleModelX = val); + AddMapping( + CommandBarComponentTags.ScaleModelY, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.ScaleModelY = val); + AddMapping( + CommandBarComponentTags.Collideable, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.Collideable = val); + AddMapping( + CommandBarComponentTags.TextEmboss, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.TextEmboss = val); + AddMapping( + CommandBarComponentTags.ShouldGhost, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.ShouldGhost = val); + AddMapping( + CommandBarComponentTags.GhostBaseOnly, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.GhostBaseOnly = val); + AddMapping( + CommandBarComponentTags.MaxBarLevel, + x => PetroglyphXmlIntegerParser.Instance.Parse(x), + (obj, val) => obj.MaxBarLevel = val); + AddMapping( + CommandBarComponentTags.MaxBarColor, + PetroglyphXmlRgbaColorParser.Instance.Parse, + (obj, val) => obj.MaxBarColor = val); + AddMapping( + CommandBarComponentTags.CrossFade, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.CrossFade = val); + AddMapping( + CommandBarComponentTags.LeftJustified, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.LeftJustified = val); + AddMapping( + CommandBarComponentTags.RightJustified, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.RightJustified = val); + AddMapping( + CommandBarComponentTags.NoShell, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.NoShell = val); + AddMapping( + CommandBarComponentTags.SnapDrag, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.SnapDrag = val); + AddMapping( + CommandBarComponentTags.SnapLocation, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.SnapLocation = val); + AddMapping( + CommandBarComponentTags.BlinkDuration, + PetroglyphXmlFloatParser.Instance.Parse, + (obj, val) => obj.BlinkDuration = val); + AddMapping( + CommandBarComponentTags.ScaleDuration, + PetroglyphXmlFloatParser.Instance.Parse, + (obj, val) => obj.ScaleDuration = val); + AddMapping( + CommandBarComponentTags.OffsetRender, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.OffsetRender = val); + AddMapping( + CommandBarComponentTags.BlinkFade, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.BlinkFade = val); + AddMapping( + CommandBarComponentTags.NoHiddenCollision, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.NoHiddenCollision = val); + AddMapping( + CommandBarComponentTags.ManualOffset, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.ManualOffset = val); + AddMapping( + CommandBarComponentTags.SelectedAlpha, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.SelectedAlpha = val); + AddMapping( + CommandBarComponentTags.PixelAlign, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.PixelAlign = val); + AddMapping( + CommandBarComponentTags.CanDragStack, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.CanDragStack = val); + AddMapping( + CommandBarComponentTags.CanAnimate, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.CanAnimate = val); + AddMapping( + CommandBarComponentTags.AnimFps, + PetroglyphXmlFloatParser.Instance.Parse, + (obj, val) => obj.AnimFps = val); + AddMapping( + CommandBarComponentTags.LoopAnim, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.LoopAnim = val); + AddMapping( + CommandBarComponentTags.SmoothBar, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.SmoothBar = val); + AddMapping( + CommandBarComponentTags.OutlinedBar, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.OutlinedBar = val); + AddMapping( + CommandBarComponentTags.DragBack, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.DragBack = val); + AddMapping( + CommandBarComponentTags.LowerEffectAdditive, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.LowerEffectAdditive = val); + AddMapping( + CommandBarComponentTags.UpperEffectAdditive, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.UpperEffectAdditive = val); + AddMapping( + CommandBarComponentTags.ClickShift, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.ClickShift = val); + AddMapping( + CommandBarComponentTags.TutorialScene, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.TutorialScene = val); + AddMapping( + CommandBarComponentTags.DialogScene, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.DialogScene = val); + AddMapping( + CommandBarComponentTags.ShouldRenderAtDragPos, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.ShouldRenderAtDragPos = val); + AddMapping( + CommandBarComponentTags.DisableDarken, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.DisableDarken = val); + AddMapping( + CommandBarComponentTags.AnimateBack, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.AnimateBack = val); + AddMapping( + CommandBarComponentTags.AnimateUpperEffect, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.AnimateUpperEffect = val); + } + } + + internal static class CommandBarComponentTags + { + public const string SelectedTextureName = "Selected_Texture_Name"; + public const string BlankTextureName = "Blank_Texture_Name"; + public const string IconTextureName = "Icon_Texture_Name"; + public const string IconAlternateTextureName = "Icon_Alternate_Texture_Name"; + public const string MouseOverTextureName = "Mouse_Over_Texture_Name"; + public const string DisabledTextureName = "Disabled_Texture_Name"; + public const string FlashTextureName = "Flash_Texture_Name"; + public const string BarTextureName = "Bar_Texture_Name"; + public const string BarOverlayName = "Bar_Overlay_Name"; + public const string BuildTextureName = "Build_Texture_Name"; + public const string ModelName = "Model_Name"; + public const string BoneName = "Bone_Name"; + public const string CursorTextureName = "Cursor_Texture_Name"; + public const string FontName = "Font_Name"; + public const string AlternateFontName = "Alternate_Font_Name"; + public const string TooltipText = "Tooltip_Text"; + public const string ClickSfx = "Click_SFX"; + public const string MouseOverSfx = "Mouse_Over_SFX"; + public const string LowerEffectTextureName = "Lower_Effect_Texture_Name"; + public const string UpperEffectTextureName = "Upper_Effect_Texture_Name"; + public const string OverlayTextureName = "Overlay_Texture_Name"; + public const string Overlay2TextureName = "Overlay2_Texture_Name"; + public const string RightClickSfx = "Right_Click_SFX"; + public const string Type = "Type"; + public const string Group = "Group"; + public const string DragAndDrop = "Drag_And_Drop"; + public const string DragSelect = "Drag_Select"; + public const string Receptor = "Receptor"; + public const string Toggle = "Toggle"; + public const string Tab = "Tab"; + public const string AssociatedText = "Associated_Text"; + public const string Hidden = "Hidden"; + public const string Scale = "Scale"; + public const string Color = "Color"; + public const string TextColor = "Text_Color"; + public const string TextColor2 = "Text_Color2"; + public const string Size = "Size"; + public const string ClearColor = "Clear_Color"; + public const string Disabled = "Disabled"; + public const string SwapTexture = "Swap_Texture"; + public const string BaseLayer = "Base_Layer"; + public const string DrawAdditive = "Draw_Additive"; + public const string TextOffset = "Text_Offset"; + public const string TextOffset2 = "Text_Offset2"; + public const string Offset = "Offset"; + public const string DefaultOffset = "Default_Offset"; + public const string DefaultOffsetWidescreen = "Default_Offset_Widescreen"; + public const string IconOffset = "Icon_Offset"; + public const string MouseOverOffset = "Mouse_Over_Offset"; + public const string DisabledOffset = "Disabled_Offset"; + public const string BuildDialOffset = "Build_Dial_Offset"; + public const string BuildDial2Offset = "Build_Dial2_Offset"; + public const string LowerEffectOffset = "Lower_Effect_Offset"; + public const string UpperEffectOffset = "Upper_Effect_Offset"; + public const string OverlayOffset = "Overlay_Offset"; + public const string Overlay2Offset = "Overlay2_Offset"; + public const string Editable = "Editable"; + public const string MaxTextLength = "Max_Text_Length"; + public const string BlinkRate = "Blink_Rate"; + public const string FontPointSize = "Font_Point_Size"; + public const string TextOutline = "Text_Outline"; + public const string MaxTextWidth = "Max_Text_Width"; + public const string Stackable = "Stackable"; + public const string ModelOffsetX = "Model_Offset_X"; + public const string ModelOffsetY = "Model_Offset_Y"; + public const string ScaleModelX = "Scale_Model_X"; + public const string ScaleModelY = "Scale_Model_Y"; + public const string Collideable = "Collideable"; + public const string TextEmboss = "Text_Emboss"; + public const string ShouldGhost = "Should_Ghost"; + public const string GhostBaseOnly = "Ghost_Base_Only"; + public const string MaxBarLevel = "Max_Bar_Level"; + public const string MaxBarColor = "Max_Bar_Color"; + public const string CrossFade = "Cross_Fade"; + public const string LeftJustified = "Left_Justified"; + public const string RightJustified = "Right_Justified"; + public const string NoShell = "No_Shell"; + public const string SnapDrag = "Snap_Drag"; + public const string SnapLocation = "Snap_Location"; + public const string BlinkDuration = "Blink_Duration"; + public const string ScaleDuration = "Scale_Duration"; + public const string OffsetRender = "Offset_Render"; + public const string BlinkFade = "Blink_Fade"; + public const string NoHiddenCollision = "No_Hidden_Collision"; + public const string ManualOffset = "Manual_Offset"; + public const string SelectedAlpha = "Selected_Alpha"; + public const string PixelAlign = "Pixel_Align"; + public const string CanDragStack = "Can_Drag_Stack"; + public const string CanAnimate = "Can_Animate"; + public const string AnimFps = "Anim_FPS"; + public const string LoopAnim = "Loop_Anim"; + public const string SmoothBar = "Smooth_Bar"; + public const string OutlinedBar = "Outlined_Bar"; + public const string DragBack = "Drag_Back"; + public const string LowerEffectAdditive = "Lower_Effect_Additive"; + public const string UpperEffectAdditive = "Upper_Effect_Additive"; + public const string ClickShift = "Click_Shift"; + public const string TutorialScene = "Tutorial_Scene"; + public const string DialogScene = "Dialog_Scene"; + public const string ShouldRenderAtDragPos = "Should_Render_At_Drag_Pos"; + public const string DisableDarken = "Disable_Darken"; + public const string AnimateBack = "Animate_Back"; + public const string AnimateUpperEffect = "Animate_Upper_Effect"; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/NamedObjects/GameObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/NamedObjects/GameObjectParser.cs new file mode 100644 index 0000000..f68f0f2 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/NamedObjects/GameObjectParser.cs @@ -0,0 +1,233 @@ +using AnakinRaW.CommonUtilities.Collections; +using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine.GameObjects; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.Parsers; +using System; +using System.Diagnostics; +using System.Xml.Linq; +using Crc32 = PG.Commons.Hashing.Crc32; + +namespace PG.StarWarsGame.Engine.Xml.Parsers; + +internal partial class GameObjectParser( + GameEngineType engine, + IServiceProvider serviceProvider, + IXmlParserErrorReporter? errorReporter = null) + : NamedXmlObjectParser(engine, new GameObjectXmlTagMapper(serviceProvider), errorReporter, serviceProvider) +{ + internal bool OverlayLoad { get; set; } + + protected override bool UpperCaseNameForCrc => true; + + protected override bool UpperCaseNameForObject => true; + + protected override GameObject CreateXmlObject( + string name, + Crc32 nameCrc, + XElement element, + IReadOnlyFrugalValueListDictionary parsedEntries, + XmlLocationInfo location) + { + if (OverlayLoad) + { + parsedEntries.TryGetFirstValue(nameCrc, out var type); + Debug.Assert(type is not null); + OverlayType(type, element, parsedEntries); + return type; + } + + // The engine actually manages a CRC table of the classification names, + // but since we uppercase the name and this feature is nowhere used, + // except for "MultiplayerStructureMarker", + // we can just use the name as the classification. + var classificationName = GetTagName(element).ToUpperInvariant(); + var gameObjectType = new GameObject(name, classificationName, nameCrc, parsedEntries.ValueCount, location); + if (Logger != null) + LogCreatingNewGameObjectType(Logger, gameObjectType.Name); + return gameObjectType; + } + + protected override void ParseObject( + GameObject xmlObject, + XElement element, + bool replace, + in IReadOnlyFrugalValueListDictionary parsedEntries) + { + if (element.HasElements && element.Attribute("SubObjectList")?.Value == "Yes") + { + // TODO + return; + } + + if (OverlayLoad) + { + OverlayType(xmlObject, element, parsedEntries); + } + else + { + base.ParseObject(xmlObject, element, replace, in parsedEntries); + } + } + + protected override void ValidateAndFixupValues(GameObject gameObject, XElement element, in IReadOnlyFrugalValueListDictionary parsedEntries) + { + if (!OverlayLoad) + { + // TODO + //BehaviorClass.AddImpliedBehaviors(this, BehaviorNames); + //InitBehaviorMap(); + + PostLoadFixup(gameObject); + if (string.IsNullOrEmpty(gameObject.VariantOfExistingTypeName)) + gameObject.IsLoadingComplete = true; + else + OverlayType(gameObject, element, parsedEntries); + } + } + + private void OverlayType(GameObject gameObject, XElement element, IReadOnlyFrugalValueListDictionary parsedEntries) + { + var baseType = gameObject.VariantOfExistingType; + if (baseType is null) + { + var baseTypeName = gameObject.VariantOfExistingTypeName; + if (string.IsNullOrEmpty(baseTypeName)) + return; + + var nameCrc = CreateNameCrc(baseTypeName); + + parsedEntries.TryGetFirstValue(nameCrc, out baseType); + if (baseType is null) + return; + } + OverlayType(baseType, gameObject, element); + } + + private void OverlayType(GameObject baseType, GameObject derivedType, XElement element) + { + if (!baseType.IsLoadingComplete) + return; + + derivedType.ApplyBaseType(baseType); + + ParseTags(derivedType, element, true, ReadOnlyFrugalValueListDictionary.Empty); + + PostLoadFixup(derivedType); + derivedType.IsLoadingComplete = true; + } + + protected override bool ParseTag( + XElement tag, + GameObject xmlObject, + bool replace, + in IReadOnlyFrugalValueListDictionary parseState) + { + // The engine ignores the return value, but we do not, so we can report unknown tags. + base.ParseTag(tag, xmlObject, replace, in parseState); + + // TODO: Once parsing is complete, return original parse result. + return true; + } + + private void PostLoadFixup(GameObject gameObject) + { + // TODO: + // MaxSpeed *= 1.0; + // MaxThrust *= 1.0; + // Asserts and some coercions + + // The engine loads references for scripts, images, hardpoints, etc., + // but we don't do that here. + } + + private sealed class GameObjectXmlTagMapper(IServiceProvider serviceProvider) : XmlTagMapper(serviceProvider) + { + protected override void BuildMappings() + { + AddMapping( + GameObjectXmlTags.GalacticModelName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.GalacticModel = val); + AddMapping( + GameObjectXmlTags.GalacticFleetOverrideModelName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.GalacticFleetOverrideModel = val); + AddMapping( + GameObjectXmlTags.DestroyedGalacticModelName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.DestroyedGalacticModel = val); + AddMapping( + GameObjectXmlTags.LandModelName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.LandModel = val); + AddMapping( + GameObjectXmlTags.LandTerrainModelMapping, + CommaSeparatedStringKeyValueListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.InternalLandTerrainModelMapping, val, replace)); + AddMapping( + GameObjectXmlTags.SpaceModelName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.SpaceModel = val); + AddMapping( + GameObjectXmlTags.LandModelAnimOverrideName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.LandAnimOverrideModel = val); + AddMapping( + GameObjectXmlTags.SpaceModelAnimOverrideName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.SpaceAnimOverrideModel = val); + + AddMapping( + GameObjectXmlTags.CompanyUnits, + PetroglyphXmlLooseStringListParser.Instance.Parse, + // The MULTI_OBJECT_REFERENCE parser never replaces (this is done by the MultiReferenceList itself) + (obj, val) => obj.GroundCompanyUnits.AddRange(val)); + + AddMapping( + GameObjectXmlTags.DamagedSmokeAssetName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.DamagedSmokeAssetModel = val); + + + AddMapping( + GameObjectXmlTags.GuiModelName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.GuiModel = val); + + AddMapping( + GameObjectXmlTags.IconName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.IconName = val); + + AddMapping( + GameObjectXmlTags.VariantOfExistingType, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.VariantOfExistingTypeName = val); + } + } + + [LoggerMessage(LogLevel.Debug, "--- Creating new GameObjectTypeClass for key '{objectName}'")] + static partial void LogCreatingNewGameObjectType(ILogger logger, string objectName); + + internal static class GameObjectXmlTags + { + public const string LandTerrainModelMapping = "Land_Terrain_Model_Mapping"; + public const string GalacticModelName = "Galactic_Model_Name"; + public const string DestroyedGalacticModelName = "Destroyed_Galactic_Model_Name"; + public const string LandModelName = "Land_Model_Name"; + public const string SpaceModelName = "Space_Model_Name"; + public const string GalacticFleetOverrideModelName = "Galactic_Fleet_Override_Model_Name"; + public const string GuiModelName = "GUI_Model_Name"; + public const string LandModelAnimOverrideName = "Land_Model_Anim_Override_Name"; + public const string SpaceModelAnimOverrideName = "Space_Model_Anim_Override_Name"; + public const string DamagedSmokeAssetName = "Damaged_Smoke_Asset_Name"; + + public const string VariantOfExistingType = "Variant_Of_Existing_Type"; + + public const string IconName = "Icon_Name"; + public const string CompanyUnits = "Company_Units"; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/NamedObjects/SfxEventParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/NamedObjects/SfxEventParser.cs new file mode 100644 index 0000000..b20f217 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/NamedObjects/SfxEventParser.cs @@ -0,0 +1,321 @@ +using System; +using System.Diagnostics; +using System.Xml.Linq; +using AnakinRaW.CommonUtilities.Collections; +using PG.Commons.Hashing; +using PG.StarWarsGame.Engine.Audio.Sfx; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.Parsers; + +namespace PG.StarWarsGame.Engine.Xml.Parsers; + +internal class SfxEventParser(GameEngineType engine, IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) + : NamedXmlObjectParser(engine, new SfxEventXmlTagMapper(serviceProvider), errorReporter, serviceProvider) +{ + // This is a slight derivation from the engine: + // The engine does not upper case the name neither for the name itself and the CRC. + // However, the SFXEventManager maps all SFXEvent by their upper-cased name CRC values. + // We create and store upper-cased name CRC to the SFXEvent too. + protected override bool UpperCaseNameForCrc => true; + protected override bool UpperCaseNameForObject => false; + + protected override SfxEvent CreateXmlObject( + string name, + Crc32 nameCrc, + XElement element, + IReadOnlyFrugalValueListDictionary parsedEntries, + XmlLocationInfo location) + { + return new SfxEvent(name, nameCrc, location); + } + + protected override void ValidateAndFixupValues(SfxEvent sfxEvent, XElement element, in IReadOnlyFrugalValueListDictionary parsedEntries) + { + if (sfxEvent.Name.Length > PGConstants.MaxSFXEventName) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.TooLongData, + Message = $"SFXEvent name '{sfxEvent.Name}' is too long." + }); + } + + if (sfxEvent.Is2D == sfxEvent.Is3D) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"SFXEvent '{sfxEvent.Name}' has the same value for is2D and is3D." + }); + } + + if (sfxEvent.MinVolume > sfxEvent.MaxVolume) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"{SfxEventXmlTags.MinVolume} should not be higher than {SfxEventXmlTags.MaxVolume} for SFXEvent '{sfxEvent.Name}'" + }); + } + + if (sfxEvent.MinPitch > sfxEvent.MaxPitch) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"{SfxEventXmlTags.MinPitch} should not be higher than {SfxEventXmlTags.MaxPitch} for SFXEvent '{sfxEvent.Name}'" + }); + } + + if (sfxEvent.MinPan2D > sfxEvent.MaxPan2D) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"{SfxEventXmlTags.MinPan2D} should not be higher than {SfxEventXmlTags.MaxPan2D} for SFXEvent '{sfxEvent.Name}'" + }); + } + + if (sfxEvent.MinPredelay > sfxEvent.MaxPredelay) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"{SfxEventXmlTags.MinPredelay} should not be higher than {SfxEventXmlTags.MaxPredelay} for SFXEvent '{sfxEvent.Name}'" + }); + } + + if (sfxEvent.MinPostdelay > sfxEvent.MaxPostdelay) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"{SfxEventXmlTags.MinPostdelay} should not be higher than {SfxEventXmlTags.MaxPostdelay} for SFXEvent '{sfxEvent.Name}'" + }); + } + + sfxEvent.FixupValues(); + } + + protected override bool ParseTag( + XElement tag, + SfxEvent sfxEvent, + bool replace, + in IReadOnlyFrugalValueListDictionary parsedEntries) + { + if (tag.Name.LocalName == SfxEventXmlTags.UsePreset) + { + // TODO: Needs is Valid Check? + + var presetName = PetroglyphXmlStringParser.Instance.Parse(tag); + + Debug.Assert(!string.IsNullOrEmpty(presetName)); + + var presetNameCrc = HashingService.GetCrc32Upper(presetName.AsSpan(), PGConstants.DefaultPGEncoding); + if (presetNameCrc != default && parsedEntries.TryGetFirstValue(presetNameCrc, out var preset)) + sfxEvent.ApplyPreset(preset); + else + { + ErrorReporter?.Report(new XmlError(this, tag) + { + Message = $"Cannot to find preset '{presetName}' for SFXEvent '{sfxEvent.Name}'", + ErrorKind = XmlParseErrorKind.MissingReference + }); + } + + // Set the preset value regardless whether we found the SFXEvent or not. + sfxEvent.UsePresetName = presetName; + + return true; + } + + return base.ParseTag(tag, sfxEvent, replace, parsedEntries); + } + + + internal sealed class SfxEventXmlTagMapper(IServiceProvider serviceProvider) + : XmlTagMapper(serviceProvider) + { + protected override void BuildMappings() + { + AddMapping( + SfxEventXmlTags.IsPreset, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.IsPreset = val); + AddMapping( + SfxEventXmlTags.UsePreset, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.UsePresetName = val); + AddMapping( + SfxEventXmlTags.Samples, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => SetOrReplaceList(obj.SamplesInternal, val, replace)); + AddMapping( + SfxEventXmlTags.PreSamples, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => SetOrReplaceList(obj.PreSamplesInternal, val, replace)); + AddMapping( + SfxEventXmlTags.PostSamples, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => SetOrReplaceList(obj.PostSamplesInternal, val, replace)); + AddMapping( + SfxEventXmlTags.TextID, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => SetOrReplaceList(obj.LocalizedTextIDsInternal, val, replace)); + AddMapping( + SfxEventXmlTags.PlaySequentially, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.PlaySequentially = val); + AddMapping( + SfxEventXmlTags.Priority, + x => PetroglyphXmlByteParser.Instance.ParseClamped(x, SfxEvent.MinPriorityValue, SfxEvent.MaxPriorityValue), + (obj, val) => obj.Priority = val); + AddMapping( + SfxEventXmlTags.Probability, + x => PetroglyphXmlBytePercentParser.Instance.ParseAtMost(x, SfxEvent.MaxProbabilityValue), + (obj, val) => obj.Probability = val); + AddMapping( + SfxEventXmlTags.PlayCount, + x => PetroglyphXmlSByteParser.Instance.ParseAtLeast(x, SfxEvent.InfinitivePlayCountValue), + (obj, val) => obj.PlayCount = val); + AddMapping( + SfxEventXmlTags.LoopFadeInSeconds, + x => PetroglyphXmlFloatParser.Instance.ParseAtLeast(x, SfxEvent.MinLoopSecondsValue), + (obj, val) => obj.LoopFadeInSeconds = val); + AddMapping( + SfxEventXmlTags.LoopFadeOutSeconds, + x => PetroglyphXmlFloatParser.Instance.ParseAtLeast(x, SfxEvent.MinLoopSecondsValue), + (obj, val) => obj.LoopFadeOutSeconds = val); + AddMapping( + SfxEventXmlTags.MaxInstances, + x => PetroglyphXmlSByteParser.Instance.ParseAtLeast(x, SfxEvent.MinMaxInstancesValue), + (obj, val) => obj.MaxInstances = val); + AddMapping( + SfxEventXmlTags.MinVolume, + x => PetroglyphXmlBytePercentParser.Instance.ParseAtMost(x, SfxEvent.MaxVolumeValue), + (obj, val) => obj.MinVolume = val); + AddMapping( + SfxEventXmlTags.MaxVolume, + x => PetroglyphXmlBytePercentParser.Instance.ParseAtMost(x, SfxEvent.MaxVolumeValue), + (obj, val) => obj.MaxVolume = val); + AddMapping( + SfxEventXmlTags.MinPitch, + x => PetroglyphXmlByteParser.Instance.ParseClamped(x, SfxEvent.MinPitchValue, SfxEvent.MaxPitchValue), + (obj, val) => obj.MinPitch = val); + AddMapping( + SfxEventXmlTags.MaxPitch, + x => PetroglyphXmlByteParser.Instance.ParseClamped(x, SfxEvent.MinPitchValue, SfxEvent.MaxPitchValue), + (obj, val) => obj.MaxPitch = val); + AddMapping( + SfxEventXmlTags.MinPan2D, + x => PetroglyphXmlByteParser.Instance.ParseAtMost(x, SfxEvent.MaxPan2dValue), + (obj, val) => obj.MinPan2D = val); + AddMapping( + SfxEventXmlTags.MaxPan2D, + x => PetroglyphXmlByteParser.Instance.ParseAtMost(x, SfxEvent.MaxPan2dValue), + (obj, val) => obj.MaxPan2D = val); + AddMapping( + SfxEventXmlTags.MinPredelay, + PetroglyphXmlUnsignedIntegerParser.Instance.Parse, + (obj, val) => obj.MinPredelay = val); + AddMapping( + SfxEventXmlTags.MaxPredelay, + PetroglyphXmlUnsignedIntegerParser.Instance.Parse, + (obj, val) => obj.MaxPredelay = val); + AddMapping( + SfxEventXmlTags.MinPostdelay, + PetroglyphXmlUnsignedIntegerParser.Instance.Parse, + (obj, val) => obj.MinPostdelay = val); + AddMapping( + SfxEventXmlTags.MaxPostdelay, + PetroglyphXmlUnsignedIntegerParser.Instance.Parse, + (obj, val) => obj.MaxPostdelay = val); + AddMapping( + SfxEventXmlTags.VolumeSaturationDistance, + // I think it was planned at some time to support -1.0 and >= 0.0, since you don't get a warning when -1.0 is coded + // but the Engine coerces anything < 0.0 to 0.0. + x => PetroglyphXmlFloatParser.Instance.ParseAtLeast(x, SfxEvent.MinVolumeSaturationValue), + (obj, val) => obj.VolumeSaturationDistance = val); + AddMapping( + SfxEventXmlTags.KillsPreviousObjectSFX, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.KillsPreviousObjectsSfx = val); + AddMapping( + SfxEventXmlTags.OverlapTest, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.OverlapTestName = val); + AddMapping( + SfxEventXmlTags.Localize, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.IsLocalized = val); + AddMapping( + SfxEventXmlTags.Is2D, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.Is2D = val); + AddMapping( + SfxEventXmlTags.Is3D, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.Is3D = val); + AddMapping( + SfxEventXmlTags.IsGui, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.IsGui = val); + AddMapping( + SfxEventXmlTags.IsHudVo, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.IsHudVo = val); + AddMapping( + SfxEventXmlTags.IsUnitResponseVo, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.IsUnitResponseVo = val); + AddMapping( + SfxEventXmlTags.IsAmbientVo, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.IsAmbientVo = val); + AddMapping( + SfxEventXmlTags.ChainedSfxEvent, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.ChainedSfxEventName = val); + } + } + + internal static class SfxEventXmlTags + { + internal const string PresetXRef = "XREF_PRESET"; + internal const string IsPreset = "Is_Preset"; + internal const string UsePreset = "Use_Preset"; + internal const string Samples = "Samples"; + internal const string PreSamples = "Pre_Samples"; + internal const string PostSamples = "Post_Samples"; + internal const string TextID = "Text_ID"; + internal const string PlaySequentially = "Play_Sequentially"; + internal const string Priority = "Priority"; + internal const string Probability = "Probability"; + internal const string PlayCount = "Play_Count"; + internal const string LoopFadeInSeconds = "Loop_Fade_In_Seconds"; + internal const string LoopFadeOutSeconds = "Loop_Fade_Out_Seconds"; + internal const string MaxInstances = "Max_Instances"; + internal const string MinVolume = "Min_Volume"; + internal const string MaxVolume = "Max_Volume"; + internal const string MinPitch = "Min_Pitch"; + internal const string MaxPitch = "Max_Pitch"; + internal const string MinPan2D = "Min_Pan2D"; + internal const string MaxPan2D = "Max_Pan2D"; + internal const string MinPredelay = "Min_Predelay"; + internal const string MaxPredelay = "Max_Predelay"; + internal const string MinPostdelay = "Min_Postdelay"; + internal const string MaxPostdelay = "Max_Postdelay"; + internal const string VolumeSaturationDistance = "Volume_Saturation_Distance"; + internal const string KillsPreviousObjectSFX = "Kills_Previous_Object_SFX"; + internal const string OverlapTest = "Overlap_Test"; + internal const string Localize = "Localize"; + internal const string Is2D = "Is_2D"; + internal const string Is3D = "Is_3D"; + internal const string IsGui = "Is_GUI"; + internal const string IsHudVo = "Is_HUD_VO"; + internal const string IsUnitResponseVo = "Is_Unit_Response_VO"; + internal const string IsAmbientVo = "Is_Ambient_VO"; + internal const string ChainedSfxEvent = "Chained_SFXEvent"; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs deleted file mode 100644 index 00f4f61..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Linq; -using System.Xml; -using AnakinRaW.CommonUtilities.Collections; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using PG.Commons.Hashing; -using PG.Commons.Services; -using PG.StarWarsGame.Engine.IO; -using PG.StarWarsGame.Files.XML; -using PG.StarWarsGame.Files.XML.Data; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Engine.Xml.Parsers; - -internal sealed class XmlContainerContentParser : ServiceBase, IPetroglyphXmlParser -{ - public event EventHandler? XmlParseError; - - private readonly IXmlParserErrorReporter? _reporter; - private readonly IPetroglyphXmlFileParserFactory _fileParserFactory; - - public XmlContainerContentParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? reporter) : base(serviceProvider) - { - _reporter = reporter; - _fileParserFactory = serviceProvider.GetRequiredService(); - Name = GetType().FullName!; - } - - public string Name { get; } - - public void ParseEntriesFromFileListXml( - string xmlFile, - IGameRepository gameRepository, - string lookupPath, - FrugalValueListDictionary entries, - Action? onFileParseAction = null) where T : notnull - { - 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}'", xmlFile); - - var args = new XmlContainerParserErrorEventArgs(xmlFile, null, true) - { - // No reason to continue - Continue = false - }; - XmlParseError?.Invoke(this, args); - return; - } - - XmlFileListContainer? container; - - try - { - var containerParser = new XmlFileListParser(Services, _reporter); - container = containerParser.ParseFile(containerStream); - if (container is null) - throw new XmlException($"Unable to parse XML container file '{xmlFile}'."); - } - catch (XmlException e) - { - var args = new XmlContainerParserErrorEventArgs(xmlFile, e, true) - { - // No reason to continue - Continue = false - }; - XmlParseError?.Invoke(this, args); - return; - } - - - var xmlFiles = container.Files.Select(x => FileSystem.Path.Combine(lookupPath, x)).ToList(); - - var parser = _fileParserFactory.CreateFileParser(_reporter); - - foreach (var file in xmlFiles) - { - if (onFileParseAction is not null) - onFileParseAction(file); - - using var fileStream = gameRepository.TryOpenFile(file); - - if (fileStream is null) - { - _reporter?.Report(parser, XmlParseErrorEventArgs.FromMissingFile(file)); - Logger.LogWarning("Could not find XML file '{File}'", file); - - var args = new XmlContainerParserErrorEventArgs(file); - XmlParseError?.Invoke(this, args); - - if (args.Continue) - continue; - return; - } - - Logger.LogDebug("Parsing File '{File}'", file); - - try - { - parser.ParseFile(fileStream, entries); - } - catch (XmlException e) - { - _reporter?.Report(parser, new XmlParseErrorEventArgs(new XmlLocationInfo(file, 0), XmlParseErrorKind.Unknown, e.Message)); - - var args = new XmlContainerParserErrorEventArgs(file, e); - XmlParseError?.Invoke(this, args); - - if (!args.Continue) - return; - } - } - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerParserErrorEventArgs.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerParserErrorEventArgs.cs deleted file mode 100644 index 87ccce1..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerParserErrorEventArgs.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Xml; - -namespace PG.StarWarsGame.Engine.Xml.Parsers; - -internal class XmlContainerParserErrorEventArgs(string file, XmlException? exception = null, bool isXmlFileList = false) -{ - public bool Continue - { - get; - // Once this is set to false, there is no way back. - set => field &= value; - } = true; - - public bool ErrorInXmlFileList { get; } = isXmlFileList; - - public string File { get; } = file; - - [MemberNotNullWhen(true, nameof(Exception))] - public bool HasException => Exception is not null; - - public bool IsFileNotFound => !HasException; - - public XmlException? Exception { get; } = exception; -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs deleted file mode 100644 index 1565e9e..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Xml.Linq; -using AnakinRaW.CommonUtilities.Collections; -using Microsoft.Extensions.DependencyInjection; -using PG.Commons.Hashing; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Engine.Xml.Parsers; - -public abstract class XmlObjectParser( - IReadOnlyFrugalValueListDictionary parsedElements, - IServiceProvider serviceProvider, - IXmlParserErrorReporter? errorReporter = null) - : XmlObjectParser(parsedElements, serviceProvider, errorReporter) where TObject : XmlObject -{ - protected void Parse(TObject xmlObject, XElement element) - { - Parse(xmlObject, element, EmptyParseState.Instance); - } - - protected sealed override bool ParseTag(XElement tag, TObject xmlObject, in EmptyParseState parseState) - { - return ParseTag(tag, xmlObject); - } - - protected abstract bool ParseTag(XElement tag, TObject xmlObject); -} - -public readonly struct EmptyParseState -{ - public static readonly EmptyParseState Instance = new(); -} - - -public abstract class XmlObjectParser( - IReadOnlyFrugalValueListDictionary parsedElements, - IServiceProvider serviceProvider, - IXmlParserErrorReporter? errorReporter = null) - : PetroglyphXmlElementParser(errorReporter) where TObject : XmlObject -{ - protected IReadOnlyFrugalValueListDictionary ParsedElements { get; } = - parsedElements ?? throw new ArgumentNullException(nameof(parsedElements)); - - protected ICrc32HashingService HashingService { get; } = serviceProvider.GetRequiredService(); - - public abstract TObject Parse(XElement element, out Crc32 crc32); - - protected void Parse(TObject xmlObject, XElement element, in TParseState state) - { - foreach (var tag in element.Elements()) - { - if (!ParseTag(tag, xmlObject, state)) - { - OnParseError(new XmlParseErrorEventArgs(tag, XmlParseErrorKind.UnknownNode, - $"The node '{tag.Name}' is not supported.")); - break; - } - } - } - - protected abstract bool ParseTag(XElement tag, TObject xmlObject, in TParseState parseState); - - protected string GetXmlObjectName(XElement element, out Crc32 crc32, bool uppercaseName) - { - GetNameAttributeValue(element, out var name); - crc32 = uppercaseName - ? HashingService.GetCrc32Upper(name.AsSpan(), PGConstants.DefaultPGEncoding) - : HashingService.GetCrc32(name.AsSpan(), PGConstants.DefaultPGEncoding); - - if (crc32 == default) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"Name for XmlObject cannot be empty.")); - } - - return name; - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphStarWarsGameXmlParseSettings.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphStarWarsGameXmlParseSettings.cs new file mode 100644 index 0000000..3952f25 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphStarWarsGameXmlParseSettings.cs @@ -0,0 +1,10 @@ +namespace PG.StarWarsGame.Engine.Xml; + +public sealed record PetroglyphStarWarsGameXmlParseSettings +{ + public required string GameManager { get; init; } + + public bool InvalidFilesListXmlFailsInitialization { get; init; } = true; + + public bool InvalidObjectXmlFailsInitialization { get; init; } = false; +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphStarWarsGameXmlParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphStarWarsGameXmlParser.cs new file mode 100644 index 0000000..f849d05 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphStarWarsGameXmlParser.cs @@ -0,0 +1,230 @@ +using System; +using System.IO; +using System.Linq; +using System.Xml; +using AnakinRaW.CommonUtilities.Collections; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PG.Commons.Hashing; +using PG.Commons.Services; +using PG.StarWarsGame.Engine.ErrorReporting; +using PG.StarWarsGame.Engine.IO; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Data; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.Parsers; + +namespace PG.StarWarsGame.Engine.Xml; + +public sealed class PetroglyphStarWarsGameXmlParser : ServiceBase, IPetroglyphXmlParserInfo +{ + private readonly IGameRepository _gameRepository; + private readonly PetroglyphStarWarsGameXmlParseSettings _settings; + private readonly IGameEngineErrorReporter _reporter; + private readonly IPetroglyphXmlFileParserFactory _fileParserFactory; + + public string Name { get; } + + public PetroglyphStarWarsGameXmlParser( + IGameRepository gameRepository, + PetroglyphStarWarsGameXmlParseSettings settings, + IServiceProvider serviceProvider, + IGameEngineErrorReporter reporter) + : base(serviceProvider) + { + _gameRepository = gameRepository; + _settings = settings; + _reporter = reporter; + _fileParserFactory = serviceProvider.GetRequiredService(); + Name = GetType().FullName!; + } + + public T? ParseFile(string xmlFile, XmlFileParser parser) where T : XmlObject + { + return ParseCore(xmlFile, parser.ParseFile, () => null); + } + + public XmlFileList ParseFileList(string xmlFile) + { + return ParseCore(xmlFile, + stream => new XmlFileListParser(Services, _reporter).ParseFile(stream), + () => XmlFileList.Empty(new XmlLocationInfo(xmlFile, null))); + } + + public void ParseEntriesFromFileListXml( + string xmlFile, + string lookupPath, + FrugalValueListDictionary entries, + Action? onParseContainerAction = null) where T : NamedXmlObject + { + var container = ParseFileList(xmlFile); + + var xmlFiles = container.Files.Select(x => FileSystem.Path.Combine(lookupPath, x)).ToList(); + + var parser = new XmlContainerFileParser(Services, + _fileParserFactory.CreateNamedXmlObjectParser(_gameRepository.EngineType, _reporter), _reporter); + + foreach (var file in xmlFiles) + { + onParseContainerAction?.Invoke(file); + if (!ParseObjectsFromContainerFile(file, parser, entries)) + return; + } + } + + public bool ParseObjectsFromContainerFile( + string xmlFile, + IXmlContainerFileParser parser, + IFrugalValueListDictionary entries) where T : NamedXmlObject + { + return ParseCore(xmlFile, stream => + { + parser.ParseFile(stream, entries); + return true; + }, () => _settings.InvalidObjectXmlFailsInitialization); + } + + private T ParseCore(string xmlFile, Func parseAction, Func invalidFileAction) + { + Logger.LogDebug("Parsing file '{XmlFile}'", xmlFile); + + using var fileStream = _gameRepository.TryOpenFile(xmlFile); + + if (fileStream is null) + { + var message = $"Could not find XML file '{xmlFile}'"; + Logger.LogWarning(message); + + _reporter.Report(new XmlError(this, locationInfo: new XmlLocationInfo(xmlFile, null)) + { + Message = message, + ErrorKind = XmlParseErrorKind.MissingFile + }); + + if (_settings.InvalidObjectXmlFailsInitialization) + { + _reporter.Report(new InitializationError + { + GameManager = _settings.GameManager, + Message = message, + }); + } + + return invalidFileAction(); + } + + try + { + return parseAction(fileStream); + } + catch (XmlException e) + { + _reporter.Report(new XmlError(this, locationInfo: new XmlLocationInfo(xmlFile, e.LineNumber)) + { + ErrorKind = XmlParseErrorKind.Unknown, + Message = e.Message, + }); + if (_settings.InvalidObjectXmlFailsInitialization) + { + _reporter.Report(new InitializationError + { + GameManager = _settings.GameManager, + Message = e.Message, + }); + } + + return invalidFileAction(); + } + } +} + +enum XmlDataType +{ + DB_DATA_TYPE_BOOL = 0x0, + DB_DATA_TYPE_DWORD = 0x1, + DB_DATA_TYPE_UNSIGNED_CHAR = 0x2, + DB_DATA_TYPE_SIGNED_CHAR = 0x3, + DB_DATA_TYPE_UNSIGNED_CHAR_PERCENT = 0x4, + DB_DATA_TYPE_UNSIGNED_INT = 0x5, + DB_DATA_TYPE_SIGNED_INT = 0x6, + DB_DATA_TYPE_UNSIGNED_INT_PERCENT = 0x7, + DB_DATA_TYPE_FLOAT = 0x8, + DB_DATA_TYPE_UNIT_FLOAT = 0x9, + DB_DATA_TYPE_DOUBLE = 0xa, + DB_DATA_TYPE_UNIT_DOUBLE = 0xb, + DB_DATA_TYPE_SIGNED_INT_HEX = 0xc, + DB_DATA_TYPE_DWORD_HEX = 0xd, + DB_DATA_TYPE_CONVERSION = 0xe, + DB_DATA_TYPE_VECTOR2 = 0xf, + DB_DATA_TYPE_VECTOR3 = 0x10, + DB_DATA_TYPE_VECTOR4 = 0x11, + DB_DATA_TYPE_DYN_VECTOR_INT = 0x12, + DB_DATA_TYPE_DYN_VECTOR_FLOAT = 0x13, + DB_DATA_TYPE_DYN_VECTOR_VECTOR3 = 0x14, + DB_DATA_TYPE_DYN_VECTOR_VECTOR2 = 0x15, + DB_DATA_TYPE_RGBA = 0x16, + DB_DATA_TYPE_STL_STRING = 0x17, + DB_DATA_TYPE_STL_LIST_STL_STRINGS = 0x18, + DB_DATA_TYPE_STL_VECTOR_DWORDS = 0x19, + DB_DATA_TYPE_STL_VECTOR_DWORDS_HEX = 0x1a, + DB_DATA_TYPE_STL_VECTOR_STL_STRINGS = 0x1b, + DB_DATA_TYPE_STL_STRING_UPPER = 0x1c, + DB_DATA_TYPE_OBJECT_REFERENCE = 0x1d, + DB_DATA_TYPE_MULTI_OBJECT_REFERENCE = 0x1e, + DB_DATA_TYPE_SFX_EVENT = 0x1f, + DB_DATA_TYPE_SPEECH_EVENT = 0x20, + DB_DATA_TYPE_MUSIC_EVENT = 0x21, + DB_DATA_TYPE_SFXEVENT_OVERRIDE_LIST_ENTRY = 0x22, + DB_DATA_TYPE_WEATHER_SFXEVENT_LOOP_LIST_ENTRY = 0x23, + DB_DATA_TYPE_WEATHER_SFXEVENT_INTERMITTENT_LIST_ENTRY = 0x24, + DB_DATA_TYPE_WEATHER_SFX_EVENT_PAIR_ARRAY_ENTRY = 0x25, + DB_DATA_TYPE_AMBIENT_SFXEVENT_INTERMITTENT_LIST_ENTRY = 0x26, + DB_DATA_TYPE_DYN_VECTOR_SFX_EVENT_ENTRY = 0x27, + DB_DATA_TYPE_TRIPLE_OBJ_TYPE_AND_SPEECH_EVENT_ENTRY = 0x28, + DB_DATA_TYPE_DYN_VECTOR_FACTION_AND_MUSIC_EVENT_PAIR_ENTRY = 0x29, + DB_DATA_TYPE_DYN_VECTOR_STL_STRINGS = 0x2a, + DB_DATA_TYPE_FACTION_DATA_OVERRIDE_UINT = 0x2b, + DB_DATA_TYPE_FACTION_DATA_OVERRIDE_FLOAT = 0x2c, + DB_DATA_TYPE_FACTION_DATA_OVERRIDE_NAMEREF = 0x2d, + DB_DATA_TYPE_QUADRATIC = 0x2e, + DB_DATA_TYPE_SPLINE = 0x2f, + DB_DATA_TYPE_LINEAR = 0x30, + DB_DATA_TYPE_PARABOLIC = 0x31, + DB_DATA_TYPE_SHIPCLASS = 0x32, + DB_DATA_TYPE_SCRIPT_VARIABLE = 0x33, + DB_DATA_TYPE_CONVERSION_STRING_PAIR_VECTOR = 0x34, + DB_DATA_TYPE_CONVERSION_OBJECT_REF_VECTOR = 0x35, + DB_DATA_TYPE_STL_VECTOR_OBJECT_REFERENCE_STRING_PAIR = 0x36, + DB_DATA_TYPE_STARTING_FORCE_DEFINITION = 0x37, + DB_DATA_TYPE_STL_VECTOR_ABILITIES = 0x38, + DB_DATA_TYPE_STL_VECTOR_ABILITIES_DATA = 0x39, + DB_DATA_TYPE_STL_VECTOR_ACTIONS = 0x3a, + DB_DATA_TYPE_STL_HASHMAP_DAMAGE_TO_DEATH_CLONE = 0x3b, + DB_DATA_TYPE_STL_VECTOR_STRING_INT_PAIR = 0x3c, + DB_DATA_TYPE_LIST_GAME_OBJECT_CATEGORY_FLOAT_PAIR = 0x3d, + DB_DATA_TYPE_DAMAGE_TO_ARMOR_MOD_ENTRY = 0x3e, + DB_DATA_QUOTED_STRING_DYN_VECTOR_ENTRY = 0x3f, + DB_DATA_TYPE_LANGUAGE_STL_STRING_ARRAY_ENTRY = 0x40, + DB_DATA_TYPE_HARD_POINT_TYPE_ARRAY_OF_DYN_VECTOR_STL_STRINGS = 0x41, + DB_DATA_TYPE_HARD_POINT_TYPE_ARRAY_OF_SFXEVENTS = 0x42, + DB_DATA_TYPE_FACTION = 0x43, + DB_DATA_TYPE_WEIGHTED_TYPE_LIST = 0x44, + DB_DATA_TYPE_STL_VECTOR_CONVERSION = 0x45, + DB_DATA_TYPE_STL_VECTOR_OBJECT_REFERENCE_INT_PAIR = 0x46, + DB_DATA_TYPE_DISCRETE_DISTRIBUTION_NAME_REFERENCE = 0x47, + DB_DATA_TYPE_STL_HASHMAP_CONVERSION_TO_FLOAT = 0x48, + DB_DATA_TYPE_UNIT_ABILITY = 0x49, + DB_TYPE_PROJECTILE_CATEGORY = 0x4a, + DB_DATA_TYPE_RENDER_MODE = 0x4b, + DB_DATA_TYPE_NON_HERO_ABILITIES_SFX_EVENT_PAIR = 0x4c, + DB_DATA_TYPE_UNIT_MODE_MULTIPLIER_MOD = 0x4d, + DB_DATA_TYPE_UNIT_MODE_FLAG_MOD = 0x4e, + DB_DATA_TYPE_STRING_INT_PAIR = 0x4f, + DB_DATA_TYPE_BIT_BOOL = 0x50, + DB_DATA_TYPE_STL_VECTOR_PROJECTILE_CATEGORIES = 0x51, + DB_DATA_TYPE_COMBAT_MOD = 0x52, + DB_DATA_TYPE_MULTI_OBJECT_REFERENCE_WITH_OR = 0x53, + DB_DATA_TYPE_CONVERSION_PTR = 0x54, + DB_DATA_TYPE_CONVERSION_OBJECT_REF_VECTOR_PTR = 0x55, + DB_DATA_TYPE_CONVERSION_64 = 0x56 +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs deleted file mode 100644 index 550f459..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using PG.StarWarsGame.Engine.Audio.Sfx; -using PG.StarWarsGame.Engine.CommandBar.Xml; -using PG.StarWarsGame.Engine.GameObjects; -using PG.StarWarsGame.Engine.Xml.Parsers.File; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Engine.Xml; - -internal sealed class PetroglyphXmlFileParserFactory(IServiceProvider serviceProvider) : IPetroglyphXmlFileParserFactory -{ - public IPetroglyphXmlFileContainerParser CreateFileParser(IXmlParserErrorReporter? errorReporter) where T : notnull - { - if (typeof(T) == typeof(SfxEvent)) - return (IPetroglyphXmlFileContainerParser) new SfxEventFileParser(serviceProvider, errorReporter); - - if (typeof(T) == typeof(CommandBarComponentData)) - return (IPetroglyphXmlFileContainerParser)new CommandBarComponentFileParser(serviceProvider, errorReporter); - - if (typeof(T) == typeof(GameObject)) - return (IPetroglyphXmlFileContainerParser)new GameObjectFileParser(serviceProvider, errorReporter); - - - throw new NotImplementedException($"Unable to get parser for type {typeof(T)}"); - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/CommandBarComponentTags.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/CommandBarComponentTags.cs deleted file mode 100644 index bc5498b..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/CommandBarComponentTags.cs +++ /dev/null @@ -1,108 +0,0 @@ -namespace PG.StarWarsGame.Engine.Xml.Tags; - -public static class CommandBarComponentTags -{ - public const string SelectedTextureName = "Selected_Texture_Name"; - public const string BlankTextureName = "Blank_Texture_Name"; - public const string IconTextureName = "Icon_Texture_Name"; - public const string IconAlternateTextureName = "Icon_Alternate_Texture_Name"; - public const string MouseOverTextureName = "Mouse_Over_Texture_Name"; - public const string DisabledTextureName = "Disabled_Texture_Name"; - public const string FlashTextureName = "Flash_Texture_Name"; - public const string BarTextureName = "Bar_Texture_Name"; - public const string BarOverlayName = "Bar_Overlay_Name"; - public const string BuildTextureName = "Build_Texture_Name"; - public const string ModelName = "Model_Name"; - public const string BoneName = "Bone_Name"; - public const string CursorTextureName = "Cursor_Texture_Name"; - public const string FontName = "Font_Name"; - public const string AlternateFontName = "Alternate_Font_Name"; - public const string TooltipText = "Tooltip_Text"; - public const string ClickSfx = "Click_SFX"; - public const string MouseOverSfx = "Mouse_Over_SFX"; - public const string LowerEffectTextureName = "Lower_Effect_Texture_Name"; - public const string UpperEffectTextureName = "Upper_Effect_Texture_Name"; - public const string OverlayTextureName = "Overlay_Texture_Name"; - public const string Overlay2TextureName = "Overlay2_Texture_Name"; - public const string RightClickSfx = "Right_Click_SFX"; - public const string Type = "Type"; - public const string Group = "Group"; - public const string DragAndDrop = "Drag_And_Drop"; - public const string DragSelect = "Drag_Select"; - public const string Receptor = "Receptor"; - public const string Toggle = "Toggle"; - public const string Tab = "Tab"; - public const string AssociatedText = "Associated_Text"; - public const string Hidden = "Hidden"; - public const string Scale = "Scale"; - public const string Color = "Color"; - public const string TextColor = "Text_Color"; - public const string TextColor2 = "Text_Color2"; - public const string Size = "Size"; - public const string ClearColor = "Clear_Color"; - public const string Disabled = "Disabled"; - public const string SwapTexture = "Swap_Texture"; - public const string BaseLayer = "Base_Layer"; - public const string DrawAdditive = "Draw_Additive"; - public const string TextOffset = "Text_Offset"; - public const string TextOffset2 = "Text_Offset2"; - public const string Offset = "Offset"; - public const string DefaultOffset = "Default_Offset"; - public const string DefaultOffsetWidescreen = "Default_Offset_Widescreen"; - public const string IconOffset = "Icon_Offset"; - public const string MouseOverOffset = "Mouse_Over_Offset"; - public const string DisabledOffset = "Disabled_Offset"; - public const string BuildDialOffset = "Build_Dial_Offset"; - public const string BuildDial2Offset = "Build_Dial2_Offset"; - public const string LowerEffectOffset = "Lower_Effect_Offset"; - public const string UpperEffectOffset = "Upper_Effect_Offset"; - public const string OverlayOffset = "Overlay_Offset"; - public const string Overlay2Offset = "Overlay2_Offset"; - public const string Editable = "Editable"; - public const string MaxTextLength = "Max_Text_Length"; - public const string BlinkRate = "Blink_Rate"; - public const string FontPointSize = "Font_Point_Size"; - public const string TextOutline = "Text_Outline"; - public const string MaxTextWidth = "Max_Text_Width"; - public const string Stackable = "Stackable"; - public const string ModelOffsetX = "Model_Offset_X"; - public const string ModelOffsetY = "Model_Offset_Y"; - public const string ScaleModelX = "Scale_Model_X"; - public const string ScaleModelY = "Scale_Model_Y"; - public const string Collideable = "Collideable"; - public const string TextEmboss = "Text_Emboss"; - public const string ShouldGhost = "Should_Ghost"; - public const string GhostBaseOnly = "Ghost_Base_Only"; - public const string MaxBarLevel = "Max_Bar_Level"; - public const string MaxBarColor = "Max_Bar_Color"; - public const string CrossFade = "Cross_Fade"; - public const string LeftJustified = "Left_Justified"; - public const string RightJustified = "Right_Justified"; - public const string NoShell = "No_Shell"; - public const string SnapDrag = "Snap_Drag"; - public const string SnapLocation = "Snap_Location"; - public const string BlinkDuration = "Blink_Duration"; - public const string ScaleDuration = "Scale_Duration"; - public const string OffsetRender = "Offset_Render"; - public const string BlinkFade = "Blink_Fade"; - public const string NoHiddenCollision = "No_Hidden_Collision"; - public const string ManualOffset = "Manual_Offset"; - public const string SelectedAlpha = "Selected_Alpha"; - public const string PixelAlign = "Pixel_Align"; - public const string CanDragStack = "Can_Drag_Stack"; - public const string CanAnimate = "Can_Animate"; - public const string AnimFps = "Anim_FPS"; - public const string LoopAnim = "Loop_Anim"; - public const string SmoothBar = "Smooth_Bar"; - public const string OutlinedBar = "Outlined_Bar"; - public const string DragBack = "Drag_Back"; - public const string LowerEffectAdditive = "Lower_Effect_Additive"; - public const string UpperEffectAdditive = "Upper_Effect_Additive"; - public const string ClickShift = "Click_Shift"; - public const string TutorialScene = "Tutorial_Scene"; - public const string DialogScene = "Dialog_Scene"; - public const string ShouldRenderAtDragPos = "Should_Render_At_Drag_Pos"; - public const string DisableDarken = "Disable_Darken"; - public const string AnimateBack = "Animate_Back"; - public const string AnimateUpperEffect = "Animate_Upper_Effect"; -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/ComponentTextureKeyExtensions.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/ComponentTextureKeyExtensions.cs deleted file mode 100644 index c072c90..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/ComponentTextureKeyExtensions.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using PG.StarWarsGame.Engine.GuiDialog; - -namespace PG.StarWarsGame.Engine.Xml.Tags; - -internal static class ComponentTextureKeyExtensions -{ - public static bool TryConvertToKey(ReadOnlySpan keyValue, out GuiComponentType key) - { - key = keyValue switch - { - "Button_Left" => GuiComponentType.ButtonLeft, - "Button_Middle" => GuiComponentType.ButtonMiddle, - "Button_Right" => GuiComponentType.ButtonRight, - "Button_Left_Mouse_Over" => GuiComponentType.ButtonLeftMouseOver, - "Button_Middle_Mouse_Over" => GuiComponentType.ButtonMiddleMouseOver, - "Button_Right_Mouse_Over" => GuiComponentType.ButtonRightMouseOver, - "Button_Left_Pressed" => GuiComponentType.ButtonLeftPressed, - "Button_Middle_Pressed" => GuiComponentType.ButtonMiddlePressed, - "Button_Right_Pressed" => GuiComponentType.ButtonRightPressed, - "Button_Left_Disabled" => GuiComponentType.ButtonLeftDisabled, - "Button_Middle_Disabled" => GuiComponentType.ButtonMiddleDisabled, - "Button_Right_Disabled" => GuiComponentType.ButtonRightDisabled, - - "Check_Off" => GuiComponentType.CheckOff, - "Check_On" => GuiComponentType.CheckOn, - - "Dial_Left" => GuiComponentType.DialLeft, - "Dial_Middle" => GuiComponentType.DialMiddle, - "Dial_Right" => GuiComponentType.DialRight, - "Dial_Plus" => GuiComponentType.DialPlus, - "Dial_Plus_Mouse_Over" => GuiComponentType.DialPlusMouseOver, - "Dial_Plus_Pressed" => GuiComponentType.DialPlusPressed, - "Dial_Minus" => GuiComponentType.DialMinus, - "Dial_Minus_Mouse_Over" => GuiComponentType.DialMinusMouseOver, - "Dial_Minus_Pressed" => GuiComponentType.DialMinusPressed, - "Dial_Tab" => GuiComponentType.DialTab, - - "Frame_Bottom" => GuiComponentType.FrameBottom, - "Frame_Bottom_Left" => GuiComponentType.FrameBottomLeft, - "Frame_Bottom_Right" => GuiComponentType.FrameBottomRight, - "Frame_Background" => GuiComponentType.FrameBackground, - "Frame_Left" => GuiComponentType.FrameLeft, - "Frame_Right" => GuiComponentType.FrameRight, - "Frame_Top" => GuiComponentType.FrameTop, - "Frame_Top_Left" => GuiComponentType.FrameTopLeft, - "Frame_Top_Right" => GuiComponentType.FrameTopRight, - "Frame_Top_Transition_Left" => GuiComponentType.FrameTopTransitionLeft, - "Frame_Top_Transition_Right" => GuiComponentType.FrameTopTransitionRight, - "Frame_Bottom_Transition_Left" => GuiComponentType.FrameBottomTransitionLeft, - "Frame_Bottom_Transition_Right" => GuiComponentType.FrameBottomTransitionRight, - "Frame_Left_Transition_Top" => GuiComponentType.FrameLeftTransitionTop, - "Frame_Left_Transition_Bottom" => GuiComponentType.FrameLeftTransitionBottom, - "Frame_Right_Transition_Top" => GuiComponentType.FrameRightTransitionTop, - "Frame_Right_Transition_Bottom" => GuiComponentType.FrameRightTransitionBottom, - - "Radio_Off" => GuiComponentType.RadioOff, - "Radio_On" => GuiComponentType.RadioOn, - "Radio_Disabled" => GuiComponentType.RadioDisabled, - "Radio_Mouse_Over" => GuiComponentType.RadioMouseOver, - - "Scroll_Down_Button" => GuiComponentType.ScrollDownButton, - "Scroll_Down_Button_Pressed" => GuiComponentType.ScrollDownButtonPressed, - "Scroll_Down_Button_Mouse_Over" => GuiComponentType.ScrollDownButtonMouseOver, - "Scroll_Down_Button_Disabled" => GuiComponentType.ScrollDownButtonDisabled, - "Scroll_Middle" => GuiComponentType.ScrollMiddle, - "Scroll_Middle_Disabled" => GuiComponentType.ScrollMiddleDisabled, - "Scroll_Tab" => GuiComponentType.ScrollTab, - "Scroll_Tab_Disabled" => GuiComponentType.ScrollTabDisabled, - "Scroll_Up_Button" => GuiComponentType.ScrollUpButton, - "Scroll_Up_Button_Pressed" => GuiComponentType.ScrollUpButtonPressed, - "Scroll_Up_Button_Mouse_Over" => GuiComponentType.ScrollUpButtonMouseOver, - "Scroll_Up_Button_Disabled" => GuiComponentType.ScrollUpButtonDisabled, - - "Trackbar_Scroll_Down_Button" => GuiComponentType.TrackbarScrollDownButton, - "Trackbar_Scroll_Down_Button_Pressed" => GuiComponentType.TrackbarScrollDownButtonPressed, - "Trackbar_Scroll_Down_Button_Mouse_Over" => GuiComponentType.TrackbarScrollDownButtonMouseOver, - "Trackbar_Scroll_Down_Button_Disabled" => GuiComponentType.TrackbarScrollDownButtonDisabled, - "Trackbar_Scroll_Middle" => GuiComponentType.TrackbarScrollMiddle, - "Trackbar_Scroll_Middle_Disabled" => GuiComponentType.TrackbarScrollMiddleDisabled, - "Trackbar_Scroll_Tab" => GuiComponentType.TrackbarScrollTab, - "Trackbar_Scroll_Tab_Disabled" => GuiComponentType.TrackbarScrollTabDisabled, - "Trackbar_Scroll_Up_Button" => GuiComponentType.TrackbarScrollUpButton, - "Trackbar_Scroll_Up_Button_Pressed" => GuiComponentType.TrackbarScrollUpButtonPressed, - "Trackbar_Scroll_Up_Button_Mouse_Over" => GuiComponentType.TrackbarScrollUpButtonMouseOver, - "Trackbar_Scroll_Up_Button_Disabled" => GuiComponentType.TrackbarScrollUpButtonDisabled, - - "Small_Frame_Bottom" => GuiComponentType.SmallFrameBottom, - "Small_Frame_Bottom_Left" => GuiComponentType.SmallFrameBottomLeft, - "Small_Frame_Bottom_Right" => GuiComponentType.SmallFrameBottomRight, - "Small_Frame_Left" => GuiComponentType.SmallFrameMiddleLeft, - "Small_Frame_Right" => GuiComponentType.SmallFrameMiddleRight, - "Small_Frame_Top" => GuiComponentType.SmallFrameTop, - "Small_Frame_Top_Left" => GuiComponentType.SmallFrameTopLeft, - "Small_Frame_Top_Right" => GuiComponentType.SmallFrameTopRight, - "Small_Frame_Background" => GuiComponentType.SmallFrameBackground, - - "Combo_Box_Popdown_Button" => GuiComponentType.ComboboxPopdown, - "Combo_Box_Popdown_Button_Pressed" => GuiComponentType.ComboboxPopdownPressed, - "Combo_Box_Popdown_Button_Mouse_Over" => GuiComponentType.ComboboxPopdownMouseOver, - "Combo_Box_Text_Box" => GuiComponentType.ComboboxTextBox, - "Combo_Box_Left_Cap" => GuiComponentType.ComboboxLeftCap, - - "Progress_Bar_Left" => GuiComponentType.ProgressLeft, - "Progress_Bar_Middle_Off" => GuiComponentType.ProgressMiddleOff, - "Progress_Bar_Middle_On" => GuiComponentType.ProgressMiddleOn, - "Progress_Bar_Right" => GuiComponentType.ProgressRight, - - "Scanlines" => GuiComponentType.Scanlines, - _ => (GuiComponentType)int.MaxValue - }; - return (int)key != int.MaxValue; - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/SfxEventXmlTags.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/SfxEventXmlTags.cs deleted file mode 100644 index 959da6b..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/SfxEventXmlTags.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace PG.StarWarsGame.Engine.Xml.Tags; - -public static class SfxEventXmlTags -{ - internal const string PresetXRef = "XREF_PRESET"; - - public const string IsPreset = "Is_Preset"; - public const string UsePreset = "Use_Preset"; - public const string Samples = "Samples"; - public const string PreSamples = "Pre_Samples"; - public const string PostSamples = "Post_Samples"; - public const string TextID = "Text_ID"; - public const string PlaySequentially = "Play_Sequentially"; - public const string Priority = "Priority"; - public const string Probability = "Probability"; - public const string PlayCount = "Play_Count"; - public const string LoopFadeInSeconds = "Loop_Fade_In_Seconds"; - public const string LoopFadeOutSeconds = "Loop_Fade_Out_Seconds"; - public const string MaxInstances = "Max_Instances"; - public const string MinVolume = "Min_Volume"; - public const string MaxVolume = "Max_Volume"; - public const string MinPitch = "Min_Pitch"; - public const string MaxPitch = "Max_Pitch"; - public const string MinPan2D = "Min_Pan2D"; - public const string MaxPan2D = "Max_Pan2D"; - public const string MinPredelay = "Min_Predelay"; - public const string MaxPredelay = "Max_Predelay"; - public const string MinPostdelay = "Min_Postdelay"; - public const string MaxPostdelay = "Max_Postdelay"; - public const string VolumeSaturationDistance = "Volume_Saturation_Distance"; - public const string KillsPreviousObjectSFX = "Kills_Previous_Object_SFX"; - public const string OverlapTest = "Overlap_Test"; - public const string Localize = "Localize"; - public const string Is2D = "Is_2D"; - public const string Is3D = "Is_3D"; - public const string IsGui = "Is_GUI"; - public const string IsHudVo = "Is_HUD_VO"; - public const string IsUnitResponseVo = "Is_Unit_Response_VO"; - public const string IsAmbientVo = "Is_Ambient_VO"; - public const string ChainedSfxEvent = "Chained_SFXEvent"; -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObject.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObject.cs deleted file mode 100644 index 71840fe..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObject.cs +++ /dev/null @@ -1,12 +0,0 @@ -using PG.StarWarsGame.Files.XML; - -namespace PG.StarWarsGame.Engine.Xml; - -public abstract class XmlObject(XmlLocationInfo location) -{ - public XmlLocationInfo Location { get; } = location; - - internal virtual void CoerceValues() - { - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObjectParser.cs new file mode 100644 index 0000000..923e6b1 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObjectParser.cs @@ -0,0 +1,48 @@ +using System.Xml.Linq; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Data; +using PG.StarWarsGame.Files.XML.ErrorHandling; + +namespace PG.StarWarsGame.Engine.Xml; + +public abstract class XmlObjectParser( + GameEngineType engine, + XmlTagMapper tagMapper, + IXmlParserErrorReporter? errorReporter = null) + : XmlObjectParserBase.EmptyParseState>(engine, tagMapper, errorReporter) + where TObject : XmlObject +{ + public TObject Parse(XElement element) + { + var xmlObject = CreateXmlObject(XmlLocationInfo.FromElement(element)); + ParseObject(xmlObject, element, false, EmptyParseState.Instance); + ValidateAndFixupValues(xmlObject, element, EmptyParseState.Instance); + return xmlObject; + } + + protected abstract TObject CreateXmlObject(XmlLocationInfo location); + + protected sealed override bool ParseTag(XElement tag, TObject xmlObject, bool replace, in EmptyParseState parseState) + { + return ParseTag(tag, xmlObject, replace); + } + + protected sealed override void ValidateAndFixupValues(TObject xmlObject, XElement element, in EmptyParseState parseState) + { + ValidateAndFixupValues(xmlObject, element); + } + + protected virtual bool ParseTag(XElement tag, TObject xmlObject, bool replace) + { + return XmlTagMapper.TryParseEntry(tag, xmlObject, replace, Engine); + } + + protected virtual void ValidateAndFixupValues(TObject xmlObject, XElement element) + { + } + + public readonly struct EmptyParseState + { + public static readonly EmptyParseState Instance = new(); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObjectParserBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObjectParserBase.cs new file mode 100644 index 0000000..9a23ddd --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObjectParserBase.cs @@ -0,0 +1,59 @@ +using System; +using System.Xml.Linq; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Data; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.Parsers; + +namespace PG.StarWarsGame.Engine.Xml; + +public abstract class XmlObjectParserBase( + GameEngineType engine, + XmlTagMapper tagMapper, + IXmlParserErrorReporter? errorReporter) + : PetroglyphXmlParserBase(errorReporter) where TObject : XmlObject +{ + protected readonly XmlTagMapper XmlTagMapper = tagMapper ?? throw new ArgumentNullException(nameof(tagMapper)); + protected readonly GameEngineType Engine = engine; + + protected virtual bool IgnoreEmptyValue => true; + + protected virtual void ValidateAndFixupValues(TObject namedXmlObject, XElement element, in TParseState state) + { + } + + protected virtual void ParseObject(TObject xmlObject, XElement element, bool replace, in TParseState state) + { + ParseTags(xmlObject, element, replace, in state); + } + + protected virtual bool ParseTag(XElement tag, TObject xmlObject, bool replace, in TParseState parseState) + { + return IsTagValid(tag) && XmlTagMapper.TryParseEntry(tag, xmlObject, replace, Engine); + } + + protected void ParseTags(TObject xmlObject, XElement element, bool replace, in TParseState state) + { + foreach (var tag in element.Elements()) + { + if (!tag.HasElements) + { + if (string.IsNullOrEmpty(tag.PGValue) && IgnoreEmptyValue) + continue; + + if (!ParseTag(tag, xmlObject, replace, state)) + { + ErrorReporter?.Report(new XmlError(this, tag) + { + Message = $"The node '{tag.Name}' is not supported.", + ErrorKind = XmlParseErrorKind.UnknownNode + }); + } + } + else + { + ParseObject(xmlObject, tag, replace, in state); + } + } + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObjectParserFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObjectParserFactory.cs new file mode 100644 index 0000000..eeee6e2 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObjectParserFactory.cs @@ -0,0 +1,30 @@ +using PG.StarWarsGame.Engine.Audio.Sfx; +using PG.StarWarsGame.Engine.CommandBar.Xml; +using PG.StarWarsGame.Engine.GameObjects; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using System; +using PG.StarWarsGame.Files.XML.Data; +using PG.StarWarsGame.Engine.Xml.Parsers; + +namespace PG.StarWarsGame.Engine.Xml; + +internal sealed class XmlObjectParserFactory(IServiceProvider serviceProvider) : IPetroglyphXmlFileParserFactory +{ + public NamedXmlObjectParser CreateNamedXmlObjectParser(GameEngineType engine, IXmlParserErrorReporter? errorReporter) where T : NamedXmlObject + { + if (typeof(T) == typeof(SfxEvent)) + return ChangeType(new SfxEventParser(engine, serviceProvider, errorReporter)); + if (typeof(T) == typeof(CommandBarComponentData)) + return ChangeType(new CommandBarComponentParser(engine, serviceProvider, errorReporter)); + if (typeof(T) == typeof(GameObject)) + return ChangeType(new GameObjectParser(engine, serviceProvider, errorReporter)); + + + throw new ParserNotFoundException(typeof(T)); + } + + private static NamedXmlObjectParser ChangeType(object obj) where T : NamedXmlObject + { + return (NamedXmlObjectParser) obj; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlTagMapper.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlTagMapper.cs new file mode 100644 index 0000000..32fc2fb --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlTagMapper.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Xml.Linq; +using AnakinRaW.CommonUtilities; +using Microsoft.Extensions.DependencyInjection; +using PG.Commons.Hashing; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Data; + +namespace PG.StarWarsGame.Engine.Xml; + +public abstract class XmlTagMapper where TObject : XmlObject +{ + private delegate void ParserValueAction(TObject target, XElement element, bool replace); + + private readonly struct MappingEntry(SupportedEngines supportedEngines, ParserValueAction action) + { + public readonly SupportedEngines SupportedEngines = supportedEngines; + public readonly ParserValueAction Action = action; + } + + [Flags] + public enum SupportedEngines + { + Eaw = 1, + Foc = 2, + All = Eaw | Foc + } + + private readonly Dictionary _tagMappings = new(); + private readonly ICrc32HashingService _crcService; + + protected XmlTagMapper(IServiceProvider serviceProvider) + { + if (serviceProvider == null) + throw new ArgumentNullException(nameof(serviceProvider)); + _crcService = serviceProvider.GetRequiredService(); + + // ReSharper disable once VirtualMemberCallInConstructor + BuildMappings(); + } + + protected abstract void BuildMappings(); + + protected static void SetOrReplaceList(IList destinationList, IEnumerable values, bool replace) + { + if (replace) + destinationList.Clear(); + foreach (var value in values) + destinationList.Add(value); + } + + protected void AddMapping( + string tagName, + Func parser, + Action setter, + SupportedEngines supportedEngines = SupportedEngines.All) + { + AddMapping(tagName, parser, (target, value, _) => setter(target, value), supportedEngines); + } + + protected void AddMapping( + string tagName, + Func parser, + Action setter, + SupportedEngines supportedEngines = SupportedEngines.All) + { + ThrowHelper.ThrowIfNullOrEmpty(tagName); + if (tagName.Length >= XmlFileConstants.MaxTagNameLength) + throw new ArgumentOutOfRangeException( + $"Tag name '{tagName}' exceeds maximum length of {XmlFileConstants.MaxTagNameLength} characters", nameof(tagName)); + + if (parser == null) + throw new ArgumentNullException(nameof(parser)); + if (setter == null) + throw new ArgumentNullException(nameof(setter)); + + var crc = GetCrc32(tagName); + + _tagMappings[crc] = new MappingEntry(supportedEngines, (target, element, replace) => + { + var value = parser(element); + setter(target, value, replace); + }); + } + + public bool TryParseEntry(XElement element, TObject target, bool replace, GameEngineType engine) + { + var tagName = element.Name.LocalName; + if (tagName.Length >= XmlFileConstants.MaxTagNameLength) + return false; + + var crc = GetCrc32(tagName); + + if (!_tagMappings.TryGetValue(crc, out var mapping)) + return false; + + if (!IsEngineSupported(mapping.SupportedEngines, engine)) + return false; + + mapping.Action(target, element, replace); + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsEngineSupported(SupportedEngines supportedEngines, GameEngineType requestedEngine) + { + // Convert enum value to its corresponding flag by shifting bit 1 left + // Eaw (0) -> 1 << 0 = 1, Foc (1) -> 1 << 1 = 2 + var engineFlag = (SupportedEngines)(1 << (int)requestedEngine); + // Use bitwise AND to check if the flag is set in supportedEngines + // Returns true if the bit is present, false otherwise + return (supportedEngines & engineFlag) != 0; + } + + private Crc32 GetCrc32(string tagName) + { + return _crcService.GetCrc32Upper(tagName, XmlFileConstants.XmlEncoding); + } +} \ 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..a92efb1 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 @@ -1,6 +1,6 @@  - netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1;net10.0 PG.StarWarsGame.Files.ALO PG.StarWarsGame.Files.ALO AlamoEngineTools.PG.StarWarsGame.Files.ALO 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..000b3a6 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 @@ -1,6 +1,6 @@  - netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1;net10.0 PG.StarWarsGame.Files.ChunkFiles PG.StarWarsGame.Files.ChunkFiles AlamoEngineTools.PG.StarWarsGame.Files.ChunkFiles @@ -17,6 +17,6 @@ preview - + \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/NamedXmlObject.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/NamedXmlObject.cs similarity index 61% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/NamedXmlObject.cs rename to src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/NamedXmlObject.cs index 7cf9c4e..2222633 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/NamedXmlObject.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/NamedXmlObject.cs @@ -1,11 +1,9 @@ using System; -using PG.Commons.Data; using PG.Commons.Hashing; -using PG.StarWarsGame.Files.XML; -namespace PG.StarWarsGame.Engine.Xml; +namespace PG.StarWarsGame.Files.XML.Data; -public abstract class NamedXmlObject(string name, Crc32 nameCrc, XmlLocationInfo location) : XmlObject(location), IHasCrc32 +public abstract class NamedXmlObject(string name, Crc32 nameCrc, XmlLocationInfo location) : XmlObject(location) { public Crc32 Crc32 { get; } = nameCrc; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/XmlFileList.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/XmlFileList.cs new file mode 100644 index 0000000..aef7f43 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/XmlFileList.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace PG.StarWarsGame.Files.XML.Data; + +public class XmlFileList(IReadOnlyList files, XmlLocationInfo location) : XmlObject(location) +{ + public static XmlFileList Empty(XmlLocationInfo location) + { + return new XmlFileList([], location); + } + + public IReadOnlyList Files { get; } = files; +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/XmlFileListContainer.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/XmlFileListContainer.cs deleted file mode 100644 index 31a16cc..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/XmlFileListContainer.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Collections.Generic; - -namespace PG.StarWarsGame.Files.XML.Data; - -public class XmlFileListContainer(IList files) -{ - public IList Files { get; } = files; -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/XmlObject.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/XmlObject.cs new file mode 100644 index 0000000..41cde2c --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/XmlObject.cs @@ -0,0 +1,6 @@ +namespace PG.StarWarsGame.Files.XML.Data; + +public abstract class XmlObject(XmlLocationInfo location) +{ + public XmlLocationInfo Location { get; } = location; +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IXmlParserErrorProvider.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IXmlParserErrorProvider.cs deleted file mode 100644 index 2470187..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IXmlParserErrorProvider.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace PG.StarWarsGame.Files.XML.ErrorHandling; - -public interface IXmlParserErrorProvider -{ - event XmlErrorEventHandler XmlParseError; -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IXmlParserErrorReporter.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IXmlParserErrorReporter.cs index 6bb14c1..bddb691 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IXmlParserErrorReporter.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IXmlParserErrorReporter.cs @@ -1,8 +1,6 @@ -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Files.XML.ErrorHandling; +namespace PG.StarWarsGame.Files.XML.ErrorHandling; public interface IXmlParserErrorReporter { - void Report(IPetroglyphXmlParser parser, XmlParseErrorEventArgs error); + void Report(XmlError error); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/PrimitiveXmlErrorReporter.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/PrimitiveXmlErrorReporter.cs index 39de0e6..4d4bce2 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/PrimitiveXmlErrorReporter.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/PrimitiveXmlErrorReporter.cs @@ -1,13 +1,14 @@ using System; -using PG.StarWarsGame.Files.XML.Parsers; namespace PG.StarWarsGame.Files.XML.ErrorHandling; -internal sealed class PrimitiveXmlErrorReporter : IXmlParserErrorReporter, IXmlParserErrorProvider +internal sealed class PrimitiveXmlErrorReporter : IXmlParserErrorReporter { - public event XmlErrorEventHandler? XmlParseError; + internal delegate void XmlErrorEventHandler(XmlError error); - private static readonly Lazy LazyInstance = new(() => new PrimitiveXmlErrorReporter()); + public event XmlErrorEventHandler? PrimitiveParseError; + + private static readonly Lazy LazyInstance = new(() => new PrimitiveXmlErrorReporter(), true); public static PrimitiveXmlErrorReporter Instance => LazyInstance.Value; @@ -15,8 +16,8 @@ private PrimitiveXmlErrorReporter() { } - public void Report(IPetroglyphXmlParser parser, XmlParseErrorEventArgs error) + public void Report(XmlError error) { - XmlParseError?.Invoke(parser, error); + PrimitiveParseError?.Invoke(error); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlError.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlError.cs new file mode 100644 index 0000000..bcdac81 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlError.cs @@ -0,0 +1,21 @@ +using System; +using System.Xml.Linq; +using PG.StarWarsGame.Files.XML.Parsers; + +namespace PG.StarWarsGame.Files.XML.ErrorHandling; + +public sealed class XmlError( + IPetroglyphXmlParserInfo parser, + XElement? element = null, + XmlLocationInfo? locationInfo = null) +{ + public XmlLocationInfo FileLocation { get; } = locationInfo ?? (element is not null ? XmlLocationInfo.FromElement(element) : default); + + public IPetroglyphXmlParserInfo Parser { get; } = parser ?? throw new ArgumentNullException(nameof(parser)); + + public XElement? Element { get; } = element; + + public required XmlParseErrorKind ErrorKind { get; init; } + + public required string Message { get; init; } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlErrorEventHandler.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlErrorEventHandler.cs deleted file mode 100644 index 69962e3..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlErrorEventHandler.cs +++ /dev/null @@ -1,5 +0,0 @@ -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Files.XML.ErrorHandling; - -public delegate void XmlErrorEventHandler(IPetroglyphXmlParser parser, XmlParseErrorEventArgs error); \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlErrorReporter.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlErrorReporter.cs index 8234729..dd8734b 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlErrorReporter.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlErrorReporter.cs @@ -1,29 +1,25 @@ using AnakinRaW.CommonUtilities; -using PG.StarWarsGame.Files.XML.Parsers; namespace PG.StarWarsGame.Files.XML.ErrorHandling; -public class XmlErrorReporter : DisposableObject, IXmlParserErrorReporter, IXmlParserErrorProvider +public class XmlErrorReporter : DisposableObject, IXmlParserErrorReporter { - public event XmlErrorEventHandler? XmlParseError; - public XmlErrorReporter() { - PrimitiveXmlErrorReporter.Instance.XmlParseError += OnPrimitiveError; + PrimitiveXmlErrorReporter.Instance.PrimitiveParseError += OnPrimitiveError; } - public virtual void Report(IPetroglyphXmlParser parser, XmlParseErrorEventArgs error) + public virtual void Report(XmlError error) { - XmlParseError?.Invoke(parser, error); } protected override void DisposeResources() { - PrimitiveXmlErrorReporter.Instance.XmlParseError -= OnPrimitiveError; + PrimitiveXmlErrorReporter.Instance.PrimitiveParseError -= OnPrimitiveError; } - private void OnPrimitiveError(IPetroglyphXmlParser parser, XmlParseErrorEventArgs error) + private void OnPrimitiveError(XmlError error) { - Report(parser, error); + Report(error); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlParseErrorEventArgs.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlParseErrorEventArgs.cs deleted file mode 100644 index afaa2ce..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlParseErrorEventArgs.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Xml.Linq; -using AnakinRaW.CommonUtilities; - -namespace PG.StarWarsGame.Files.XML.ErrorHandling; - -public class XmlParseErrorEventArgs : EventArgs -{ - public XmlLocationInfo Location { get; } - - public XElement? Element { get; } - - public XmlParseErrorKind ErrorKind { get; } - - public string Message { get; } - - public XmlParseErrorEventArgs(XElement element, XmlParseErrorKind errorKind, string message) - { - Element = element ?? throw new ArgumentNullException(nameof(element)); - Location = XmlLocationInfo.FromElement(element); - ErrorKind = errorKind; - Message = message; - } - - public XmlParseErrorEventArgs(XmlLocationInfo location, XmlParseErrorKind errorKind, string message) - { - Location = location; - Message = message; - ErrorKind = errorKind; - } - - public static XmlParseErrorEventArgs FromMissingFile(string file) - { - ThrowHelper.ThrowIfNullOrEmpty(file); - return new XmlParseErrorEventArgs(new XmlLocationInfo(file, null), XmlParseErrorKind.MissingFile, "XML file not found."); - } - - public static XmlParseErrorEventArgs FromEmptyRoot(XElement element) - { - return new XmlParseErrorEventArgs(element, XmlParseErrorKind.EmptyRoot, "XML file has an empty root node."); - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlParseErrorKind.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlParseErrorKind.cs index aa3f8d2..6308a74 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlParseErrorKind.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlParseErrorKind.cs @@ -50,5 +50,13 @@ public enum XmlParseErrorKind /// /// The XML tag name is null or empty. /// - EmptyNodeName + EmptyNodeName = 11, + /// + /// The XML tag has child elements. + /// + TagHasElements = 12, + /// + /// The name of the XML Element (not the value of the attribute "Name") has an unexpected name. + /// + UnexceptedElementName = 13, } \ 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..4a8bced 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 @@ -1,6 +1,6 @@  - netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1;net10.0 PG.StarWarsGame.Files.XML PG.StarWarsGame.Files.XML AlamoEngineTools.PG.StarWarsGame.Files.XML @@ -18,7 +18,7 @@ preview - + diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlElementParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlElementParser.cs deleted file mode 100644 index 14a8aa1..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlElementParser.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Xml.Linq; - -namespace PG.StarWarsGame.Files.XML.Parsers; - -public interface IPetroglyphXmlElementParser : IPetroglyphXmlParser where T : notnull -{ - T Parse(XElement element); -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlFileParser.cs deleted file mode 100644 index 5f9c4b4..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlFileParser.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.IO; - -namespace PG.StarWarsGame.Files.XML.Parsers; - -public interface IPetroglyphXmlFileParser : IPetroglyphXmlParser where T : notnull -{ - T? ParseFile(Stream xmlStream); -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlParserInfo.cs similarity index 64% rename from src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlParser.cs rename to src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlParserInfo.cs index fdf5fd5..6ba0ebf 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlParserInfo.cs @@ -1,6 +1,6 @@ namespace PG.StarWarsGame.Files.XML.Parsers; -public interface IPetroglyphXmlParser +public interface IPetroglyphXmlParserInfo { string Name { get; } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/PetroglyphXmlFileParserBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/PetroglyphXmlFileParserBase.cs index 6ffba48..2ac7f6c 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/PetroglyphXmlFileParserBase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/PetroglyphXmlFileParserBase.cs @@ -1,7 +1,6 @@ using System; using System.IO; using System.IO.Abstractions; -using System.Text; using System.Xml; using System.Xml.Linq; using Microsoft.Extensions.DependencyInjection; @@ -23,20 +22,22 @@ public abstract class PetroglyphXmlFileParserBase(IServiceProvider serviceProvid protected virtual bool LoadLineInfo => true; - protected XElement? GetRootElement(Stream xmlStream, out string fileName) + protected XElement GetRootElement(Stream xmlStream, out string fileName) { fileName = GetStrippedFileName(xmlStream.GetFilePath()); if (string.IsNullOrEmpty(fileName)) throw new InvalidOperationException("Unable to parse XML from unnamed stream. Either parse from a file or MEG stream."); - SkipLeadingWhiteSpace(fileName, xmlStream); + SkipCharactersUntilXmlHeader(fileName, xmlStream); - var xmlReader = XmlReader.Create(xmlStream, new XmlReaderSettings + var asciiStreamReader = new StreamReader(xmlStream, XmlFileConstants.XmlEncoding, false, 1024, leaveOpen: true); + using var xmlReader = XmlReader.Create(asciiStreamReader, new XmlReaderSettings { IgnoreWhitespace = true, IgnoreComments = true, - IgnoreProcessingInstructions = true + IgnoreProcessingInstructions = true, + CloseInput = true }, fileName); var options = LoadOptions.SetBaseUri; @@ -44,7 +45,22 @@ public abstract class PetroglyphXmlFileParserBase(IServiceProvider serviceProvid options |= LoadOptions.SetLineInfo; var doc = XDocument.Load(xmlReader, options); - return doc.Root; + + var root = doc.Root; + + if (root is null) + throw new XmlException("No root node found."); + + if (!root.HasElements) + { + ErrorReporter?.Report(new XmlError(this, root) + { + ErrorKind = XmlParseErrorKind.EmptyRoot, + Message = "XML file has an empty root node.", + }); + } + + return root; } private string GetStrippedFileName(string filePath) @@ -61,23 +77,25 @@ private string GetStrippedFileName(string filePath) } - private void SkipLeadingWhiteSpace(string fileName, Stream stream) + private void SkipCharactersUntilXmlHeader(string fileName, Stream stream) { - using var r = new StreamReader(stream, Encoding.ASCII, false, 10, true); + using var r = new StreamReader(stream, XmlFileConstants.XmlEncoding, false, 10, true); var count = 0; - while (true) - { - var c = (char)r.Read(); - if (!char.IsWhiteSpace(c)) - break; + // It might be possible, that a XML file starts with leading spaces or even a encoding BOM. + // The engine skips everything until the first '<' character, so we have to do the same to avoid parsing errors. + + while ((char)r.Read() != '<') count++; - } if (count != 0) { - OnParseError(new XmlParseErrorEventArgs(new XmlLocationInfo(fileName, 0), - XmlParseErrorKind.DataBeforeHeader, $"XML header is not the first entry of the XML file."));} + ErrorReporter?.Report(new XmlError(this, locationInfo: new XmlLocationInfo(fileName, 0)) + { + ErrorKind = XmlParseErrorKind.DataBeforeHeader, + Message = "XML header is not the first entry of the XML file.", + }); + } stream.Position = count; } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/PetroglyphXmlParserBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/PetroglyphXmlParserBase.cs index 7395eb6..ef2380e 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/PetroglyphXmlParserBase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/PetroglyphXmlParserBase.cs @@ -1,10 +1,12 @@ -using System.Linq; +using System; +using System.Linq; +using System.Runtime.Serialization; using PG.StarWarsGame.Files.XML.ErrorHandling; using System.Xml.Linq; namespace PG.StarWarsGame.Files.XML.Parsers; -public abstract class PetroglyphXmlParserBase : IPetroglyphXmlParser +public abstract class PetroglyphXmlParserBase : IPetroglyphXmlParserInfo { protected readonly IXmlParserErrorReporter? ErrorReporter; @@ -15,6 +17,41 @@ public override string ToString() return Name; } + protected bool IsTagValid(XElement element) + { + if (element.HasElements) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.TagHasElements, + Message = "A tag cannot have elements.", + }); + return false; + } + var tagName = element.Name.LocalName; + if (string.IsNullOrEmpty(tagName)) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.EmptyNodeName, + Message = "A tag name cannot be null or empty.", + }); + return false; + } + + if (tagName.Length > XmlFileConstants.MaxTagNameLength) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.TooLongData, + Message = $"A tag name can be only {XmlFileConstants.MaxTagNameLength} chars long.", + }); + return false; + } + + return true; + } + protected PetroglyphXmlParserBase(IXmlParserErrorReporter? errorReporter) { Name = GetType().FullName!; @@ -33,29 +70,38 @@ protected string GetNameAttributeValue(XElement element) return nameAttribute is null ? string.Empty : nameAttribute.Value; } - protected bool GetNameAttributeValue(XElement element, out string value) + protected bool GetNameAttributeValue(XElement element, out string value, bool uppercase) { return GetAttributeValue(element, "Name", out value!, string.Empty); } - protected bool GetAttributeValue(XElement element, string attribute, out string? value, string? defaultValue = null) + protected bool GetAttributeValue( + XElement element, + string attribute, + out string? value, + string defaultValue = "", + bool uppercase = false) { + // In this engine, this is actually case-sensitive var nameAttribute = element.Attributes() .FirstOrDefault(a => a.Name.LocalName == attribute); - + if (nameAttribute is null) { value = defaultValue; - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.MissingAttribute, $"Missing attribute '{attribute}'")); + if (uppercase) + value = value.ToUpperInvariant(); + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.MissingAttribute, + Message = $"Missing attribute '{attribute}'", + }); return false; } value = nameAttribute.Value; + if (uppercase) + value = value.ToUpperInvariant(); return true; } - - protected virtual void OnParseError(XmlParseErrorEventArgs error) - { - ErrorReporter?.Report(this, error); - } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/INamedXmlObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/INamedXmlObjectParser.cs new file mode 100644 index 0000000..ec7f855 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/INamedXmlObjectParser.cs @@ -0,0 +1,11 @@ +using System.Xml.Linq; +using AnakinRaW.CommonUtilities.Collections; +using PG.Commons.Hashing; +using PG.StarWarsGame.Files.XML.Data; + +namespace PG.StarWarsGame.Files.XML.Parsers; + +public interface INamedXmlObjectParser : IPetroglyphXmlParserInfo where T : NamedXmlObject +{ + T Parse(XElement element, IReadOnlyFrugalValueListDictionary parsedEntries, out Crc32 nameCrc); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlFileContainerParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IXmlContainerFileParser.cs similarity index 55% rename from src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlFileContainerParser.cs rename to src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IXmlContainerFileParser.cs index cd21e00..f9e8349 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlFileContainerParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IXmlContainerFileParser.cs @@ -1,10 +1,13 @@ using System.IO; using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; +using PG.StarWarsGame.Files.XML.Data; namespace PG.StarWarsGame.Files.XML.Parsers; -public interface IPetroglyphXmlFileContainerParser : IPetroglyphXmlParser where T : notnull +public interface IXmlContainerFileParser : IPetroglyphXmlParserInfo where T : NamedXmlObject { + INamedXmlObjectParser ElementParser { get; } + void ParseFile(Stream xmlStream, IFrugalValueListDictionary parsedEntries); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlElementParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlElementParser.cs deleted file mode 100644 index 5fa2c6d..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlElementParser.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Xml.Linq; -using PG.StarWarsGame.Files.XML.ErrorHandling; - -namespace PG.StarWarsGame.Files.XML.Parsers; - -public abstract class PetroglyphXmlElementParser(IXmlParserErrorReporter? errorReporter = null) - : PetroglyphXmlParserBase(errorReporter), IPetroglyphXmlElementParser where T : notnull -{ - public abstract T Parse(XElement element); -} \ 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 deleted file mode 100644 index d2b862a..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileContainerParser.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.IO; -using System.Xml.Linq; -using AnakinRaW.CommonUtilities.Collections; -using PG.Commons.Hashing; -using PG.StarWarsGame.Files.XML.ErrorHandling; - -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, IFrugalValueListDictionary parsedEntries) - { - var root = GetRootElement(xmlStream, out var fileName); - if (root is not null) - Parse(root, parsedEntries, fileName); - } - - protected abstract void Parse(XElement element, IFrugalValueListDictionary parsedElements, string fileName); -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs deleted file mode 100644 index 986cfef..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.IO; -using System.Xml.Linq; -using PG.StarWarsGame.Files.XML.ErrorHandling; - -namespace PG.StarWarsGame.Files.XML.Parsers; - -public abstract class PetroglyphXmlFileParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) - : PetroglyphXmlFileParserBase(serviceProvider, errorReporter), IPetroglyphXmlFileParser where T : notnull -{ - public T? ParseFile(Stream xmlStream) - { - var root = GetRootElement(xmlStream, out var fileName); - if (root is null) - { - var location = new XmlLocationInfo(fileName, 0); - OnParseError(new XmlParseErrorEventArgs(location, XmlParseErrorKind.EmptyRoot, - "Unable to get root node from XML file.")); - } - return root is null ? default : Parse(root, fileName); - } - - protected abstract T Parse(XElement element, string fileName); -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/CommaSeparatedStringKeyValueListParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/CommaSeparatedStringKeyValueListParser.cs index 1d2a5b3..8316fd5 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/CommaSeparatedStringKeyValueListParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/CommaSeparatedStringKeyValueListParser.cs @@ -1,12 +1,14 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Xml.Linq; +using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Files.XML.Parsers; // Used e.g, by // Format: Key, Value, Key, Value // There might be arbitrary spaces, tabs and newlines -// TODO: This class is not yet implemented, compliant to the engine + public sealed class CommaSeparatedStringKeyValueListParser : PetroglyphPrimitiveXmlParser> { public static readonly CommaSeparatedStringKeyValueListParser Instance = new(); @@ -17,13 +19,30 @@ private CommaSeparatedStringKeyValueListParser() private protected override IList<(string key, string value)> DefaultValue => []; - protected internal override IList<(string key, string value)> ParseCore(string trimmedValue, XElement element) + internal override int EngineDataTypeId => 0x34; + + protected internal override IList<(string key, string value)> ParseCore(ReadOnlySpan trimmedValue, XElement element) { - var values = element.Value.Split(','); + var valueText = element.PGValue; + + if (string.IsNullOrEmpty(valueText)) + return DefaultValue; + + if (valueText.Length >= 0x10000) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.TooLongData, + Message = "Input string exceeds maximum size." + }); + return DefaultValue; + } + + var values = valueText.Split(','); // Cases: Empty tag or invalid value (e.g, terrain only, wrong separator, etc.) - if (values.Length <= 1) - return new List<(string key, string value)>(0); + if (values.Length < 2) + return DefaultValue; var keyValueList = new List<(string key, string value)>(values.Length + 1 / 2); @@ -31,7 +50,14 @@ private CommaSeparatedStringKeyValueListParser() { // Case: Incomplete key-value pair if (values.Length - 1 < i + 1) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.MalformedValue, + Message = "Unexpected end of string. Missing string for conversion/string pair!" + }); break; + } var key = values[i].Trim(); var value = values[i + 1].Trim(); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphNumberParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphNumberParser.cs new file mode 100644 index 0000000..e93b0a6 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphNumberParser.cs @@ -0,0 +1,88 @@ +using System; +using System.Xml.Linq; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.Utilities; +#if NET7_0_OR_GREATER +using System.Numerics; +#endif + +namespace PG.StarWarsGame.Files.XML.Parsers; + +public abstract class PetroglyphNumberParser : PetroglyphPrimitiveXmlParser where T : struct, IEquatable, IComparable +#if NET10_0_OR_GREATER + , INumber, IMinMaxValue +#endif +{ + +#if NET7_0_OR_GREATER + protected virtual T MaxValue => T.MaxValue; + + protected virtual T MinValue => T.MinValue; +#else + protected abstract T MaxValue { get; } + + protected abstract T MinValue { get; } + +#endif + + public T ParseAtLeast(XElement element, T minValue) + { + if (minValue.CompareTo(MinValue) < 0 || minValue.CompareTo(MaxValue) > 0) + throw new ArgumentOutOfRangeException(nameof(minValue), "minValue is out of range."); + + var value = Parse(element); + var corrected = PGMath.Max(value, minValue); + if (!corrected.Equals(value)) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"Expected value to be at least {minValue} but got value '{value}'.", + }); + } + + return corrected; + } + + public T ParseAtMost(XElement element, T maxValue) + { + if (maxValue.CompareTo(MinValue) < 0 || maxValue.CompareTo(MaxValue) > 0) + throw new ArgumentOutOfRangeException(nameof(maxValue), "maxValue is out of range."); + + var value = Parse(element); + var corrected = PGMath.Min(value, maxValue); + if (!corrected.Equals(value)) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"Expected value to be at least {maxValue} but got value '{value}'.", + }); + } + + return corrected; + } + + + public T ParseClamped(XElement element, T minValue, T maxValue) + { + if (minValue.CompareTo(MinValue) < 0 || minValue.CompareTo(MaxValue) > 0) + throw new ArgumentOutOfRangeException(nameof(minValue), "minValue is out of range."); + if (maxValue.CompareTo(MinValue) < 0 || maxValue.CompareTo(MaxValue) > 0) + throw new ArgumentOutOfRangeException(nameof(maxValue), "maxValue is out of range."); + if (minValue.CompareTo(maxValue) > 0) + throw new ArgumentException("minValue must be less than or equal to maxValue."); + + var value = Parse(element); + var clamped = PGMath.Clamp(value, minValue, maxValue); + if (!value.Equals(clamped)) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"Expected integer between {minValue} and {maxValue} but got value '{value}'.", + }); + } + return clamped; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphPrimitiveXmlParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphPrimitiveXmlParser.cs index 0fd6fc1..48c53c3 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphPrimitiveXmlParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphPrimitiveXmlParser.cs @@ -1,30 +1,26 @@ -using System.Xml.Linq; -using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using System; +using System.Xml.Linq; namespace PG.StarWarsGame.Files.XML.Parsers; -public abstract class PetroglyphPrimitiveXmlParser : PetroglyphXmlElementParser where T : notnull +public abstract class PetroglyphPrimitiveXmlParser : PetroglyphXmlParserBase where T : notnull { private protected abstract T DefaultValue { get; } + internal abstract int EngineDataTypeId { get; } + private protected PetroglyphPrimitiveXmlParser() : base(PrimitiveXmlErrorReporter.Instance) { } - public sealed override T Parse(XElement element) + public T Parse(XElement element) { - var tagName = element.Name.LocalName; - if (string.IsNullOrEmpty(tagName)) - { - ErrorReporter?.Report(this, new XmlParseErrorEventArgs(element, XmlParseErrorKind.EmptyNodeName, "A tag name cannot be null or empty.")); + if (!IsTagValid(element)) return DefaultValue; - } - if (tagName.Length >= 256) - ErrorReporter?.Report(this, new XmlParseErrorEventArgs(element, XmlParseErrorKind.TooLongData, "A tag name cannot be null or empty.")); - - var value = element.Value.Trim(); + var value = element.PGValue.AsSpan().Trim(); return value.Length == 0 ? DefaultValue : ParseCore(value, element); } - protected internal abstract T ParseCore(string trimmedValue, XElement element); + protected internal abstract T ParseCore(ReadOnlySpan trimmedValue, XElement element); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlBooleanParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlBooleanParser.cs index 8672d1b..c609d0f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlBooleanParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlBooleanParser.cs @@ -1,4 +1,5 @@ -using System.Xml.Linq; +using System; +using System.Xml.Linq; namespace PG.StarWarsGame.Files.XML.Parsers; @@ -6,13 +7,15 @@ public sealed class PetroglyphXmlBooleanParser : PetroglyphPrimitiveXmlParser false; + + internal override int EngineDataTypeId => 0x0 & 0x50; + private PetroglyphXmlBooleanParser() { } - private protected override bool DefaultValue => false; - - protected internal override bool ParseCore(string trimmedValue, XElement element) + protected internal override bool ParseCore(ReadOnlySpan trimmedValue, XElement element) { // Yes! The engine only checks if the values is exact 1 or starts with Tt or Yy // At least it's efficient, I guess... diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlByteParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlByteParser.cs index aa7ba4b..56bcb5c 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlByteParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlByteParser.cs @@ -1,27 +1,39 @@ -using System.Xml.Linq; -using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using System; +using System.Xml.Linq; namespace PG.StarWarsGame.Files.XML.Parsers; -public sealed class PetroglyphXmlByteParser : PetroglyphPrimitiveXmlParser +public sealed class PetroglyphXmlByteParser : PetroglyphNumberParser { public static readonly PetroglyphXmlByteParser Instance = new(); + private protected override byte DefaultValue => 0; + + internal override int EngineDataTypeId => 0x2; + +#if !NET7_0_OR_GREATER + protected override byte MaxValue => byte.MaxValue; + + protected override byte MinValue => byte.MinValue; +#endif + private PetroglyphXmlByteParser() { } - private protected override byte DefaultValue => 0; - - protected internal override byte ParseCore(string trimmedValue, XElement element) + protected internal override byte ParseCore(ReadOnlySpan trimmedValue, XElement element) { var intValue = PetroglyphXmlIntegerParser.Instance.ParseCore(trimmedValue, element); var asByte = (byte)intValue; if (intValue != asByte) { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"Expected a byte value (0 - 255) but got value '{intValue}'.")); + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"Expected a byte value (0 - 255) but got value '{intValue}'.", + }); } return asByte; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlBytePercentParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlBytePercentParser.cs new file mode 100644 index 0000000..e419da4 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlBytePercentParser.cs @@ -0,0 +1,45 @@ +using PG.StarWarsGame.Files.XML.ErrorHandling; +using System; +using System.Xml.Linq; + +namespace PG.StarWarsGame.Files.XML.Parsers; + +public sealed class PetroglyphXmlBytePercentParser : PetroglyphNumberParser +{ + public static readonly PetroglyphXmlBytePercentParser Instance = new(); + + private protected override byte DefaultValue => 0; + + internal override int EngineDataTypeId => 0x4; + + protected override byte MaxValue => 100; + +#if !NET7_0_OR_GREATER + protected override byte MinValue => byte.MinValue; +#endif + + private PetroglyphXmlBytePercentParser() + { + } + + protected internal override byte ParseCore(ReadOnlySpan trimmedValue, XElement element) + { + var intValue = PetroglyphXmlIntegerParser.Instance.ParseCore(trimmedValue, element); + + if (intValue > MaxValue) + intValue = MaxValue; + + var asByte = (byte)intValue; + // Add additional check (> 100), cause the PG implementation is broken, but we need to stay "bug-compatible". + if (intValue != asByte) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"Expected a byte value (0 - 100) but got value '{asByte}'.", + }); + } + + return asByte; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlFloatParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlFloatParser.cs index d3b4e85..c8d0335 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlFloatParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlFloatParser.cs @@ -1,40 +1,42 @@ -using System; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using System; using System.Globalization; using System.Xml.Linq; -using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Files.XML.Parsers; -public sealed class PetroglyphXmlFloatParser : PetroglyphPrimitiveXmlParser +public sealed class PetroglyphXmlFloatParser : PetroglyphNumberParser { public static readonly PetroglyphXmlFloatParser Instance = new(); private protected override float DefaultValue => 0.0f; - private PetroglyphXmlFloatParser() - { - } + internal override int EngineDataTypeId => 0x8; - public float ParseAtLeast(XElement element, float minValue) - { - var value = Parse(element); - var corrected = Math.Max(value, minValue); - if (corrected != value) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"Expected float to be at least {minValue} but got value '{value}'.")); - } +#if !NET7_0_OR_GREATER + protected override float MaxValue => float.MaxValue; - return corrected; - } + protected override float MinValue => float.MinValue; +#endif - protected internal override float ParseCore(string trimmedValue, XElement element) + private PetroglyphXmlFloatParser() + { + } + + protected internal override float ParseCore(ReadOnlySpan trimmedValue, XElement element) { // The engine always loads FP numbers a long double and then converts that result to float - if (!double.TryParse(trimmedValue, NumberStyles.Any, CultureInfo.InvariantCulture, out var doubleValue)) + if (!double.TryParse(trimmedValue +#if NETSTANDARD2_0 + .ToString() +#endif + , NumberStyles.Any, CultureInfo.InvariantCulture, out var doubleValue)) { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.MalformedValue, - $"Expected double but got value '{trimmedValue}'.")); + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.MalformedValue, + Message = $"Expected double but got value '{trimmedValue.ToString()}'.", + }); return 0.0f; } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlIntegerParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlIntegerParser.cs index fd0c0ec..485c89f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlIntegerParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlIntegerParser.cs @@ -1,43 +1,47 @@ -using System.Xml.Linq; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Utilities; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using System; +using System.Xml.Linq; namespace PG.StarWarsGame.Files.XML.Parsers; -public sealed class PetroglyphXmlIntegerParser : PetroglyphPrimitiveXmlParser +public sealed class PetroglyphXmlIntegerParser : PetroglyphNumberParser { public static readonly PetroglyphXmlIntegerParser Instance = new(); private protected override int DefaultValue => 0; + internal override int EngineDataTypeId => 0x6; + +#if !NET7_0_OR_GREATER + protected override int MaxValue => int.MaxValue; + + protected override int MinValue => int.MinValue; +#endif + private PetroglyphXmlIntegerParser() { } - protected internal override int ParseCore(string trimmedValue, XElement element) + protected internal override int ParseCore(ReadOnlySpan trimmedValue, XElement element) { // The engines uses the C++ function std::atoi which is a little more loose. // For example the value '123d' get parsed to 123, // whereas in C# int.TryParse returns (false, 0) - if (!int.TryParse(trimmedValue, out var i)) + if (!int.TryParse(trimmedValue +#if NETSTANDARD2_0 + .ToString() +#endif + + , out var i)) { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.MalformedValue, - $"Expected integer but got '{trimmedValue}'.")); - return 0; + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.MalformedValue, + Message = $"Expected integer but got '{trimmedValue.ToString()}'.", + }); + return DefaultValue; } return i; } - - public int ParseWithRange(XElement element, int minValue, int maxValue) - { - var value = Parse(element); - var clamped = PGMath.Clamp(value, minValue, maxValue); - if (value != clamped) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"Expected integer between {minValue} and {maxValue} but got value '{value}'.")); - } - return clamped; - } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlLooseStringListParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlLooseStringListParser.cs index d06f660..3859a3a 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlLooseStringListParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlLooseStringListParser.cs @@ -20,21 +20,25 @@ public sealed class PetroglyphXmlLooseStringListParser : PetroglyphPrimitiveXmlP public static readonly PetroglyphXmlLooseStringListParser Instance = new(); private protected override IList DefaultValue => []; + internal override int EngineDataTypeId => 0x18 & 0x1B; private PetroglyphXmlLooseStringListParser() { } - protected internal override IList ParseCore(string trimmedValue, XElement element) + protected internal override IList ParseCore(ReadOnlySpan trimmedValue, XElement element) { if (trimmedValue.Length > 0x2000) { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.TooLongData, - $"Input value is too long '{trimmedValue.Length}' at {XmlLocationInfo.FromElement(element)}")); + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.TooLongData, + Message = $"Input value is too long '{trimmedValue.Length}' at {XmlLocationInfo.FromElement(element)}", + }); return DefaultValue; } - var entries = trimmedValue.Split(Separators, StringSplitOptions.RemoveEmptyEntries); + var entries = trimmedValue.ToString().Split(Separators, StringSplitOptions.RemoveEmptyEntries); return entries; } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlMax100ByteParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlMax100ByteParser.cs deleted file mode 100644 index ede64a3..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlMax100ByteParser.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Xml.Linq; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Utilities; - -namespace PG.StarWarsGame.Files.XML.Parsers; - -public sealed class PetroglyphXmlMax100ByteParser : PetroglyphPrimitiveXmlParser -{ - public static readonly PetroglyphXmlMax100ByteParser Instance = new(); - - private protected override byte DefaultValue => 0; - - private PetroglyphXmlMax100ByteParser() - { - } - - protected internal override byte ParseCore(string trimmedValue, XElement element) - { - var intValue = PetroglyphXmlIntegerParser.Instance.ParseCore(trimmedValue, element); - - if (intValue > 100) - intValue = 100; - - var asByte = (byte)intValue; - if (intValue != asByte) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"Expected a byte value (0 - 255) but got value '{intValue}'.")); - } - - // Add additional check, cause the PG implementation is broken, but we need to stay "bug-compatible". - if (asByte > 100) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"Expected a byte value (0 - 100) but got value '{asByte}'.")); - } - - return asByte; - } - - public byte ParseWithRange(XElement element, byte minValue, byte maxValue) - { - if (maxValue > 100) - { - OnParseError(new XmlParseErrorEventArgs( - element, XmlParseErrorKind.InvalidValue, - $"The provided maxValue '{maxValue}' is above 100.")); - } - - // TODO: Do we need to coerce maxValue??? - - var value = Parse(element); - - var clamped = PGMath.Clamp(value, minValue, maxValue); - if (value != clamped) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"Expected byte between {minValue} and {maxValue} but got value '{value}'.")); - } - return clamped; - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlRgbaColorParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlRgbaColorParser.cs index 1ecf10a..e040aaf 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlRgbaColorParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlRgbaColorParser.cs @@ -9,12 +9,13 @@ public sealed class PetroglyphXmlRgbaColorParser : PetroglyphPrimitiveXmlParser< public static readonly PetroglyphXmlRgbaColorParser Instance = new(); private protected override Vector4Int DefaultValue => default; + internal override int EngineDataTypeId => 0x16; private PetroglyphXmlRgbaColorParser() { } - protected internal override Vector4Int ParseCore(string trimmedValue, XElement element) + protected internal override Vector4Int ParseCore(ReadOnlySpan trimmedValue, XElement element) { var values = PetroglyphXmlLooseStringListParser.Instance.ParseCore(trimmedValue, element); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlSByteParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlSByteParser.cs new file mode 100644 index 0000000..234b6ae --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlSByteParser.cs @@ -0,0 +1,41 @@ +using System; +using System.Xml.Linq; +using PG.StarWarsGame.Files.XML.ErrorHandling; + +namespace PG.StarWarsGame.Files.XML.Parsers; + +public sealed class PetroglyphXmlSByteParser : PetroglyphNumberParser +{ + public static readonly PetroglyphXmlSByteParser Instance = new(); + + private protected override sbyte DefaultValue => 0; + + internal override int EngineDataTypeId => 0x3; + +#if !NET7_0_OR_GREATER + protected override sbyte MaxValue => sbyte.MaxValue; + + protected override sbyte MinValue => sbyte.MinValue; +#endif + + private PetroglyphXmlSByteParser() + { + } + + protected internal override sbyte ParseCore(ReadOnlySpan trimmedValue, XElement element) + { + var intValue = PetroglyphXmlIntegerParser.Instance.ParseCore(trimmedValue, element); + + var asSByte = (sbyte)intValue; + if (intValue != asSByte) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"Expected a byte value (0 - 255) but got value '{intValue}'.", + }); + } + + return asSByte; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlStringParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlStringParser.cs index dcca294..010a0f8 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlStringParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlStringParser.cs @@ -1,4 +1,5 @@ -using System.Xml.Linq; +using System; +using System.Xml.Linq; namespace PG.StarWarsGame.Files.XML.Parsers; @@ -6,14 +7,16 @@ public sealed class PetroglyphXmlStringParser : PetroglyphPrimitiveXmlParser string.Empty; + + internal override int EngineDataTypeId => 0x17 & 0x1D & 0x1F; + private PetroglyphXmlStringParser() { } - private protected override string DefaultValue => string.Empty; - - protected internal override string ParseCore(string trimmedValue, XElement element) + protected internal override string ParseCore(ReadOnlySpan trimmedValue, XElement element) { - return trimmedValue; + return trimmedValue.ToString(); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlUnsignedIntegerParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlUnsignedIntegerParser.cs index 7061cca..420f21d 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlUnsignedIntegerParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlUnsignedIntegerParser.cs @@ -1,27 +1,39 @@ -using System.Xml.Linq; -using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using System; +using System.Xml.Linq; namespace PG.StarWarsGame.Files.XML.Parsers; -public sealed class PetroglyphXmlUnsignedIntegerParser : PetroglyphPrimitiveXmlParser +public sealed class PetroglyphXmlUnsignedIntegerParser : PetroglyphNumberParser { public static readonly PetroglyphXmlUnsignedIntegerParser Instance = new(); private protected override uint DefaultValue => 0; + internal override int EngineDataTypeId => 0x5; + +#if !NET7_0_OR_GREATER + protected override uint MaxValue => uint.MaxValue; + + protected override uint MinValue => uint.MinValue; +#endif + private PetroglyphXmlUnsignedIntegerParser() { } - protected internal override uint ParseCore(string trimmedValue, XElement element) + protected internal override uint ParseCore(ReadOnlySpan trimmedValue, XElement element) { var intValue = PetroglyphXmlIntegerParser.Instance.ParseCore(trimmedValue, element); var asUint = (uint)intValue; if (intValue != asUint) { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"Expected unsigned integer but got '{intValue}'.")); + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"Expected unsigned integer but got '{intValue}'.", + }); } return asUint; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlVector2FParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlVector2FParser.cs index 94900b7..488ee0a 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlVector2FParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlVector2FParser.cs @@ -1,4 +1,5 @@ -using System.Numerics; +using System; +using System.Numerics; using System.Xml.Linq; namespace PG.StarWarsGame.Files.XML.Parsers; @@ -11,13 +12,15 @@ public sealed class PetroglyphXmlVector2FParser : PetroglyphPrimitiveXmlParser default; + + internal override int EngineDataTypeId => 0x0F; + private PetroglyphXmlVector2FParser() { } - private protected override Vector2 DefaultValue => default; - - protected internal override Vector2 ParseCore(string trimmedValue, XElement element) + protected internal override Vector2 ParseCore(ReadOnlySpan trimmedValue, XElement element) { var listOfValues = LooseStringListParser.Parse(element); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/XmlContainerFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/XmlContainerFileParser.cs new file mode 100644 index 0000000..c75df69 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/XmlContainerFileParser.cs @@ -0,0 +1,33 @@ +using AnakinRaW.CommonUtilities.Collections; +using PG.Commons.Hashing; +using PG.StarWarsGame.Files.XML.Data; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using System; +using System.IO; +using System.Xml.Linq; + +namespace PG.StarWarsGame.Files.XML.Parsers; + +public sealed class XmlContainerFileParser( + IServiceProvider serviceProvider, + INamedXmlObjectParser elementParser, + IXmlParserErrorReporter? listener = null) + : PetroglyphXmlFileParserBase(serviceProvider, listener), IXmlContainerFileParser + where T : NamedXmlObject +{ + public INamedXmlObjectParser ElementParser { get; } = + elementParser ?? throw new ArgumentNullException(nameof(elementParser)); + + public void ParseFile(Stream xmlStream, IFrugalValueListDictionary parsedEntries) + { + var root = GetRootElement(xmlStream, out _); + foreach (var xElement in root.Elements()) + ParseElement(xElement, parsedEntries); + } + + private void ParseElement(XElement element, IFrugalValueListDictionary parsedEntries) + { + var parsedElement = ElementParser.Parse(element, parsedEntries, out var entryCrc); + parsedEntries.Add(entryCrc, parsedElement); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/XmlFileListParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/XmlFileListParser.cs index 380ea3f..0718ac4 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/XmlFileListParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/XmlFileListParser.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Xml.Linq; using PG.StarWarsGame.Files.XML.Data; using PG.StarWarsGame.Files.XML.ErrorHandling; @@ -7,32 +8,39 @@ namespace PG.StarWarsGame.Files.XML.Parsers; public sealed class XmlFileListParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : - PetroglyphXmlFileParser(serviceProvider, errorReporter) -{ - protected override bool LoadLineInfo => false; - - protected override XmlFileListContainer Parse(XElement element, string fileName) + XmlFileParser(serviceProvider, errorReporter) +{ + protected override XmlFileList ParseRoot(XElement element, string fileName) { var files = new List(); foreach (var child in element.Elements()) { var tagName = GetTagName(child); - if (tagName == "File") + if (tagName != "File") { - var file = PetroglyphXmlStringParser.Instance.Parse(child); - if (file.Length == 0) + ErrorReporter?.Report(new XmlError(this, child) { - ErrorReporter?.Report(this, - new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, "Empty value in tag.")); - } - files.Add(file); + ErrorKind = XmlParseErrorKind.UnexceptedElementName, + Message = $"Tag '<{tagName}>' should not be used. Use '' only.", + }); } - else + + // NB: There intentionally is not else branch here, because that's how the engine behaves. + // It checks whether the tag is called "File" and reports an assert if not. + // However, it still consumes the value and treats it as file. + + var file = PetroglyphXmlStringParser.Instance.Parse(child); + if (file.Length == 0) { - ErrorReporter?.Report(this, new XmlParseErrorEventArgs(child, XmlParseErrorKind.UnknownNode, - $"Tag '<{tagName}>' is not supported. Only '' is supported.")); + ErrorReporter?.Report(new XmlError(this, child) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = "Empty value in tag", + }); } + + files.Add(file); } - return new XmlFileListContainer(files); + return new XmlFileList(new ReadOnlyCollection(files), new XmlLocationInfo(fileName, null)); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/XmlFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/XmlFileParser.cs new file mode 100644 index 0000000..8890a61 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/XmlFileParser.cs @@ -0,0 +1,19 @@ +using PG.StarWarsGame.Files.XML.Data; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using System; +using System.IO; +using System.Xml.Linq; + +namespace PG.StarWarsGame.Files.XML.Parsers; + +public abstract class XmlFileParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) + : PetroglyphXmlFileParserBase(serviceProvider, errorReporter) where T : XmlObject +{ + public T ParseFile(Stream xmlStream) + { + var root = GetRootElement(xmlStream, out var fileName); + return ParseRoot(root, fileName); + } + + protected abstract T ParseRoot(XElement element, string fileName); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Utilities/PGMath.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Utilities/PGMath.cs index bc40317..4290b15 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Utilities/PGMath.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Utilities/PGMath.cs @@ -1,5 +1,8 @@ using System; using System.Runtime.CompilerServices; +#if NET7_0_OR_GREATER +using System.Numerics; +#endif namespace PG.StarWarsGame.Files.XML.Utilities; @@ -8,8 +11,11 @@ internal static class PGMath [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int Clamp(int value, int min, int max) { +#if NETSTANDARD2_1_OR_GREATER || NET + return Math.Clamp(value, min, max); +#endif if (min > max) - throw new ArgumentException("min cannot be larger than max."); + throw new ArgumentException($"'{max}' cannot be greater than {min}."); if (value < min) return min; return value > max ? max : value; @@ -18,10 +24,60 @@ public static int Clamp(int value, int min, int max) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static byte Clamp(byte value, byte min, byte max) { +#if NETSTANDARD2_1_OR_GREATER || NET + return Math.Clamp(value, min, max); +#endif if (min > max) - throw new ArgumentException("min cannot be larger than max."); + throw new ArgumentException($"'{max}' cannot be greater than {min}."); if (value < min) return min; return value > max ? max : value; } + +#if NET7_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Clamp(T value, T min, T max) where T : INumber + { + if (min > max) + throw new ArgumentException($"'{max}' cannot be greater than {min}."); + if (value < min) + return min; + return value > max ? max : value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Max(T val1, T val2) where T : INumber + { + return (val1 >= val2) ? val1 : val2; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Min(T val1, T val2) where T : INumber + { + return (val1 <= val2) ? val1 : val2; + } +# else + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Clamp(T value, T min, T max) where T : struct, IEquatable, IComparable + { + if (min.CompareTo(max) > 0) + throw new ArgumentException($"'{max}' cannot be greater than {min}."); + if (value.CompareTo(min) < 0) + return min; + return value.CompareTo(max) > 0 ? max : value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Max(T val1, T val2) where T : struct, IEquatable, IComparable + { + return val1.CompareTo(val2) >= 0 ? val1 : val2; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Min(T val1, T val2) where T : struct, IEquatable, IComparable + { + return val1.CompareTo(val2) <= 0 ? val1 : val2; + } +#endif } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XElementExtensions.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XElementExtensions.cs new file mode 100644 index 0000000..eb7a6c0 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XElementExtensions.cs @@ -0,0 +1,15 @@ +using System.Xml.Linq; + +namespace PG.StarWarsGame.Files.XML; + +public static class XElementExtensions +{ + extension(XElement element) + { + /// + /// Gets the value of the element as the Petroglyph engine would parse it. + /// That is, if the element has child elements, it returns an empty string, otherwise it returns the value of the element. + /// + public string PGValue => element.HasElements ? string.Empty : element.Value; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XmlFileConstants.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XmlFileConstants.cs new file mode 100644 index 0000000..fb0de39 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XmlFileConstants.cs @@ -0,0 +1,10 @@ +using System.Text; + +namespace PG.StarWarsGame.Files.XML; + +public static class XmlFileConstants +{ + public const int MaxTagNameLength = 255; + + public static readonly Encoding XmlEncoding = Encoding.ASCII; +} \ 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 58914c1..f28c11c 100644 --- a/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj +++ b/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj @@ -13,19 +13,19 @@ - - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 7e40e2fbc714e1f80c1682912809aa69cdba5158 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Wed, 25 Mar 2026 13:17:00 +0100 Subject: [PATCH 12/14] fix merge --- .../Baseline/Json/JsonVerificationBaseline.cs | 4 +- .../Reporting/BaselineVerificationTarget.cs | 23 ---- .../Reporting/InvalidBaselineException.cs | 14 -- .../Reporting/Json/JsonBaselineParser.cs | 36 ----- .../Reporting/Json/JsonBaselineSchema.cs | 129 ------------------ .../Reporting/Json/JsonVerificationTarget.cs | 10 +- 6 files changed, 7 insertions(+), 209 deletions(-) delete mode 100644 src/ModVerify/Reporting/BaselineVerificationTarget.cs delete mode 100644 src/ModVerify/Reporting/InvalidBaselineException.cs delete mode 100644 src/ModVerify/Reporting/Json/JsonBaselineParser.cs delete mode 100644 src/ModVerify/Reporting/Json/JsonBaselineSchema.cs diff --git a/src/ModVerify/Reporting/Baseline/Json/JsonVerificationBaseline.cs b/src/ModVerify/Reporting/Baseline/Json/JsonVerificationBaseline.cs index 3e25aed..13d9b56 100644 --- a/src/ModVerify/Reporting/Baseline/Json/JsonVerificationBaseline.cs +++ b/src/ModVerify/Reporting/Baseline/Json/JsonVerificationBaseline.cs @@ -33,8 +33,8 @@ public JsonVerificationBaseline(VerificationBaseline baseline) [JsonConstructor] private JsonVerificationBaseline( JsonVerificationTarget target, - Version version, - VerificationSeverity minimumSeverity, + Version version, + VerificationSeverity minimumSeverity, IEnumerable errors) { Target = target; diff --git a/src/ModVerify/Reporting/BaselineVerificationTarget.cs b/src/ModVerify/Reporting/BaselineVerificationTarget.cs deleted file mode 100644 index 6e21ddd..0000000 --- a/src/ModVerify/Reporting/BaselineVerificationTarget.cs +++ /dev/null @@ -1,23 +0,0 @@ -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/InvalidBaselineException.cs b/src/ModVerify/Reporting/InvalidBaselineException.cs deleted file mode 100644 index 37ab9c8..0000000 --- a/src/ModVerify/Reporting/InvalidBaselineException.cs +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 669ef53..0000000 --- a/src/ModVerify/Reporting/Json/JsonBaselineParser.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.IO; -using System.Text.Json; - -namespace AET.ModVerify.Reporting.Json; - -internal static class JsonBaselineParser -{ - public static VerificationBaseline Parse(Stream dataStream) - { - if (dataStream == null) - throw new ArgumentNullException(nameof(dataStream)); - try - { - var jsonNode = JsonDocument.Parse(dataStream); - var jsonBaseline = EvaluateAndDeserialize(jsonNode); - - if (jsonBaseline is null) - throw new InvalidBaselineException($"Unable to parse input from stream to {nameof(VerificationBaseline)}. Unknown Error!"); - - return new VerificationBaseline(jsonBaseline); - } - catch (JsonException cause) - { - throw new InvalidBaselineException(cause.Message, cause); - } - } - - private static JsonVerificationBaseline? EvaluateAndDeserialize(JsonDocument? json) - { - if (json is null) - return null; - JsonBaselineSchema.Evaluate(json.RootElement); - return json.Deserialize(); - } -} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonBaselineSchema.cs b/src/ModVerify/Reporting/Json/JsonBaselineSchema.cs deleted file mode 100644 index 12e3705..0000000 --- a/src/ModVerify/Reporting/Json/JsonBaselineSchema.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Text.Json; -using Json.Schema; -using Json.Schema.Keywords; - -namespace AET.ModVerify.Reporting.Json; - -public static class JsonBaselineSchema -{ - private static readonly JsonSchema Schema; - private static readonly EvaluationOptions EvaluationOptions; - private static readonly BuildOptions BuildOptions; - - static JsonBaselineSchema() - { - BuildOptions = new BuildOptions - { - Dialect = Dialect.Draft202012 - }; - - Schema = GetCurrentSchema(); - EvaluationOptions = new EvaluationOptions - { - OutputFormat = OutputFormat.Hierarchical - }; - } - - /// - /// Evaluates a JSON node against the ModVerify Baseline JSON schema. - /// - /// The JSON node to evaluate. - /// is not valid against the baseline JSON schema. - public static void Evaluate(JsonElement json) - { - var result = Schema.Evaluate(json, EvaluationOptions); - ThrowOnValidationError(result); - } - - private static void ThrowOnValidationError(EvaluationResults result) - { - if (!result.IsValid) - { - var error = GetFirstError(result); - var errorMessage = "Baseline JSON not valid"; - - if (error is null) - errorMessage += ": Unknown Error"; - else - errorMessage += $": {error}"; - - throw new InvalidBaselineException(errorMessage); - } - } - - private static KeyValuePair? GetFirstError(EvaluationResults result) - { - if (result.Errors is not null) - return result.Errors.First(); - - if (result.Details is not null) - { - foreach (var child in result.Details) - { - var error = GetFirstError(child); - if (error is not null) - return error; - } - } - return null; - } - - private static JsonSchema GetCurrentSchema() - { - using var resourceStream = typeof(JsonBaselineSchema) - .Assembly.GetManifestResourceStream($"AET.ModVerify.Resources.Schemas.{GetVersionedPath()}.baseline.json"); - - Debug.Assert(resourceStream is not null); - var json = JsonDocument.Parse(resourceStream!).RootElement; - var schema = JsonSchema.Build(json, BuildOptions); - - - if (schema.Root.Keywords.FirstOrDefault(x => x.Handler is IdKeyword)?.Value is not Uri id - || !UriContainsVersion(id, VerificationBaseline.LatestVersionString)) - throw new InvalidOperationException("Internal error: The embedded schema version does not match the expected baseline version!"); - - return schema; - } - - private static bool UriContainsVersion(Uri id, string latestVersionString) - { - foreach (var segment in id.Segments) - { - var trimmed = segment.AsSpan().TrimEnd('/'); - if (trimmed.Equals(latestVersionString, StringComparison.OrdinalIgnoreCase)) - return true; - } - return false; - } - - private static string GetVersionedPath() - { - var version = VerificationBaseline.LatestVersion; - var sb = new StringBuilder(); - - AddVersionSegment(version.Major, ref sb); - AddVersionSegment(version.Minor, ref sb); - AddVersionSegment(version.Build, ref sb); - AddVersionSegment(version.Revision, ref sb); - - // Remove the trailing dot - sb.Length -= 1; - - return sb.ToString(); - - static void AddVersionSegment(int segment, ref StringBuilder sb) - { - if (segment >= 0) - { - sb.Append('_'); - sb.Append(segment); - sb.Append("."); - } - } - } -} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonVerificationTarget.cs b/src/ModVerify/Reporting/Json/JsonVerificationTarget.cs index fe7e33d..ccde4b7 100644 --- a/src/ModVerify/Reporting/Json/JsonVerificationTarget.cs +++ b/src/ModVerify/Reporting/Json/JsonVerificationTarget.cs @@ -6,7 +6,7 @@ namespace AET.ModVerify.Reporting.Json; internal class JsonVerificationTarget { - [JsonPropertyName("name")] + [JsonPropertyName("name")] public string Name { get; } [JsonPropertyName("engine")] @@ -18,7 +18,7 @@ internal class JsonVerificationTarget [JsonPropertyName("version")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Version{ get; } + public string? Version { get; } [JsonPropertyName("location")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -26,9 +26,9 @@ internal class JsonVerificationTarget [JsonConstructor] private JsonVerificationTarget( - string name, - string? version, - JsonGameLocation? location, + string name, + string? version, + JsonGameLocation? location, GameEngineType engine, bool isGame) { From b73e5cd06b7abb15f6418c05810effe0d1d8ab10 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Wed, 25 Mar 2026 13:18:21 +0100 Subject: [PATCH 13/14] remove old baseline --- .../Resources/Schemas/2.1/baseline.json | 124 ------------------ 1 file changed, 124 deletions(-) delete mode 100644 src/ModVerify/Resources/Schemas/2.1/baseline.json diff --git a/src/ModVerify/Resources/Schemas/2.1/baseline.json b/src/ModVerify/Resources/Schemas/2.1/baseline.json deleted file mode 100644 index da37c4d..0000000 --- a/src/ModVerify/Resources/Schemas/2.1/baseline.json +++ /dev/null @@ -1,124 +0,0 @@ -{ - "$id": "https://AlamoEngine-Tools.github.io/schemas/mod-verify/2.1/baseline", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "description": "Represents a baseline for AET ModVerify", - "type": "object", - "$defs": { - "location": { - "type": "object", - "properties": { - "modPaths": { - "type": "array", - "items": { - "type": "string" - } - }, - "gamePath": { - "type": "string" - }, - "fallbackPaths": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "modPaths", - "gamePath", - "fallbackPaths" - ], - "additionalProperties": false - }, - "target": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "version": { - "type": "string" - }, - "engine": { - "enum": [ "Eaw", "Foc" ] - }, - "location": { - "$ref": "#/$defs/location" - }, - "isGame": { - "type": "boolean" - } - }, - "required": [ - "name", - "engine" - ], - "additionalProperties": false - }, - "severity": { - "enum": [ "Information", "Warning", "Error", "Critical" ] - }, - "error": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "message": { - "type": "string" - }, - "asset": { - "type": "string" - }, - "severity": { - "$ref": "#/$defs/severity" - }, - "verifiers": { - "type": "array", - "items": { - "type": "string" - } - }, - "context": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "id", - "message", - "asset", - "severity", - "verifiers", - "context" - ], - "additionalProperties": false - } - }, - "properties": { - "version": { - "const": "2.1" - }, - "minSeverity": { - "$ref": "#/$defs/severity" - }, - "errors": { - "type": "array", - "items": { - "$ref": "#/$defs/error" - }, - "additionalItems": false - }, - "target": { - "$ref": "#/$defs/target" - } - }, - "required": [ - "version", - "minSeverity", - "errors" - ], - "additionalProperties": false -} \ No newline at end of file From 95b30acec1054de599eda118a71b6fd1b04a336e Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Wed, 25 Mar 2026 13:19:50 +0100 Subject: [PATCH 14/14] update subs --- modules/ModdingToolBase | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ModdingToolBase b/modules/ModdingToolBase index 5103bad..da072f4 160000 --- a/modules/ModdingToolBase +++ b/modules/ModdingToolBase @@ -1 +1 @@ -Subproject commit 5103bad6f09ba88061ccbc36ee285ee9300744cc +Subproject commit da072f43e6b85aab35b43d11f6b36eab61bdcfa6