From 6aa6fca09942267dabec3b635afe7ebd7c084233 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Mon, 3 Feb 2025 13:38:52 +0100 Subject: [PATCH] Check icons (#13) * update branch in sub * add mdt to solution * update branch in sub * add mdt to solution * prototype texture verifier * improve error reporting * finish new game init reporting * start GameManagers * update subs * update sub * move to gamemanager arch * first support for gui dialogs * remove valuelistdict to xml objects to avoid boxing * fix gameobject land terrain model mapping uses only the first ocurrance * update deps and module * minor changes * fix initialization * correct file not found handling * use spans for repositories * support custom xml parser action * reduce allocations in repository * update valuestringbuilder usages * fix some files are not found. * extract constants * implement parsing commandbar components (except colors) * move code * move code * move code * remove submodule * remove module * update all deps and make run again * read corrupt string throws BinaryCorruptedExcpetion * update to .net9 --- .github/workflows/release.yml | 8 +- .github/workflows/test.yml | 2 +- .gitmodules | 3 - Directory.Build.props | 2 +- ModVerify.sln | 21 -- PetroglyphTools | 1 - .../GameFinder/GameFinderService.cs | 28 +- .../ModSelectors/AutomaticModSelector.cs | 26 +- .../ModSelectors/ConsoleModSelector.cs | 4 +- .../ModSelectors/ManualModSelector.cs | 6 +- .../ModSelectors/ModSelectorBase.cs | 5 +- .../ModSelectors/SettingsBasedModSelector.cs | 30 +- src/ModVerify.CliApp/ModVerify.CliApp.csproj | 26 +- src/ModVerify.CliApp/Program.cs | 30 +- .../Properties/launchSettings.json | 2 +- src/ModVerify.CliApp/SettingsBuilder.cs | 1 + src/ModVerify/ModVerify.csproj | 9 +- src/ModVerify/ModVerify.csproj.DotSettings | 2 + .../ConcurrentGameDatabaseErrorListener.cs | 26 ++ .../Reporting/IDatabaseErrorCollection.cs | 10 + .../Reporting/Reporters/ConsoleReporter.cs | 5 +- .../Reporting/Reporters/FileBasedReporter.cs | 1 + .../Reporters/JSON/JsonReporterSettings.cs | 4 +- .../Reporting/Reporters/ReporterBase.cs | 1 + .../Text/TextFileReporterSettings.cs | 4 +- .../VerificationReportersExtensions.cs | 1 + .../FileBasedReporterSettings.cs | 2 +- .../GlobalVerificationReportSettings.cs | 4 +- .../VerificationReportSettings.cs | 2 +- .../Reporting/VerificationReportBroker.cs | 1 + src/ModVerify/Settings/GameVerifySettings.cs | 2 +- src/ModVerify/Utilities/PathExtensions.cs | 22 ++ src/ModVerify/VerificationProvider.cs | 1 + src/ModVerify/Verifiers/AudioFilesVerifier.cs | 132 ++++--- ...ameDatabaseInitializationErrorCollector.cs | 29 ++ .../InitializationErrorReporter.cs | 16 + .../InitializationErrorReporterBase.cs | 48 +++ .../XmlParseErrorReporter.cs} | 55 +-- .../Verifiers/DuplicateNameFinder.cs | 14 +- src/ModVerify/Verifiers/GameVerifierBase.cs | 18 +- .../Verifiers/ReferencedModelsVerifier.cs | 40 +- .../ReferencedTexturesVerifier.GUI.cs | 143 ++++++++ .../Verifiers/ReferencedTexturesVerifier.cs | 32 ++ src/ModVerify/Verifiers/VerifierErrorCodes.cs | 2 + src/ModVerify/VerifyGamePipeline.cs | 40 +- .../Audio/Sfx/ISfxEventGameManager.cs | 5 + .../Audio/Sfx/SfxEvent.cs | 239 ++++++++++++ .../Audio/Sfx/SfxEventGameManager.cs | 79 ++++ .../CommandBar/CommandBarComponentType.cs | 15 + .../CommandBar/CommandBarGameManager.cs | 84 +++++ .../CommandBar/Xml/CommandBarComponentData.cs | 142 +++++++ .../DataTypes/GameConstants.cs | 6 - .../DataTypes/GameObject.cs | 65 ---- .../DataTypes/SfxEvent.cs | 260 ------------- .../DataTypes/XmlObject.cs | 47 --- .../ErrorReporting/DatabaseErrorListener.cs | 12 + .../DatabaseErrorListenerWrapper.cs | 61 +++ .../ErrorReporting/IDatabaseErrorListener.cs | 7 + .../ErrorReporting/InitializationError.cs | 8 + .../Database/ErrorReporting/XmlError.cs | 18 + .../Database/GameDatabase.cs | 22 +- .../Database/GameDatabaseService.cs | 47 +-- .../Database/GameInitializationOptions.cs | 14 + .../Database/GameInitializer.cs | 109 ++++++ .../Database/GameManagerBase.cs | 94 +++++ .../Database/IGameDatabase.cs | 17 +- .../Database/IGameDatabaseService.cs | 11 +- .../{IXmlDatabase.cs => IGameManager.cs} | 3 +- .../Initialization/CreateDatabaseStep.cs | 25 -- .../GameDatabaseCreationPipeline.cs | 147 -------- .../ParseXmlDatabaseFromContainerStep.cs | 69 ---- .../Initialization/ParseXmlDatabaseStep.cs | 42 --- .../Database/XmlDatabase.cs | 27 -- .../FocHardcodedConstants.cs | 2 +- .../GameConstants/GameConstants.cs | 17 + .../GameConstants/GameConstantsXml.cs | 3 + .../GameConstants/IGameConstants.cs | 3 + .../GameObjects/GameObject.cs | 84 +++++ .../GameObjectType.cs | 2 +- .../GameObjectTypeTypeGameManager.cs | 42 +++ .../GameObjects/IGameObjectTypeGameManager.cs | 5 + .../GuiDialog/ComponentTextureEntry.cs | 10 + .../GuiDialog/GuiComponentType.cs | 93 +++++ .../GuiDialog/GuiDialogGameManager.cs | 169 +++++++++ .../GuiDialogGameManager_Initialization.cs | 190 ++++++++++ .../GuiDialog/GuiTextureOrigin.cs | 7 + .../GuiDialog/IGuiDialogManager.cs | 26 ++ .../TypeBasedComponentTextureEntryComparer.cs | 22 ++ .../GuiDialog/Xml/GuiDialogsXml.cs | 9 + .../GuiDialog/Xml/GuiDialogsXmlTextureData.cs | 15 + .../GuiDialog/Xml/XmlComponentTextureData.cs | 14 + .../IO/Repositories/EffectsRepository.cs | 103 ++++++ .../IO/Repositories/FileFoundInfo.cs | 27 ++ .../Repositories/FocGameRepository.cs | 55 +-- .../IO/Repositories/GameRepository.Files.cs | 216 +++++++++++ .../{ => IO}/Repositories/GameRepository.cs | 170 +-------- .../Repositories/GameRepositoryFactory.cs | 9 +- .../{ => IO}/Repositories/IGameRepository.cs | 7 +- .../IO/Repositories/IGameRepositoryFactory.cs | 8 + .../IO/Repositories/IRepository.cs | 17 + .../IO/Repositories/MultiPassRepository.cs | 64 ++++ .../IO/Repositories/TextureRepository.cs | 69 ++++ .../Utilities/DirectoryInfoGlobbingWrapper.cs | 4 +- .../Utilities/FileInfoGlobbingWrapper.cs | 2 +- .../{ => IO}/Utilities/MatcherExtensions.cs | 4 +- .../IO/Utilities/PathExtensions.cs | 30 ++ .../EawGameLanguageManager.cs | 2 +- .../FocGameLanguageManager.cs | 2 +- .../GameLanguageManager.cs | 46 +-- .../GameLanguageManagerProvider.cs | 2 +- .../IGameLanguageManager.cs | 2 +- .../IGameLanguageManagerProvider.cs | 2 +- .../LanguageType.cs} | 2 +- .../PG.StarWarsGame.Engine.csproj | 16 +- .../PG.StarWarsGame.Engine.csproj.DotSettings | 7 + .../PG.StarWarsGame.Engine/PGConstants.cs | 13 +- .../PetroglyphEngineServiceContribution.cs | 11 +- .../Rendering/RgbaColor.cs | 63 ++++ .../Repositories/EffectsRepository.cs | 42 --- .../Repositories/IGameRepositoryFactory.cs | 9 - .../Repositories/IRepository.cs | 12 - .../Repositories/MultiPassRepository.cs | 46 --- .../Repositories/TextureRepository.cs | 64 ---- .../Utilities/ValueStringBuilder.cs | 347 +++++++++++++++++- .../Xml/IPetroglyphXmlFileParserFactory.cs | 2 +- .../Xml/NamedXmlObject.cs | 13 + .../Parsers/Data/CommandBarComponentParser.cs | 342 +++++++++++++++++ .../Xml/Parsers/Data/GameConstantsParser.cs | 12 +- .../Xml/Parsers/Data/GameObjectParser.cs | 105 ++++-- .../Xml/Parsers/Data/SfxEventParser.cs | 218 +++++++---- .../File/CommandBarComponentFileParser.cs | 38 ++ .../Parsers/File/GameObjectFileFileParser.cs | 8 +- .../Xml/Parsers/File/GuiDialogParser.cs | 65 ++++ .../Xml/Parsers/File/SfxEventFileParser.cs | 19 +- .../Xml/Parsers/IXmlContainerContentParser.cs | 20 + .../Xml/Parsers/XmlContainerContentParser.cs | 109 ++++++ .../XmlContainerParserErrorEventArgs.cs | 40 ++ .../Xml/Parsers/XmlObjectParser.cs | 82 +++-- .../Xml/PetroglyphXmlParserFactory.cs | 16 +- .../Xml/Tags/CommandBarComponentTags.cs | 108 ++++++ .../Xml/Tags/ComponentTextureKeyExtensions.cs | 114 ++++++ .../PG.StarWarsGame.Engine/Xml/XmlObject.cs | 12 + .../Identifier/AloContentInfoIdentifier.cs | 2 +- .../Binary/Reader/ModelFileReader.cs | 2 +- .../Binary/Reader/ParticleReaderV1.cs | 2 +- .../Files/AloFileInformation.cs | 1 - .../Files/IAloFile.cs | 3 +- .../Files/Models/AloModelFile.cs | 1 - .../Files/Particles/AloParticleFile.cs | 1 - .../PG.StarWarsGame.Files.ALO.csproj | 3 + .../Services/AloFileService.cs | 22 +- .../Services/IAloFileService.cs | 1 - .../Binary/Reader/ChunkFileReaderBase.cs | 4 +- .../Binary/Reader/ChunkReader.cs | 51 +-- .../Files/IChunkFile.cs | 3 +- .../PG.StarWarsGame.Files.ChunkFiles.csproj | 5 +- .../ErrorHandling/XmlParseErrorEventArgs.cs | 23 +- .../ErrorHandling/XmlParseErrorKind.cs | 8 + .../PG.StarWarsGame.Files.XML.csproj | 7 +- .../Parsers/IPetroglyphXmlFileParser.cs | 3 +- .../Parsers/PetroglyphXmlFileParser.cs | 37 +- .../Parsers/PetroglyphXmlParser.cs | 29 +- .../Primitives/IPrimitiveParserProvider.cs | 2 + .../Primitives/PetroglyphXmlByteParser.cs | 5 +- .../Primitives/PetroglyphXmlFloatParser.cs | 28 +- .../Primitives/PetroglyphXmlIntegerParser.cs | 44 ++- .../PetroglyphXmlLooseStringListParser.cs | 4 +- .../PetroglyphXmlMax100ByteParser.cs | 28 +- .../PetroglyphXmlUnsignedIntegerParser.cs | 6 +- .../Primitives/PetroglyphXmlVector2FParser.cs | 34 ++ .../Primitives/PrimitiveParserProvider.cs | 5 + .../Primitives/XmlFileContainerParser.cs | 5 +- .../ValueListDictionary.cs | 312 ---------------- 173 files changed, 4697 insertions(+), 2029 deletions(-) delete mode 100644 .gitmodules delete mode 160000 PetroglyphTools create mode 100644 src/ModVerify/ModVerify.csproj.DotSettings create mode 100644 src/ModVerify/Reporting/ConcurrentGameDatabaseErrorListener.cs create mode 100644 src/ModVerify/Reporting/IDatabaseErrorCollection.cs rename src/ModVerify/Reporting/{ => Settings}/FileBasedReporterSettings.cs (88%) rename src/ModVerify/Reporting/{ => Settings}/GlobalVerificationReportSettings.cs (82%) rename src/ModVerify/Reporting/{ => Settings}/VerificationReportSettings.cs (76%) create mode 100644 src/ModVerify/Utilities/PathExtensions.cs create mode 100644 src/ModVerify/Verifiers/DatabaseError/GameDatabaseInitializationErrorCollector.cs create mode 100644 src/ModVerify/Verifiers/DatabaseError/InitializationErrorReporter.cs create mode 100644 src/ModVerify/Verifiers/DatabaseError/InitializationErrorReporterBase.cs rename src/ModVerify/Verifiers/{XmlParseErrorCollector.cs => DatabaseError/XmlParseErrorReporter.cs} (54%) create mode 100644 src/ModVerify/Verifiers/ReferencedTexturesVerifier.GUI.cs create mode 100644 src/ModVerify/Verifiers/ReferencedTexturesVerifier.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/ISfxEventGameManager.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/SfxEvent.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/SfxEventGameManager.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarComponentType.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/Xml/CommandBarComponentData.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/GameConstants.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/GameObject.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/SfxEvent.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/XmlObject.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Database/ErrorReporting/DatabaseErrorListener.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Database/ErrorReporting/DatabaseErrorListenerWrapper.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Database/ErrorReporting/IDatabaseErrorListener.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Database/ErrorReporting/InitializationError.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Database/ErrorReporting/XmlError.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameInitializationOptions.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameInitializer.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameManagerBase.cs rename src/PetroglyphTools/PG.StarWarsGame.Engine/Database/{IXmlDatabase.cs => IGameManager.cs} (75%) delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/CreateDatabaseStep.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/ParseXmlDatabaseFromContainerStep.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/ParseXmlDatabaseStep.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Database/XmlDatabase.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/GameConstants.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/GameConstantsXml.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/IGameConstants.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObject.cs rename src/PetroglyphTools/PG.StarWarsGame.Engine/{DataTypes => GameObjects}/GameObjectType.cs (95%) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectTypeTypeGameManager.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/IGameObjectTypeGameManager.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/ComponentTextureEntry.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiComponentType.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager_Initialization.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiTextureOrigin.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/IGuiDialogManager.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/TypeBasedComponentTextureEntryComparer.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/GuiDialogsXml.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/GuiDialogsXmlTextureData.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/XmlComponentTextureData.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/EffectsRepository.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/FileFoundInfo.cs rename src/PetroglyphTools/PG.StarWarsGame.Engine/{ => IO}/Repositories/FocGameRepository.cs (56%) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs rename src/PetroglyphTools/PG.StarWarsGame.Engine/{ => IO}/Repositories/GameRepository.cs (62%) rename src/PetroglyphTools/PG.StarWarsGame.Engine/{ => IO}/Repositories/GameRepositoryFactory.cs (52%) rename src/PetroglyphTools/PG.StarWarsGame.Engine/{ => IO}/Repositories/IGameRepository.cs (70%) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/IGameRepositoryFactory.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/IRepository.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/MultiPassRepository.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/TextureRepository.cs rename src/PetroglyphTools/PG.StarWarsGame.Engine/{ => IO}/Utilities/DirectoryInfoGlobbingWrapper.cs (95%) rename src/PetroglyphTools/PG.StarWarsGame.Engine/{ => IO}/Utilities/FileInfoGlobbingWrapper.cs (95%) rename src/PetroglyphTools/PG.StarWarsGame.Engine/{ => IO}/Utilities/MatcherExtensions.cs (97%) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Utilities/PathExtensions.cs rename src/PetroglyphTools/PG.StarWarsGame.Engine/{Language => Localization}/EawGameLanguageManager.cs (92%) rename src/PetroglyphTools/PG.StarWarsGame.Engine/{Language => Localization}/FocGameLanguageManager.cs (94%) rename src/PetroglyphTools/PG.StarWarsGame.Engine/{Language => Localization}/GameLanguageManager.cs (85%) rename src/PetroglyphTools/PG.StarWarsGame.Engine/{Language => Localization}/GameLanguageManagerProvider.cs (93%) rename src/PetroglyphTools/PG.StarWarsGame.Engine/{Language => Localization}/IGameLanguageManager.cs (92%) rename src/PetroglyphTools/PG.StarWarsGame.Engine/{Language => Localization}/IGameLanguageManagerProvider.cs (70%) rename src/PetroglyphTools/PG.StarWarsGame.Engine/{Language/SupportedLanguage.cs => Localization/LanguageType.cs} (80%) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj.DotSettings create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/RgbaColor.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/EffectsRepository.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepositoryFactory.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IRepository.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/MultiPassRepository.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/TextureRepository.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/NamedXmlObject.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/CommandBarComponentParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/CommandBarComponentFileParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GuiDialogParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/IXmlContainerContentParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerParserErrorEventArgs.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/CommandBarComponentTags.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/ComponentTextureKeyExtensions.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObject.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlVector2FParser.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/ValueListDictionary.cs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 31c58a8..a0cd9f9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: - name: Create NetFramework Release run: dotnet publish .\src\ModVerify.CliApp\ModVerify.CliApp.csproj --configuration Release -f net48 --output ./releases/net48 - name: Create Net Core Release - run: dotnet publish .\src\ModVerify.CliApp\ModVerify.CliApp.csproj --configuration Release -f net8.0 --output ./releases/net8.0 + run: dotnet publish .\src\ModVerify.CliApp\ModVerify.CliApp.csproj --configuration Release -f net9.0 --output ./releases/net9.0 - name: Upload a Build Artifact uses: actions/upload-artifact@v4 with: @@ -53,8 +53,8 @@ jobs: path: ./releases - name: Create NET Core .zip # Change into the artifacts directory to avoid including the directory itself in the zip archive - working-directory: ./releases/net8.0 - run: zip -r ../ModVerify-Net8.zip . + working-directory: ./releases/net9.0 + run: zip -r ../ModVerify-Net9.zip . - uses: dotnet/nbgv@v0.4.2 id: nbgv - name: Create GitHub release @@ -66,4 +66,4 @@ jobs: generate_release_notes: true files: | ./releases/net48/ModVerify.exe - ./releases/ModVerify-Net8.zip \ No newline at end of file + ./releases/ModVerify-Net9.zip \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 20a36cc..f6c4d43 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,6 +25,6 @@ jobs: submodules: recursive - uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 9.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 deleted file mode 100644 index 25d6b29..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "PetroglyphTools"] - path = PetroglyphTools - url = https://github.com/AlamoEngine-Tools/PetroglyphTools diff --git a/Directory.Build.props b/Directory.Build.props index 1d41c28..26f6094 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ all - 3.6.139 + 3.6.143 \ No newline at end of file diff --git a/ModVerify.sln b/ModVerify.sln index baee725..d09a64e 100644 --- a/ModVerify.sln +++ b/ModVerify.sln @@ -5,12 +5,6 @@ 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}") = "PG.Commons", "PetroglyphTools\PG.Commons\PG.Commons\PG.Commons.csproj", "{1A9E1B15-DD77-47E3-893E-AFADF982CEC6}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PG.StarWarsGame.Files.DAT", "PetroglyphTools\PG.StarWarsGame.Files.DAT\PG.StarWarsGame.Files.DAT\PG.StarWarsGame.Files.DAT.csproj", "{4630F85C-D1C4-4454-9126-BE13F6901E0B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PG.StarWarsGame.Files.MEG", "PetroglyphTools\PG.StarWarsGame.Files.MEG\PG.StarWarsGame.Files.MEG\PG.StarWarsGame.Files.MEG.csproj", "{885291F4-E5E8-45B2-B0B4-40B2910228A3}" -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}" @@ -29,18 +23,6 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {1A9E1B15-DD77-47E3-893E-AFADF982CEC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1A9E1B15-DD77-47E3-893E-AFADF982CEC6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1A9E1B15-DD77-47E3-893E-AFADF982CEC6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1A9E1B15-DD77-47E3-893E-AFADF982CEC6}.Release|Any CPU.Build.0 = Release|Any CPU - {4630F85C-D1C4-4454-9126-BE13F6901E0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4630F85C-D1C4-4454-9126-BE13F6901E0B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4630F85C-D1C4-4454-9126-BE13F6901E0B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4630F85C-D1C4-4454-9126-BE13F6901E0B}.Release|Any CPU.Build.0 = Release|Any CPU - {885291F4-E5E8-45B2-B0B4-40B2910228A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {885291F4-E5E8-45B2-B0B4-40B2910228A3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {885291F4-E5E8-45B2-B0B4-40B2910228A3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {885291F4-E5E8-45B2-B0B4-40B2910228A3}.Release|Any CPU.Build.0 = Release|Any CPU {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 @@ -70,9 +52,6 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {1A9E1B15-DD77-47E3-893E-AFADF982CEC6} = {15F8B753-814A-406E-9147-EB048DADAC96} - {4630F85C-D1C4-4454-9126-BE13F6901E0B} = {15F8B753-814A-406E-9147-EB048DADAC96} - {885291F4-E5E8-45B2-B0B4-40B2910228A3} = {15F8B753-814A-406E-9147-EB048DADAC96} {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} diff --git a/PetroglyphTools b/PetroglyphTools deleted file mode 160000 index 0347671..0000000 --- a/PetroglyphTools +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0347671ec7c89d2a79b167e2110df3cd495f71aa diff --git a/src/ModVerify.CliApp/GameFinder/GameFinderService.cs b/src/ModVerify.CliApp/GameFinder/GameFinderService.cs index fa21e3b..2e5f5f4 100644 --- a/src/ModVerify.CliApp/GameFinder/GameFinderService.cs +++ b/src/ModVerify.CliApp/GameFinder/GameFinderService.cs @@ -8,7 +8,6 @@ using PG.StarWarsGame.Infrastructure.Games; using PG.StarWarsGame.Infrastructure.Mods; using PG.StarWarsGame.Infrastructure.Services; -using PG.StarWarsGame.Infrastructure.Services.Dependencies; using PG.StarWarsGame.Infrastructure.Services.Detection; namespace AET.ModVerifyTool.GameFinder; @@ -69,17 +68,20 @@ public GameFinderResult FindGamesFromPathOrGlobal(string path) private bool TryDetectGame(GameType gameType, IList detectors, out GameDetectionResult result) { var gd = new CompositeGameDetector(detectors, _serviceProvider); - result = gd.Detect(new GameDetectorOptions(gameType)); - if (result.Error is not null) + try { - _logger?.LogTrace($"Unable to find game installation: {result.Error.Message}", result.Error); - return false; + result = gd.Detect(gameType); + if (result.GameLocation is null) + return false; + return true; } - if (result.GameLocation is null) + catch (Exception e) + { + result = GameDetectionResult.NotInstalled(gameType); + _logger?.LogTrace($"Unable to find game installation: {e.Message}"); return false; - - return true; + } } private GameFinderResult FindGames(IList detectors) @@ -128,15 +130,15 @@ private GameFinderResult FindGames(IList detectors) private void SetupMods(IGame game) { - var modFinder = _serviceProvider.GetRequiredService(); + var modFinder = _serviceProvider.GetRequiredService(); var modRefs = modFinder.FindMods(game); var mods = new List(); foreach (var modReference in modRefs) { - var mod = _modFactory.FromReference(game, modReference, CultureInfo.InvariantCulture); - mods.AddRange(mod); + var mod = _modFactory.CreatePhysicalMod(game, modReference, CultureInfo.InvariantCulture); + mods.Add(mod); } foreach (var mod in mods) @@ -145,9 +147,7 @@ private void SetupMods(IGame game) // Mods need to be added to the game first, before resolving their dependencies. foreach (var mod in mods) { - var resolver = _serviceProvider.GetRequiredService(); - mod.ResolveDependencies(resolver, - new DependencyResolverOptions { CheckForCycle = true, ResolveCompleteChain = true }); + mod.ResolveDependencies(); } } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectors/AutomaticModSelector.cs b/src/ModVerify.CliApp/ModSelectors/AutomaticModSelector.cs index 1d2a85f..00fc4ae 100644 --- a/src/ModVerify.CliApp/ModSelectors/AutomaticModSelector.cs +++ b/src/ModVerify.CliApp/ModSelectors/AutomaticModSelector.cs @@ -1,11 +1,9 @@ using System; -using System.Diagnostics; using System.Globalization; using System.IO.Abstractions; +using System.Linq; using AET.ModVerifyTool.GameFinder; using AET.ModVerifyTool.Options; -using EawModinfo.Model; -using EawModinfo.Spec; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using PG.StarWarsGame.Engine; @@ -13,7 +11,7 @@ using PG.StarWarsGame.Infrastructure.Games; using PG.StarWarsGame.Infrastructure.Mods; using PG.StarWarsGame.Infrastructure.Services; -using PG.StarWarsGame.Infrastructure.Services.Dependencies; +using PG.StarWarsGame.Infrastructure.Services.Detection; namespace AET.ModVerifyTool.ModSelectors; @@ -21,10 +19,14 @@ internal class AutomaticModSelector(IServiceProvider serviceProvider) : ModSelec { private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); - public override GameLocations? Select(GameInstallationsSettings settings, out IPhysicalPlayableObject? targetObject, out GameEngineType? actualEngineType) + public override GameLocations? Select( + GameInstallationsSettings settings, + out IPhysicalPlayableObject? targetObject, + out GameEngineType? actualEngineType) { var pathToVerify = settings.AutoPath; - Debug.Assert(pathToVerify is not null); + if (pathToVerify is null) + throw new InvalidOperationException("path to verify cannot be null."); actualEngineType = settings.EngineType; @@ -100,14 +102,18 @@ private GameLocations GetDetachedModLocations(string modPath, GameFinderResult g 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.FromReference(game, new ModReference(modPath, ModType.Default), true, CultureInfo.InvariantCulture); + mod = modFactory.CreatePhysicalMod(game, modRef, CultureInfo.InvariantCulture); game.AddMod(mod); - var resolver = ServiceProvider.GetRequiredService(); - mod.ResolveDependencies(resolver, - new DependencyResolverOptions { CheckForCycle = true, ResolveCompleteChain = true }); + mod.ResolveDependencies(); return GetLocations(mod, gameResult, settings.AdditionalFallbackPaths); } diff --git a/src/ModVerify.CliApp/ModSelectors/ConsoleModSelector.cs b/src/ModVerify.CliApp/ModSelectors/ConsoleModSelector.cs index c218305..953b0fe 100644 --- a/src/ModVerify.CliApp/ModSelectors/ConsoleModSelector.cs +++ b/src/ModVerify.CliApp/ModSelectors/ConsoleModSelector.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; +using AET.Modinfo.Spec; using AET.ModVerifyTool.GameFinder; using AET.ModVerifyTool.Options; -using EawModinfo.Spec; using PG.StarWarsGame.Engine; using PG.StarWarsGame.Infrastructure; using PG.StarWarsGame.Infrastructure.Mods; @@ -11,7 +11,7 @@ namespace AET.ModVerifyTool.ModSelectors; internal class ConsoleModSelector(IServiceProvider serviceProvider) : ModSelectorBase(serviceProvider) { - public override GameLocations? Select(GameInstallationsSettings settings, out IPhysicalPlayableObject? targetObject, + public override GameLocations Select(GameInstallationsSettings settings, out IPhysicalPlayableObject targetObject, out GameEngineType? actualEngineType) { var gameResult = GameFinderService.FindGames(); diff --git a/src/ModVerify.CliApp/ModSelectors/ManualModSelector.cs b/src/ModVerify.CliApp/ModSelectors/ManualModSelector.cs index 08f6537..d5913c6 100644 --- a/src/ModVerify.CliApp/ModSelectors/ManualModSelector.cs +++ b/src/ModVerify.CliApp/ModSelectors/ManualModSelector.cs @@ -7,7 +7,9 @@ namespace AET.ModVerifyTool.ModSelectors; internal class ManualModSelector(IServiceProvider serviceProvider) : ModSelectorBase(serviceProvider) { - public override GameLocations Select(GameInstallationsSettings settings, out IPhysicalPlayableObject? targetObject, + public override GameLocations Select( + GameInstallationsSettings settings, + out IPhysicalPlayableObject? targetObject, out GameEngineType? actualEngineType) { actualEngineType = settings.EngineType; @@ -21,7 +23,7 @@ public override GameLocations Select(GameInstallationsSettings settings, out IPh return new GameLocations( settings.ModPaths, - settings.GamePath, + 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 index 861451a..0eec285 100644 --- a/src/ModVerify.CliApp/ModSelectors/ModSelectorBase.cs +++ b/src/ModVerify.CliApp/ModSelectors/ModSelectorBase.cs @@ -25,7 +25,9 @@ protected ModSelectorBase(IServiceProvider serviceProvider) GameFinderService = new GameFinderService(serviceProvider); } - public abstract GameLocations? Select(GameInstallationsSettings settings, out IPhysicalPlayableObject? targetObject, + public abstract GameLocations? Select( + GameInstallationsSettings settings, + out IPhysicalPlayableObject? targetObject, out GameEngineType? actualEngineType); protected GameLocations GetLocations(IPhysicalPlayableObject playableObject, GameFinderResult finderResult, IList additionalFallbackPaths) @@ -66,7 +68,6 @@ private IList GetModPaths(IPhysicalPlayableObject modOrGame) var traverser = ServiceProvider.GetRequiredService(); return traverser.Traverse(mod) - .Select(x => x.Mod) .OfType().Select(x => x.Directory.FullName) .ToList(); } diff --git a/src/ModVerify.CliApp/ModSelectors/SettingsBasedModSelector.cs b/src/ModVerify.CliApp/ModSelectors/SettingsBasedModSelector.cs index 356e6c2..c620aae 100644 --- a/src/ModVerify.CliApp/ModSelectors/SettingsBasedModSelector.cs +++ b/src/ModVerify.CliApp/ModSelectors/SettingsBasedModSelector.cs @@ -1,21 +1,13 @@ using System; -using System.Globalization; -using System.IO.Abstractions; using System.Linq; using AET.ModVerifyTool.Options; -using EawModinfo.Model; -using EawModinfo.Spec; -using Microsoft.Extensions.DependencyInjection; using PG.StarWarsGame.Engine; using PG.StarWarsGame.Infrastructure; -using PG.StarWarsGame.Infrastructure.Games; -using PG.StarWarsGame.Infrastructure.Services.Name; namespace AET.ModVerifyTool.ModSelectors; internal class SettingsBasedModSelector(IServiceProvider serviceProvider) { - private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); public VerifyGameInstallationData CreateInstallationDataFromSettings(GameInstallationsSettings settings) { var gameLocations = new ModSelectorFactory(serviceProvider).CreateSelector(settings) @@ -35,30 +27,12 @@ public VerifyGameInstallationData CreateInstallationDataFromSettings(GameInstall }; } - private string GetNameFromGameLocations(IPlayableObject? targetObject, GameLocations gameLocations, GameEngineType engineType) + private static string GetNameFromGameLocations(IPlayableObject? targetObject, GameLocations gameLocations, GameEngineType engineType) { if (targetObject is not null) return targetObject.Name; var mod = gameLocations.ModPaths.FirstOrDefault(); - - var name = mod is not null ? GetNameFromMod(mod) : GetNameFromGame(engineType); - - if (string.IsNullOrEmpty(name)) - throw new InvalidOperationException("Mod or game name cannot be null or empty."); - - return name; - } - - private string? GetNameFromGame(GameEngineType type) - { - var nameResolver = serviceProvider.GetRequiredService(); - return nameResolver.ResolveName(new GameIdentity(type.FromEngineType(), GamePlatform.Undefined), CultureInfo.InvariantCulture); - } - - private string? GetNameFromMod(string mod) - { - var nameResolver = serviceProvider.GetRequiredService(); - return nameResolver.ResolveName(new ModReference(_fileSystem.Path.GetFullPath(mod), ModType.Default), CultureInfo.InvariantCulture); + return mod ?? gameLocations.GamePath; } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModVerify.CliApp.csproj b/src/ModVerify.CliApp/ModVerify.CliApp.csproj index 1fbe04f..82ddda9 100644 --- a/src/ModVerify.CliApp/ModVerify.CliApp.csproj +++ b/src/ModVerify.CliApp/ModVerify.CliApp.csproj @@ -2,7 +2,7 @@ false - net8.0;net48 + net9.0;net48 Exe AET.ModVerifyTool ModVerify @@ -20,23 +20,24 @@ + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -50,7 +51,6 @@ - @@ -58,6 +58,10 @@ + + + + diff --git a/src/ModVerify.CliApp/Program.cs b/src/ModVerify.CliApp/Program.cs index ea5fff9..a8da9eb 100644 --- a/src/ModVerify.CliApp/Program.cs +++ b/src/ModVerify.CliApp/Program.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.IO.Abstractions; -using System.Runtime.CompilerServices; using System.Threading.Tasks; using AET.ModVerify; using AET.ModVerify.Reporting.Reporters; @@ -16,19 +14,19 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Console; -using PG.Commons.Extensibility; +using PG.Commons; using PG.StarWarsGame.Engine; using PG.StarWarsGame.Files.ALO; -using PG.StarWarsGame.Files.DAT.Services.Builder; -using PG.StarWarsGame.Files.MEG.Data.Archives; +using PG.StarWarsGame.Files.MEG; +using PG.StarWarsGame.Files.MTD; using PG.StarWarsGame.Files.XML; using PG.StarWarsGame.Infrastructure; -using PG.StarWarsGame.Infrastructure.Clients; using PG.StarWarsGame.Infrastructure.Services.Detection; using PG.StarWarsGame.Infrastructure.Services.Name; using Serilog; using Serilog.Events; using Serilog.Filters; +using Testably.Abstractions; namespace AET.ModVerifyTool; @@ -90,7 +88,7 @@ await parseResult.WithNotParsedAsync(e => private static IServiceCollection CreateCoreServices(bool verboseLogging) { - var fileSystem = new FileSystem(); + var fileSystem = new RealFileSystem(); var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(new WindowsRegistry()); @@ -107,13 +105,12 @@ private static IServiceProvider CreateAppServices(IServiceCollection serviceColl serviceCollection.AddSingleton(sp => new HashingService(sp)); SteamAbstractionLayer.InitializeServices(serviceCollection); - PetroglyphGameClients.InitializeServices(serviceCollection); PetroglyphGameInfrastructure.InitializeServices(serviceCollection); - RuntimeHelpers.RunClassConstructor(typeof(IDatBuilder).TypeHandle); - RuntimeHelpers.RunClassConstructor(typeof(IMegArchive).TypeHandle); + serviceCollection.SupportMTD(); + serviceCollection.SupportMEG(); AloServiceContribution.ContributeServices(serviceCollection); - serviceCollection.CollectPgServiceContributions(); + PetroglyphCommons.ContributeServices(serviceCollection); XmlServiceContribution.ContributeServices(serviceCollection); PetroglyphEngineServiceContribution.ContributeServices(serviceCollection); @@ -122,15 +119,8 @@ private static IServiceProvider CreateAppServices(IServiceCollection serviceColl SetupReporting(serviceCollection, settings); - serviceCollection.AddSingleton(sp => new CompositeModNameResolver(sp, s => - new List - { - new OfflineWorkshopNameResolver(s), - new OnlineWorkshopNameResolver(s), - new DirectoryModNameResolver(s) - })); - - serviceCollection.AddSingleton(sp => new OfflineModGameTypeResolver(sp)); + serviceCollection.AddSingleton(sp => new OnlineModNameResolver(sp)); + serviceCollection.AddSingleton(sp => new OnlineModGameTypeResolver(sp)); return serviceCollection.BuildServiceProvider(); } diff --git a/src/ModVerify.CliApp/Properties/launchSettings.json b/src/ModVerify.CliApp/Properties/launchSettings.json index 51b4cdc..d84156a 100644 --- a/src/ModVerify.CliApp/Properties/launchSettings.json +++ b/src/ModVerify.CliApp/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "Interactive": { "commandName": "Project", - "commandLineArgs": "-o verifyResults --minFailSeverity Information --baseline focBaseline.json" + "commandLineArgs": "-o verifyResults --minFailSeverity Information --baseline c:/test/focBaseline.json" }, "FromModPath": { diff --git a/src/ModVerify.CliApp/SettingsBuilder.cs b/src/ModVerify.CliApp/SettingsBuilder.cs index eeba013..27c7857 100644 --- a/src/ModVerify.CliApp/SettingsBuilder.cs +++ b/src/ModVerify.CliApp/SettingsBuilder.cs @@ -3,6 +3,7 @@ using System.IO; using System.IO.Abstractions; using AET.ModVerify.Reporting; +using AET.ModVerify.Reporting.Settings; using AET.ModVerify.Settings; using AET.ModVerifyTool.Options; using Microsoft.Extensions.DependencyInjection; diff --git a/src/ModVerify/ModVerify.csproj b/src/ModVerify/ModVerify.csproj index 4a77b42..0649885 100644 --- a/src/ModVerify/ModVerify.csproj +++ b/src/ModVerify/ModVerify.csproj @@ -22,7 +22,8 @@ - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -31,7 +32,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -39,4 +40,8 @@ + + + + diff --git a/src/ModVerify/ModVerify.csproj.DotSettings b/src/ModVerify/ModVerify.csproj.DotSettings new file mode 100644 index 0000000..8af804c --- /dev/null +++ b/src/ModVerify/ModVerify.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/ModVerify/Reporting/ConcurrentGameDatabaseErrorListener.cs b/src/ModVerify/Reporting/ConcurrentGameDatabaseErrorListener.cs new file mode 100644 index 0000000..95461ad --- /dev/null +++ b/src/ModVerify/Reporting/ConcurrentGameDatabaseErrorListener.cs @@ -0,0 +1,26 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using PG.StarWarsGame.Engine.Database.ErrorReporting; + +namespace AET.ModVerify.Reporting; + +internal class ConcurrentGameDatabaseErrorListener : DatabaseErrorListener, IDatabaseErrorCollection +{ + private readonly ConcurrentBag _xmlErrors = new(); + + private readonly ConcurrentBag _initializationErrors = new(); + + public IEnumerable XmlErrors => _xmlErrors.ToList(); + public IEnumerable InitializationErrors => _initializationErrors.ToList(); + + public override void OnXmlError(XmlError error) + { + _xmlErrors.Add(error); + } + + public override void OnInitializationError(InitializationError error) + { + _initializationErrors.Add(error); + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/IDatabaseErrorCollection.cs b/src/ModVerify/Reporting/IDatabaseErrorCollection.cs new file mode 100644 index 0000000..90c8203 --- /dev/null +++ b/src/ModVerify/Reporting/IDatabaseErrorCollection.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using PG.StarWarsGame.Engine.Database.ErrorReporting; + +namespace AET.ModVerify.Reporting; + +internal interface IDatabaseErrorCollection +{ + IEnumerable XmlErrors { get; } + IEnumerable InitializationErrors { get; } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/ConsoleReporter.cs b/src/ModVerify/Reporting/Reporters/ConsoleReporter.cs index bda277f..2e1cb6a 100644 --- a/src/ModVerify/Reporting/Reporters/ConsoleReporter.cs +++ b/src/ModVerify/Reporting/Reporters/ConsoleReporter.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using AET.ModVerify.Reporting.Settings; namespace AET.ModVerify.Reporting.Reporters; @@ -9,7 +10,7 @@ internal class ConsoleReporter(VerificationReportSettings settings, IServiceProv { public override Task ReportAsync(IReadOnlyCollection errors) { - var filteredErrors = FilteredErrors(errors).ToList(); + var filteredErrors = FilteredErrors(errors).OrderByDescending(x => x.Severity).ToList(); Console.WriteLine(); Console.WriteLine("GAME VERIFICATION RESULT"); @@ -20,7 +21,7 @@ public override Task ReportAsync(IReadOnlyCollection errors) Console.WriteLine("No errors!"); foreach (var error in filteredErrors) - Console.WriteLine(error); + Console.WriteLine($"[{error.Severity}] [{error.Id}] Message={error.Message}"); Console.WriteLine(); diff --git a/src/ModVerify/Reporting/Reporters/FileBasedReporter.cs b/src/ModVerify/Reporting/Reporters/FileBasedReporter.cs index d0213d0..b66f29a 100644 --- a/src/ModVerify/Reporting/Reporters/FileBasedReporter.cs +++ b/src/ModVerify/Reporting/Reporters/FileBasedReporter.cs @@ -1,6 +1,7 @@ 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/Reporters/JSON/JsonReporterSettings.cs b/src/ModVerify/Reporting/Reporters/JSON/JsonReporterSettings.cs index fd5962d..4207b36 100644 --- a/src/ModVerify/Reporting/Reporters/JSON/JsonReporterSettings.cs +++ b/src/ModVerify/Reporting/Reporters/JSON/JsonReporterSettings.cs @@ -1,3 +1,5 @@ -namespace AET.ModVerify.Reporting.Reporters.JSON; +using AET.ModVerify.Reporting.Settings; + +namespace AET.ModVerify.Reporting.Reporters.JSON; public record JsonReporterSettings : FileBasedReporterSettings; \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/ReporterBase.cs b/src/ModVerify/Reporting/Reporters/ReporterBase.cs index 9360c0e..ff71507 100644 --- a/src/ModVerify/Reporting/Reporters/ReporterBase.cs +++ b/src/ModVerify/Reporting/Reporters/ReporterBase.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using AET.ModVerify.Reporting.Settings; namespace AET.ModVerify.Reporting.Reporters; diff --git a/src/ModVerify/Reporting/Reporters/Text/TextFileReporterSettings.cs b/src/ModVerify/Reporting/Reporters/Text/TextFileReporterSettings.cs index e6847a3..8fb833b 100644 --- a/src/ModVerify/Reporting/Reporters/Text/TextFileReporterSettings.cs +++ b/src/ModVerify/Reporting/Reporters/Text/TextFileReporterSettings.cs @@ -1,4 +1,6 @@ -namespace AET.ModVerify.Reporting.Reporters.Text; +using AET.ModVerify.Reporting.Settings; + +namespace AET.ModVerify.Reporting.Reporters.Text; public record TextFileReporterSettings : FileBasedReporterSettings { diff --git a/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs b/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs index 04f7c7d..41c2938 100644 --- a/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs +++ b/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs @@ -1,5 +1,6 @@ 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; diff --git a/src/ModVerify/Reporting/FileBasedReporterSettings.cs b/src/ModVerify/Reporting/Settings/FileBasedReporterSettings.cs similarity index 88% rename from src/ModVerify/Reporting/FileBasedReporterSettings.cs rename to src/ModVerify/Reporting/Settings/FileBasedReporterSettings.cs index 6616258..fef047c 100644 --- a/src/ModVerify/Reporting/FileBasedReporterSettings.cs +++ b/src/ModVerify/Reporting/Settings/FileBasedReporterSettings.cs @@ -1,6 +1,6 @@ using System; -namespace AET.ModVerify.Reporting; +namespace AET.ModVerify.Reporting.Settings; public record FileBasedReporterSettings : VerificationReportSettings { diff --git a/src/ModVerify/Reporting/GlobalVerificationReportSettings.cs b/src/ModVerify/Reporting/Settings/GlobalVerificationReportSettings.cs similarity index 82% rename from src/ModVerify/Reporting/GlobalVerificationReportSettings.cs rename to src/ModVerify/Reporting/Settings/GlobalVerificationReportSettings.cs index 3982d4d..fe19d68 100644 --- a/src/ModVerify/Reporting/GlobalVerificationReportSettings.cs +++ b/src/ModVerify/Reporting/Settings/GlobalVerificationReportSettings.cs @@ -1,6 +1,4 @@ -using System; - -namespace AET.ModVerify.Reporting; +namespace AET.ModVerify.Reporting.Settings; public record GlobalVerificationReportSettings : VerificationReportSettings { diff --git a/src/ModVerify/Reporting/VerificationReportSettings.cs b/src/ModVerify/Reporting/Settings/VerificationReportSettings.cs similarity index 76% rename from src/ModVerify/Reporting/VerificationReportSettings.cs rename to src/ModVerify/Reporting/Settings/VerificationReportSettings.cs index fc02024..6be2905 100644 --- a/src/ModVerify/Reporting/VerificationReportSettings.cs +++ b/src/ModVerify/Reporting/Settings/VerificationReportSettings.cs @@ -1,4 +1,4 @@ -namespace AET.ModVerify.Reporting; +namespace AET.ModVerify.Reporting.Settings; public record VerificationReportSettings { diff --git a/src/ModVerify/Reporting/VerificationReportBroker.cs b/src/ModVerify/Reporting/VerificationReportBroker.cs index f9c3c68..ea4d127 100644 --- a/src/ModVerify/Reporting/VerificationReportBroker.cs +++ b/src/ModVerify/Reporting/VerificationReportBroker.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using AET.ModVerify.Reporting.Settings; using AET.ModVerify.Verifiers; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/src/ModVerify/Settings/GameVerifySettings.cs b/src/ModVerify/Settings/GameVerifySettings.cs index 488bfe4..5e67f46 100644 --- a/src/ModVerify/Settings/GameVerifySettings.cs +++ b/src/ModVerify/Settings/GameVerifySettings.cs @@ -1,4 +1,4 @@ -using AET.ModVerify.Reporting; +using AET.ModVerify.Reporting.Settings; namespace AET.ModVerify.Settings; diff --git a/src/ModVerify/Utilities/PathExtensions.cs b/src/ModVerify/Utilities/PathExtensions.cs new file mode 100644 index 0000000..6ddc47a --- /dev/null +++ b/src/ModVerify/Utilities/PathExtensions.cs @@ -0,0 +1,22 @@ +using System; +using System.IO.Abstractions; +using AnakinRaW.CommonUtilities.FileSystem; + +namespace AET.ModVerify.Utilities; + +public static class PathExtensions +{ + public static ReadOnlySpan GetGameStrippedPath(this IPath path, ReadOnlySpan gamePath, ReadOnlySpan modPath) + { + if (!path.IsPathFullyQualified(modPath)) + return modPath; + + if (modPath.Length <= gamePath.Length) + return modPath; + + if (path.IsChildOf(gamePath, modPath)) + return modPath.Slice(gamePath.Length); + + return modPath; + } +} \ No newline at end of file diff --git a/src/ModVerify/VerificationProvider.cs b/src/ModVerify/VerificationProvider.cs index a1800dc..ca0f42d 100644 --- a/src/ModVerify/VerificationProvider.cs +++ b/src/ModVerify/VerificationProvider.cs @@ -13,5 +13,6 @@ public IEnumerable GetAllDefaultVerifiers(IGameDatabase databa yield return new ReferencedModelsVerifier(database, settings, serviceProvider); yield return new DuplicateNameFinder(database, settings, serviceProvider); yield return new AudioFilesVerifier(database, settings, serviceProvider); + yield return new ReferencedTexturesVerifier(database, settings, serviceProvider); } } \ No newline at end of file diff --git a/src/ModVerify/Verifiers/AudioFilesVerifier.cs b/src/ModVerify/Verifiers/AudioFilesVerifier.cs index 1f4a3e9..e5ba70b 100644 --- a/src/ModVerify/Verifiers/AudioFilesVerifier.cs +++ b/src/ModVerify/Verifiers/AudioFilesVerifier.cs @@ -1,18 +1,22 @@ 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.Database; -using PG.StarWarsGame.Engine.DataTypes; -using PG.StarWarsGame.Engine.Language; +using PG.StarWarsGame.Engine.Localization; using PG.StarWarsGame.Files.MEG.Services.Builder.Normalization; + #if NETSTANDARD2_0 using AnakinRaW.CommonUtilities.FileSystem; #endif @@ -21,14 +25,21 @@ namespace AET.ModVerify.Verifiers; public class AudioFilesVerifier : GameVerifierBase { - private readonly PetroglyphDataEntryPathNormalizer _pathNormalizer; + private static readonly PathNormalizeOptions SampleNormalizerOptions = new() + { + UnifyCase = UnifyCasingKind.UpperCaseForce, + UnifySeparatorKind = DirectorySeparatorKind.Windows, + UnifyDirectorySeparators = true + }; + + private readonly EmpireAtWarMegDataEntryPathNormalizer _pathNormalizer = EmpireAtWarMegDataEntryPathNormalizer.Instance; private readonly ICrc32HashingService _hashingService; private readonly IFileSystem _fileSystem; private readonly IGameLanguageManager _languageManager; - public AudioFilesVerifier(IGameDatabase gameDatabase, GameVerifySettings settings, IServiceProvider serviceProvider) : base(gameDatabase, settings, serviceProvider) + public AudioFilesVerifier(IGameDatabase gameDatabase, GameVerifySettings settings, IServiceProvider serviceProvider) + : base(gameDatabase, settings, serviceProvider) { - _pathNormalizer = new(serviceProvider); _hashingService = serviceProvider.GetRequiredService(); _fileSystem = serviceProvider.GetRequiredService(); _languageManager = serviceProvider.GetRequiredService() @@ -41,67 +52,86 @@ protected override void RunVerification(CancellationToken token) { var visitedSamples = new HashSet(); var languagesToVerify = GetLanguagesToVerify().ToList(); - foreach (var sfxEvent in Database.SfxEvents.Entries) + foreach (var sfxEvent in Database.SfxGameManager.Entries) { foreach (var codedSample in sfxEvent.AllSamples) { - VerifySample(codedSample, sfxEvent, languagesToVerify, visitedSamples); + VerifySample(codedSample.AsSpan(), sfxEvent, languagesToVerify, visitedSamples); } } } - private void VerifySample(string sample, SfxEvent sfxEvent, IEnumerable languagesToVerify, HashSet visitedSamples) + private void VerifySample(ReadOnlySpan sample, SfxEvent sfxEvent, IEnumerable languagesToVerify, HashSet visitedSamples) { - Span sampleNameBuffer = stackalloc char[PGConstants.MaxPathLength]; + char[]? pooledBuffer = null; - var i = _pathNormalizer.Normalize(sample.AsSpan(), sampleNameBuffer); - var normalizedSampleName = sampleNameBuffer.Slice(0, i); - var crc = _hashingService.GetCrc32(normalizedSampleName, PGConstants.PGCrc32Encoding); - if (!visitedSamples.Add(crc)) - return; + var buffer = sample.Length < PGConstants.MaxMegEntryPathLength + ? stackalloc char[PGConstants.MaxMegEntryPathLength] + : pooledBuffer = ArrayPool.Shared.Rent(sample.Length); - - if (normalizedSampleName.Length > PGConstants.MaxPathLength) + try { - AddError(VerificationError.Create( - this, - VerifierErrorCodes.FilePathTooLong, - $"Sample name '{sample}' is too long.", - VerificationSeverity.Error, - sample)); - return; - } + var length = PathNormalizer.Normalize(sample, buffer, SampleNormalizerOptions); + var sampleNameBuffer = buffer.Slice(0, length); - var normalizedSampleNameString = normalizedSampleName.ToString(); + var crc = _hashingService.GetCrc32(sampleNameBuffer, Encoding.ASCII); + if (!visitedSamples.Add(crc)) + return; - if (sfxEvent.IsLocalized) - { - foreach (var language in languagesToVerify) + if (sfxEvent.IsLocalized) + { + foreach (var language in languagesToVerify) + { + VerifySampleLocalized(sfxEvent, sampleNameBuffer, language, out var localized); + if (!localized) + return; + } + } + else { - var localizedSampleName = _languageManager.LocalizeFileName(normalizedSampleNameString, language, out var localized); - VerifySample(localizedSampleName, sfxEvent); - - if (!localized) - return; + VerifySample(sampleNameBuffer, sfxEvent); } } - else + finally { - VerifySample(normalizedSampleNameString, sfxEvent); + if (pooledBuffer is not null) + ArrayPool.Shared.Return(pooledBuffer); } + } - private void VerifySample(string sample, SfxEvent sfxEvent) + 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( this, - VerifierErrorCodes.SampleNotFound, - $"Audio file '{sample}' could not be found.", + VerifierErrorCodes.SampleNotFound, + $"Audio file '{sampleString}' could not be found.", VerificationSeverity.Error, - sample)); + sampleString)); return; } using var binaryReader = new BinaryReader(sampleStream); @@ -121,42 +151,46 @@ private void VerifySample(string sample, SfxEvent sfxEvent) if (format != WaveFormats.PCM) { + var sampleString = sample.ToString(); AddError(VerificationError.Create( this, VerifierErrorCodes.SampleNotPCM, - $"Audio file '{sample}' has an invalid format '{format}'. Supported is {WaveFormats.PCM}", + $"Audio file '{sampleString}' has an invalid format '{format}'. Supported is {WaveFormats.PCM}", VerificationSeverity.Error, - sample)); + sampleString)); } if (channels > 1 && !IsAmbient2D(sfxEvent)) { + var sampleString = sample.ToString(); AddError(VerificationError.Create( this, VerifierErrorCodes.SampleNotMono, - $"Audio file '{sample}' is not mono audio.", - VerificationSeverity.Information, - sample)); + $"Audio file '{sampleString}' is not mono audio.", + VerificationSeverity.Information, + sampleString)); } if (sampleRate > 48_000) { + var sampleString = sample.ToString(); AddError(VerificationError.Create( this, VerifierErrorCodes. InvalidSampleRate, - $"Audio file '{sample}' has a too high sample rate of {sampleRate}. Maximum is 48.000Hz.", + $"Audio file '{sampleString}' has a too high sample rate of {sampleRate}. Maximum is 48.000Hz.", VerificationSeverity.Error, - sample)); + sampleString)); } if (bitPerSecondPerChannel > 16) { + var sampleString = sample.ToString(); AddError(VerificationError.Create( this, VerifierErrorCodes.InvalidBitsPerSeconds, - $"Audio file '{sample}' has an invalid bit size of {bitPerSecondPerChannel}. Supported are 16bit.", - VerificationSeverity.Error, - sample)); + $"Audio file '{sampleString}' has an invalid bit size of {bitPerSecondPerChannel}. Supported are 16bit.", + VerificationSeverity.Error, + sampleString)); } } diff --git a/src/ModVerify/Verifiers/DatabaseError/GameDatabaseInitializationErrorCollector.cs b/src/ModVerify/Verifiers/DatabaseError/GameDatabaseInitializationErrorCollector.cs new file mode 100644 index 0000000..e8543d6 --- /dev/null +++ b/src/ModVerify/Verifiers/DatabaseError/GameDatabaseInitializationErrorCollector.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using AET.ModVerify.Reporting; +using AET.ModVerify.Settings; +using PG.StarWarsGame.Engine.Database; + +namespace AET.ModVerify.Verifiers; + +internal sealed class GameDatabaseInitializationErrorCollector( + IDatabaseErrorCollection errorCollection, + IGameDatabase gameDatabase, + GameVerifySettings settings, + IServiceProvider serviceProvider) : GameVerifierBase(gameDatabase, settings, serviceProvider) +{ + public override string FriendlyName => "Reporting Game Initialization Errors"; + + protected override void RunVerification(CancellationToken token) + { + AddErrors(new InitializationErrorReporter(Repository, Services).GetErrors(errorCollection.InitializationErrors)); + AddErrors(new XmlParseErrorReporter(Repository, Services).GetErrors(errorCollection.XmlErrors)); + } + + private void AddErrors(IEnumerable errors) + { + foreach (var error in errors) + AddError(error); + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/DatabaseError/InitializationErrorReporter.cs b/src/ModVerify/Verifiers/DatabaseError/InitializationErrorReporter.cs new file mode 100644 index 0000000..e2e2e10 --- /dev/null +++ b/src/ModVerify/Verifiers/DatabaseError/InitializationErrorReporter.cs @@ -0,0 +1,16 @@ +using System; +using AET.ModVerify.Reporting; +using PG.StarWarsGame.Engine.Database.ErrorReporting; +using PG.StarWarsGame.Engine.IO.Repositories; + +namespace AET.ModVerify.Verifiers; + +internal sealed class InitializationErrorReporter(IGameRepository gameRepository, IServiceProvider serviceProvider) : InitializationErrorReporterBase(gameRepository, serviceProvider) +{ + public override string Name => "InitializationErrors"; + + protected override void CreateError(InitializationError error, out ErrorData errorData) + { + errorData = new ErrorData("INIT00", error.Message, [error.GameManager], VerificationSeverity.Critical); + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/DatabaseError/InitializationErrorReporterBase.cs b/src/ModVerify/Verifiers/DatabaseError/InitializationErrorReporterBase.cs new file mode 100644 index 0000000..df3e22e --- /dev/null +++ b/src/ModVerify/Verifiers/DatabaseError/InitializationErrorReporterBase.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using AET.ModVerify.Reporting; +using AnakinRaW.CommonUtilities; +using PG.StarWarsGame.Engine.IO.Repositories; + +namespace AET.ModVerify.Verifiers; + +internal abstract class InitializationErrorReporterBase(IGameRepository gameRepository, IServiceProvider serviceProvider) +{ + 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 IEnumerable GetErrors(IEnumerable errors) + { + foreach (var error in errors) + { + CreateError(error, out var data); + yield return new VerificationError(data.Identifier, data.Message, Name, data.Assets, data.Severity); + } + } + + protected abstract void CreateError(T error, out ErrorData errorData); + + protected readonly ref struct ErrorData + { + public string Identifier { get; } + public string Message { get; } + public IEnumerable Assets { get; } + public VerificationSeverity Severity { get; } + + public ErrorData(string identifier, string message, IEnumerable assets, VerificationSeverity severity) + { + ThrowHelper.ThrowIfNullOrEmpty(identifier); + ThrowHelper.ThrowIfNullOrEmpty(message); + Identifier = identifier; + Message = message; + Assets = assets; + Severity = severity; + } + + public ErrorData(string identifier, string message, VerificationSeverity severity) : this(identifier, message, [], severity) + { + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/XmlParseErrorCollector.cs b/src/ModVerify/Verifiers/DatabaseError/XmlParseErrorReporter.cs similarity index 54% rename from src/ModVerify/Verifiers/XmlParseErrorCollector.cs rename to src/ModVerify/Verifiers/DatabaseError/XmlParseErrorReporter.cs index 0819eb8..9c0bdc9 100644 --- a/src/ModVerify/Verifiers/XmlParseErrorCollector.cs +++ b/src/ModVerify/Verifiers/DatabaseError/XmlParseErrorReporter.cs @@ -1,39 +1,36 @@ using System; using System.Collections.Generic; -using System.Threading; +using System.IO.Abstractions; using AET.ModVerify.Reporting; -using AET.ModVerify.Settings; -using PG.StarWarsGame.Engine.Database; +using AET.ModVerify.Utilities; +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Engine.Database.ErrorReporting; +using PG.StarWarsGame.Engine.IO.Repositories; using PG.StarWarsGame.Files.XML.ErrorHandling; namespace AET.ModVerify.Verifiers; -public sealed class XmlParseErrorCollector( - IEnumerable xmlErrors, - IGameDatabase gameDatabase, - GameVerifySettings settings, - IServiceProvider serviceProvider) : - GameVerifierBase(gameDatabase, settings, serviceProvider) +internal sealed class XmlParseErrorReporter(IGameRepository gameRepository, IServiceProvider serviceProvider) : + InitializationErrorReporterBase(gameRepository, serviceProvider) { - public override string FriendlyName => "XML Parsing Errors"; + private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); - protected override void RunVerification(CancellationToken token) + public override string Name => "XMLError"; + + protected override void CreateError(XmlError error, out ErrorData errorData) { - foreach (var xmlError in xmlErrors) - AddError(ConvertXmlToVerificationError(xmlError)); - } + var id = GetIdFromError(error.ErrorKind); + var severity = GetSeverityFromError(error.ErrorKind); - private VerificationError ConvertXmlToVerificationError(XmlParseErrorEventArgs xmlError) - { - var id = GetIdFromError(xmlError.ErrorKind); - var severity = GetSeverityFromError(xmlError.ErrorKind); + var strippedFileName = _fileSystem.Path + .GetGameStrippedPath(GameRepository.Path.AsSpan(), error.FileLocation.XmlFile.ToUpperInvariant().AsSpan()).ToString(); var assets = new List { - GetGameStrippedPath(xmlError.File.ToUpperInvariant()) + strippedFileName }; - var xmlElement = xmlError.Element; + var xmlElement = error.Element; if (xmlElement is not null) { @@ -49,11 +46,19 @@ private VerificationError ConvertXmlToVerificationError(XmlParseErrorEventArgs x } - return VerificationError.Create(this, id, xmlError.Message, severity, assets); + var errorMessage = CreateErrorMessage(error, strippedFileName); + errorData = new ErrorData(id, errorMessage, assets, severity); + } + + private static string CreateErrorMessage(XmlError error, string strippedFileName) + { + if (error.FileLocation.Line.HasValue) + return $"{error.Message} File='{strippedFileName} #{error.FileLocation.Line.Value}'"; + return $"{error.Message} File='{strippedFileName}'"; } - private VerificationSeverity GetSeverityFromError(XmlParseErrorKind xmlErrorErrorKind) + private static VerificationSeverity GetSeverityFromError(XmlParseErrorKind xmlErrorErrorKind) { return xmlErrorErrorKind switch { @@ -65,11 +70,13 @@ private VerificationSeverity GetSeverityFromError(XmlParseErrorKind xmlErrorErro XmlParseErrorKind.MissingReference => VerificationSeverity.Error, XmlParseErrorKind.TooLongData => VerificationSeverity.Warning, XmlParseErrorKind.DataBeforeHeader => VerificationSeverity.Information, + XmlParseErrorKind.MissingNode => VerificationSeverity.Critical, + XmlParseErrorKind.UnknownNode => VerificationSeverity.Information, _ => VerificationSeverity.Warning }; } - private string GetIdFromError(XmlParseErrorKind xmlErrorErrorKind) + private static string GetIdFromError(XmlParseErrorKind xmlErrorErrorKind) { return xmlErrorErrorKind switch { @@ -82,6 +89,8 @@ private string GetIdFromError(XmlParseErrorKind xmlErrorErrorKind) XmlParseErrorKind.TooLongData => VerifierErrorCodes.XmlValueTooLong, XmlParseErrorKind.Unknown => VerifierErrorCodes.GenericXmlError, XmlParseErrorKind.DataBeforeHeader => VerifierErrorCodes.XmlDataBeforeHeader, + XmlParseErrorKind.MissingNode => VerifierErrorCodes.XmlMissingNode, + XmlParseErrorKind.UnknownNode => VerifierErrorCodes.XmlUnsupportedTag, _ => throw new ArgumentOutOfRangeException(nameof(xmlErrorErrorKind), xmlErrorErrorKind, null) }; } diff --git a/src/ModVerify/Verifiers/DuplicateNameFinder.cs b/src/ModVerify/Verifiers/DuplicateNameFinder.cs index 14d2e67..0a60b50 100644 --- a/src/ModVerify/Verifiers/DuplicateNameFinder.cs +++ b/src/ModVerify/Verifiers/DuplicateNameFinder.cs @@ -5,7 +5,7 @@ using AET.ModVerify.Settings; using AnakinRaW.CommonUtilities.Collections; using PG.StarWarsGame.Engine.Database; -using PG.StarWarsGame.Engine.DataTypes; +using PG.StarWarsGame.Engine.Xml; namespace AET.ModVerify.Verifiers; @@ -19,15 +19,15 @@ public sealed class DuplicateNameFinder( protected override void RunVerification(CancellationToken token) { - CheckDatabaseForDuplicates(Database.GameObjects, "GameObject"); - CheckDatabaseForDuplicates(Database.SfxEvents, "SFXEvent"); + CheckDatabaseForDuplicates(Database.GameObjectTypeManager, "GameObject"); + CheckDatabaseForDuplicates(Database.SfxGameManager, "SFXEvent"); } - private void CheckDatabaseForDuplicates(IXmlDatabase database, string databaseName) where T : XmlObject + private void CheckDatabaseForDuplicates(IGameManager gameManager, string databaseName) where T : NamedXmlObject { - foreach (var key in database.EntryKeys) + foreach (var key in gameManager.EntryKeys) { - var entries = database.GetEntries(key); + var entries = gameManager.GetEntries(key); if (entries.Count > 1) { var entryNames = entries.Select(x => x.Name); @@ -41,7 +41,7 @@ private void CheckDatabaseForDuplicates(IXmlDatabase database, string data } } - private string CreateDuplicateErrorMessage(string databaseName, ReadOnlyFrugalList entries) where T : XmlObject + private string CreateDuplicateErrorMessage(string databaseName, ReadOnlyFrugalList entries) where T : NamedXmlObject { var firstEntry = entries.First(); diff --git a/src/ModVerify/Verifiers/GameVerifierBase.cs b/src/ModVerify/Verifiers/GameVerifierBase.cs index a1f10cd..768236c 100644 --- a/src/ModVerify/Verifiers/GameVerifierBase.cs +++ b/src/ModVerify/Verifiers/GameVerifierBase.cs @@ -1,16 +1,14 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO.Abstractions; using System.Threading; using AET.ModVerify.Reporting; using AET.ModVerify.Settings; -using AnakinRaW.CommonUtilities.FileSystem; using AnakinRaW.CommonUtilities.SimplePipeline.Steps; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using PG.StarWarsGame.Engine.Database; -using PG.StarWarsGame.Engine.Repositories; +using PG.StarWarsGame.Engine.IO.Repositories; namespace AET.ModVerify.Verifiers; @@ -70,18 +68,4 @@ protected void GuardedVerify(Action action, Predicate exceptionFilter exceptionHandler(e); } } - - protected string GetGameStrippedPath(string path) - { - if (!FileSystem.Path.IsPathFullyQualified(path)) - return path; - - if (path.Length <= Repository.Path.Length) - return path; - - if (path.StartsWith(Repository.Path, StringComparison.OrdinalIgnoreCase)) - return path.Substring(Repository.Path.Length); - - return path; - } } \ No newline at end of file diff --git a/src/ModVerify/Verifiers/ReferencedModelsVerifier.cs b/src/ModVerify/Verifiers/ReferencedModelsVerifier.cs index 8a6e7ba..bfb9f7c 100644 --- a/src/ModVerify/Verifiers/ReferencedModelsVerifier.cs +++ b/src/ModVerify/Verifiers/ReferencedModelsVerifier.cs @@ -5,9 +5,8 @@ using System.Threading; using AET.ModVerify.Reporting; using AET.ModVerify.Settings; +using AET.ModVerify.Utilities; using Microsoft.Extensions.DependencyInjection; -using PG.Commons.Binary; -using PG.Commons.Files; using PG.Commons.Utilities; using PG.StarWarsGame.Engine; using PG.StarWarsGame.Engine.Database; @@ -16,7 +15,8 @@ using PG.StarWarsGame.Files.ALO.Services; using PG.StarWarsGame.Files.ChunkFiles.Data; using AnakinRaW.CommonUtilities.FileSystem; -using System.Reflection; +using PG.StarWarsGame.Files; +using PG.StarWarsGame.Files.Binary; namespace AET.ModVerify.Verifiers; @@ -34,7 +34,7 @@ public sealed class ReferencedModelsVerifier( protected override void RunVerification(CancellationToken token) { - var aloQueue = new Queue(Database.GameObjects.Entries + var aloQueue = new Queue(Database.GameObjectTypeManager.Entries .SelectMany(x => x.Models) .Concat(FocHardcodedConstants.HardcodedModels)); @@ -85,7 +85,7 @@ private void VerifyModelOrParticle(Stream modelStream, Queue workingQueu } catch (BinaryCorruptedException e) { - var aloFile = GetGameStrippedPath(modelStream.GetFilePath()); + var aloFile = FileSystem.Path.GetGameStrippedPath(Repository.Path.AsSpan(), modelStream.GetFilePath().AsSpan()).ToString(); var message = $"{aloFile} is corrupted: {e.Message}"; AddError(VerificationError.Create(this, VerifierErrorCodes.ModelBroken, message, VerificationSeverity.Critical, aloFile)); } @@ -99,7 +99,7 @@ private void VerifyParticle(IAloParticleFile file) e => e is ArgumentException, _ => { - var modelFilePath = GetGameStrippedPath(file.FilePath); + var modelFilePath = FileSystem.Path.GetGameStrippedPath(Repository.Path.AsSpan(), file.FilePath.AsSpan()).ToString(); AddError(VerificationError.Create( this, VerifierErrorCodes.InvalidTexture, @@ -116,7 +116,7 @@ private void VerifyParticle(IAloParticleFile file) if (!fileName.Equals(name, StringComparison.OrdinalIgnoreCase)) { - var modelFilePath = GetGameStrippedPath(file.FilePath); + var modelFilePath = FileSystem.Path.GetGameStrippedPath(Repository.Path.AsSpan(), file.FilePath.AsSpan()).ToString(); AddError(VerificationError.Create( this, VerifierErrorCodes.InvalidParticleName, @@ -135,7 +135,8 @@ private void VerifyModel(IAloModelFile file, Queue workingQueue) e => e is ArgumentException, _ => { - var modelFilePath = GetGameStrippedPath(file.FilePath); + var modelFilePath = + FileSystem.Path.GetGameStrippedPath(Repository.Path.AsSpan(), file.FilePath.AsSpan()).ToString(); AddError(VerificationError.Create( this, VerifierErrorCodes.InvalidTexture, @@ -151,13 +152,14 @@ private void VerifyModel(IAloModelFile file, Queue workingQueue) e => e is ArgumentException, _ => { - var modelFilePath = GetGameStrippedPath(file.FilePath); + var shaderPath = + FileSystem.Path.GetGameStrippedPath(Repository.Path.AsSpan(), file.FilePath.AsSpan()).ToString(); AddError(VerificationError.Create( this, VerifierErrorCodes.InvalidShader, - $"Invalid texture file name '{shader}' in model '{modelFilePath}'", + $"Invalid texture file name '{shader}' in model '{shaderPath}'", VerificationSeverity.Error, - shader, modelFilePath)); + shader, shaderPath)); }); } @@ -168,13 +170,14 @@ private void VerifyModel(IAloModelFile file, Queue workingQueue) e => e is ArgumentException, _ => { - var modelFilePath = GetGameStrippedPath(file.FilePath); + var proxyPath = FileSystem.Path + .GetGameStrippedPath(Repository.Path.AsSpan(), file.FilePath.AsSpan()).ToString(); AddError(VerificationError.Create( this, VerifierErrorCodes.InvalidProxy, - $"Invalid proxy file name '{proxy}' in model '{modelFilePath}'", + $"Invalid proxy file name '{proxy}' in model '{proxyPath}'", VerificationSeverity.Error, - proxy, modelFilePath)); + proxy, proxyPath)); }); } } @@ -186,7 +189,8 @@ private void VerifyTextureExists(IPetroglyphFileHolder().ToArray(); + + private void VerifyGuiTextures(ISet visitedTextures) + { + VerifyMegaTexturesExist(); + + var components = new List + { + DefaultComponentIdentifier + }; + components.AddRange(Database.GuiDialogManager.Components); + + foreach (var component in components) + VerifyGuiComponentTexturesExist(component, visitedTextures); + + } + + private void VerifyMegaTexturesExist() + { + var megaTextureName = Database.GuiDialogManager.GuiDialogsXml?.TextureData.MegaTexture; + if (Database.GuiDialogManager.MtdFile is null) + { + var mtdFileName = megaTextureName ?? "<>"; + VerificationError.Create(this, MtdNotFound, $"MtdFile '{mtdFileName}.mtd' could not be found", + VerificationSeverity.Critical, mtdFileName); + } + + + if (megaTextureName is not null) + { + var megaTextureFileName = $"{megaTextureName}.tga"; + + if (!Repository.TextureRepository.FileExists(megaTextureFileName)) + { + VerificationError.Create(this, TexutreNotFound, $"Could not find texture '{megaTextureFileName}' could not be found", + VerificationSeverity.Error, megaTextureFileName); + } + } + + + var compressedMegaTextureName = Database.GuiDialogManager.GuiDialogsXml?.TextureData.CompressedMegaTexture; + if (compressedMegaTextureName is not null) + { + var compressedMegaTextureFieName = $"{compressedMegaTextureName}.dds"; + + if (!Repository.TextureRepository.FileExists(compressedMegaTextureFieName)) + { + VerificationError.Create(this, TexutreNotFound, $"Could not find texture '{compressedMegaTextureFieName}' could not be found", + VerificationSeverity.Error, compressedMegaTextureFieName); + } + } + } + + private void VerifyGuiComponentTexturesExist(string component, ISet visitedTextures) + { + var middleButtonInRepoMode = false; + + + var entriesForComponent = GetTextureEntriesForComponents(component, out var defined); + if (!defined) + return; + + foreach (var componentType in GuiComponentTypes) + { + try + { + if (!entriesForComponent.TryGetValue(componentType, out var texture)) + continue; + + if (!visitedTextures.Add(texture.Texture)) + { + // If we are in a special case we don't want to skip + if (!middleButtonInRepoMode && + componentType is not GuiComponentType.ButtonMiddle && + componentType is not GuiComponentType.Scanlines && + componentType is not GuiComponentType.FrameBackground) + continue; + } + + if (!Database.GuiDialogManager.TextureExists( + texture, + out var origin, + out var isNone, + middleButtonInRepoMode) + && !isNone) + { + + if (origin == GuiTextureOrigin.MegaTexture && texture.Texture.Length > MtdFileConstants.MaxFileNameSize) + { + AddError(VerificationError.Create(this, FileNameTooLong, + $"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(this, TexutreNotFound, + message, VerificationSeverity.Error, + texture.Texture, component, origin.ToString())); + } + } + + if (componentType is GuiComponentType.ButtonMiddle && origin is GuiTextureOrigin.Repository) + middleButtonInRepoMode = true; + } + finally + { + + if (componentType >= GuiComponentType.ButtonRightDisabled) + middleButtonInRepoMode = false; + } + } + } + + private IReadOnlyDictionary GetTextureEntriesForComponents(string component, out bool defined) + { + if (component == DefaultComponentIdentifier) + { + defined = true; + return Database.GuiDialogManager.DefaultTextureEntries; + } + return Database.GuiDialogManager.GetTextureEntries(component, out defined); + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/ReferencedTexturesVerifier.cs b/src/ModVerify/Verifiers/ReferencedTexturesVerifier.cs new file mode 100644 index 0000000..58e600c --- /dev/null +++ b/src/ModVerify/Verifiers/ReferencedTexturesVerifier.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using AET.ModVerify.Settings; +using PG.StarWarsGame.Engine.Database; + +namespace AET.ModVerify.Verifiers; +public sealed partial class ReferencedTexturesVerifier( + IGameDatabase gameDatabase, + GameVerifySettings settings, + IServiceProvider serviceProvider) + : + GameVerifierBase(gameDatabase, settings, serviceProvider) +{ + public const string MtdNotFound = "TEX00"; + public const string TexutreNotFound = "TEX01"; + public const string FileNameTooLong = "PAT00"; + + protected override void RunVerification(CancellationToken token) + { + var textures = new HashSet(StringComparer.OrdinalIgnoreCase); + try + { + VerifyGuiTextures(textures); + } + finally + { + textures.Clear(); + } + + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/VerifierErrorCodes.cs b/src/ModVerify/Verifiers/VerifierErrorCodes.cs index d4b3fa0..6a68e91 100644 --- a/src/ModVerify/Verifiers/VerifierErrorCodes.cs +++ b/src/ModVerify/Verifiers/VerifierErrorCodes.cs @@ -32,4 +32,6 @@ public static class VerifierErrorCodes public const string MissingXmlReference = "XML06"; public const string XmlValueTooLong = "XML07"; public const string XmlDataBeforeHeader = "XML08"; + public const string XmlMissingNode = "XML09"; + public const string XmlUnsupportedTag = "XML10"; } \ No newline at end of file diff --git a/src/ModVerify/VerifyGamePipeline.cs b/src/ModVerify/VerifyGamePipeline.cs index e546447..6eda635 100644 --- a/src/ModVerify/VerifyGamePipeline.cs +++ b/src/ModVerify/VerifyGamePipeline.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -13,8 +12,6 @@ using Microsoft.Extensions.Logging; using PG.StarWarsGame.Engine; using PG.StarWarsGame.Engine.Database; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; namespace AET.ModVerify; @@ -23,14 +20,12 @@ public abstract class VerifyGamePipeline : Pipeline private readonly List _verificationSteps = new(); private readonly GameEngineType _targetType; private readonly GameLocations _gameLocations; - private readonly ParallelRunner _verifyRunner; + private readonly ParallelStepRunner _verifyRunner; protected GameVerifySettings Settings { get; } public IReadOnlyCollection Errors { get; private set; } = Array.Empty(); - - private readonly ConcurrentBag _xmlParseErrors = new(); - + protected VerifyGamePipeline(GameEngineType targetType, GameLocations gameLocations, GameVerifySettings settings, IServiceProvider serviceProvider) : base(serviceProvider) { @@ -41,10 +36,9 @@ protected VerifyGamePipeline(GameEngineType targetType, GameLocations gameLocati if (settings.ParallelVerifiers is < 0 or > 64) throw new ArgumentException("Settings has invalid parallel worker number.", nameof(settings)); - _verifyRunner = new ParallelRunner(settings.ParallelVerifiers, serviceProvider); + _verifyRunner = new ParallelStepRunner(settings.ParallelVerifiers, serviceProvider); } - protected sealed override Task PrepareCoreAsync() { _verificationSteps.Clear(); @@ -58,21 +52,18 @@ protected sealed override async Task RunCoreAsync(CancellationToken token) { var databaseService = ServiceProvider.GetRequiredService(); - IGameDatabase database; - try - { - databaseService.XmlParseError += OnXmlParseError; - database = await databaseService.CreateDatabaseAsync(_targetType, _gameLocations, token); - } - finally + var initializationErrorListener = new ConcurrentGameDatabaseErrorListener(); + var initOptions = new GameInitializationOptions { - databaseService.XmlParseError -= OnXmlParseError; - databaseService.Dispose(); - } + Locations = _gameLocations, + TargetEngineType = _targetType, + ErrorListener = initializationErrorListener + + }; + var database = await databaseService.InitializeGameAsync(initOptions, token); + + AddStep(new GameDatabaseInitializationErrorCollector(initializationErrorListener, database, Settings, ServiceProvider)); - - AddStep(new XmlParseErrorCollector(_xmlParseErrors, database, Settings, ServiceProvider)); - foreach (var gameVerificationStep in CreateVerificationSteps(database)) AddStep(gameVerificationStep); @@ -113,9 +104,4 @@ private void AddStep(GameVerifierBase verifier) _verifyRunner.AddStep(verifier); _verificationSteps.Add(verifier); } - - private void OnXmlParseError(IPetroglyphXmlParser sender, XmlParseErrorEventArgs e) - { - _xmlParseErrors.Add(e); - } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/ISfxEventGameManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/ISfxEventGameManager.cs new file mode 100644 index 0000000..f054f1c --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/ISfxEventGameManager.cs @@ -0,0 +1,5 @@ +using PG.StarWarsGame.Engine.Database; + +namespace PG.StarWarsGame.Engine.Audio.Sfx; + +public interface ISfxEventGameManager : IGameManager; \ 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 new file mode 100644 index 0000000..aed53f2 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/SfxEvent.cs @@ -0,0 +1,239 @@ +using System.Collections.Generic; +using System.Linq; +using PG.Commons.Hashing; +using PG.StarWarsGame.Engine.Xml; +using PG.StarWarsGame.Files.XML; + +namespace PG.StarWarsGame.Engine.Audio.Sfx; + +public sealed class SfxEvent : NamedXmlObject +{ + private byte _minVolume = DefaultMinVolume; + private byte _maxVolume = DefaultMaxVolume; + private byte _minPitch = DefaultMinPitch; + private byte _maxPitch = DefaultMaxPitch; + private byte _minPan2D = DefaultMinPan2d; + private byte _maxPan2D = DefaultMaxPan2d; + private uint _minPredelay; + private uint _maxPredelay; + private uint _minPostdelay; + private uint _maxPostdelay; + + public const byte MaxVolumeValue = 100; + public const byte MaxPitchValue = 200; + public const byte MinPitchValue = 50; + 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; + + // Default values which are not the default value of the type + public const byte DefaultPriority = 3; + public const bool DefaultIs3d = true; + public const byte DefaultProbability = 100; + public const sbyte DefaultPlayCount = 1; + public const sbyte DefaultMaxInstances = 1; + public const byte DefaultMinVolume = 100; + public const byte DefaultMaxVolume = 100; + public const byte DefaultMinPitch = 100; + public const byte DefaultMaxPitch = 100; + public const byte DefaultMinPan2d = 50; + public const byte DefaultMaxPan2d = 50; + public const float DefaultVolumeSaturationDistance = 300.0f; + + public bool IsPreset { get; internal set; } + + public bool Is3D { get; internal set; } = DefaultIs3d; + + public bool Is2D { get; internal set; } + + public bool IsGui { get; internal set; } + + public bool IsHudVo { get; internal set; } + + public bool IsUnitResponseVo { get; internal set; } + + public bool IsAmbientVo { get; internal set; } + + public bool IsLocalized { get; internal set; } + + public SfxEvent? Preset { get; internal set; } + + public string? UsePresetName { get; internal set; } + + public bool PlaySequentially { get; internal set; } + + public IEnumerable AllSamples => PreSamples.Concat(Samples).Concat(PostSamples); + + public IReadOnlyList PreSamples { get; internal set; } = []; + + public IReadOnlyList Samples { get; internal set; } = []; + + public IReadOnlyList PostSamples { get; internal set; } = []; + + public IReadOnlyList LocalizedTextIDs { get; internal set; } = []; + + public byte Priority { get; internal set; } = DefaultPriority; + + public byte Probability { get; internal set; } = DefaultProbability; + + public sbyte PlayCount { get; internal set; } = DefaultPlayCount; + + public float LoopFadeInSeconds { get; internal set; } + + public float LoopFadeOutSeconds { get; internal set; } + + public sbyte MaxInstances { get; internal set; } = DefaultMaxInstances; + + public float VolumeSaturationDistance { get; internal set; } = DefaultVolumeSaturationDistance; + + public bool KillsPreviousObjectsSfx { get; internal set; } + + public string? OverlapTestName { get; internal set; } + + public string? ChainedSfxEventName { get; internal set; } + + public byte MinVolume + { + get => _minVolume; + internal set => _minVolume = value; + } + + public byte MaxVolume + { + get => _maxVolume; + internal set => _maxVolume = value; + } + + public byte MinPitch + { + get => _minPitch; + internal set => _minPitch = value; + } + + public byte MaxPitch + { + get => _maxPitch; + internal set => _maxPitch = value; + } + + public byte MinPan2D + { + get => _minPan2D; + internal set => _minPan2D = value; + } + + public byte MaxPan2D + { + get => _maxPan2D; + internal set => _maxPan2D = value; + } + + public uint MinPredelay + { + get => _minPredelay; + internal set => _minPredelay = value; + } + + public uint MaxPredelay + { + get => _maxPredelay; + internal set => _maxPredelay = value; + } + + public uint MinPostdelay + { + get => _minPostdelay; + internal set => _minPostdelay = value; + } + + public uint MaxPostdelay + { + get => _maxPostdelay; + internal set => _maxPostdelay = value; + } + + + internal SfxEvent(string name, Crc32 nameCrc, XmlLocationInfo location) + : base(name, nameCrc, location) + { + } + + internal override void CoerceValues() + { + AdjustMinMaxValues(ref _minVolume, ref _maxVolume); + AdjustMinMaxValues(ref _minPitch, ref _maxPitch); + AdjustMinMaxValues(ref _minPan2D, ref _maxPan2D); + AdjustMinMaxValues(ref _minPredelay, ref _maxPredelay); + AdjustMinMaxValues(ref _minPostdelay, ref _maxPostdelay); + } + + /* + * The engine also copies the of the preset (which is usually null). + * As this would cause this SFXEvent loose the information of the coded preset, we do not copy the preset's preset value. + * Example: + * + * + * Preset Yes + * 90 + * + * + * Engine Behavior: SFXEvent instance(Name: A, Use_Preset: null, Min_Volume: 90) + * PG.StarWarsGame.Engine Behavior: SFXEvent instance(Name: A, Use_Preset: Preset, Min_Volume: 90) + */ + public void ApplyPreset(SfxEvent preset) + { + Is3D = preset.Is3D; + Is2D = preset.Is2D; + IsGui = preset.IsGui; + IsHudVo = preset.IsHudVo; + 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; + Priority = preset.Priority; + Probability = preset.Probability; + PlayCount = preset.PlayCount; + LoopFadeInSeconds = preset.LoopFadeInSeconds; + LoopFadeOutSeconds = preset.LoopFadeOutSeconds; + MaxInstances = preset.MaxInstances; + MinPredelay = preset.MinPredelay; + MaxPredelay = preset.MaxPredelay; + MinPostdelay = preset.MinPostdelay; + MaxPostdelay = preset.MaxPostdelay; + OverlapTestName = preset.OverlapTestName; + ChainedSfxEventName = preset.ChainedSfxEventName; + MinVolume = preset.MinVolume; + MaxVolume = preset.MaxVolume; + MinPitch = preset.MinPitch; + MaxPitch = preset.MaxPitch; + MinPan2D = preset.MinPan2D; + MaxPan2D = preset.MaxPan2D; + } + + private static void AdjustMinMaxValues(ref byte minValue, ref byte maxValue) + { + if (minValue > maxValue) + minValue = maxValue; + if (maxValue < minValue) + maxValue = minValue; + } + + private static void AdjustMinMaxValues(ref uint minValue, ref uint maxValue) + { + if (minValue > maxValue) + minValue = maxValue; + if (maxValue < minValue) + maxValue = minValue; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/SfxEventGameManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/SfxEventGameManager.cs new file mode 100644 index 0000000..0b4c407 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/SfxEventGameManager.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine.Database; +using PG.StarWarsGame.Engine.Database.ErrorReporting; +using PG.StarWarsGame.Engine.IO.Repositories; +using PG.StarWarsGame.Engine.Localization; +using PG.StarWarsGame.Engine.Xml.Parsers; + +namespace PG.StarWarsGame.Engine.Audio.Sfx; + +internal class SfxEventGameManager(GameRepository repository, DatabaseErrorListenerWrapper errorListener, IServiceProvider serviceProvider) + : GameManagerBase(repository, errorListener, serviceProvider), ISfxEventGameManager +{ + public IEnumerable InstalledLanguages { get; private set; } = []; + + protected override async Task InitializeCoreAsync(CancellationToken token) + { + Logger?.LogInformation("Initializing Language files..."); + InstalledLanguages = GameRepository.InitializeInstalledSfxMegFiles().ToList(); + Logger?.LogInformation("Finished initializing Language files"); + + Logger?.LogInformation("Parsing SFXEvents..."); + + var contentParser = ServiceProvider.GetRequiredService(); + contentParser.XmlParseError += OnParseError; + try + { + await Task.Run(() => contentParser.ParseEntriesFromContainerXml( + "DATA\\XML\\SFXEventFiles.XML", + ErrorListener, + GameRepository, + "DATA\\XML", + NamedEntries, + VerifyFilePathLength), + token); + } + finally + { + contentParser.XmlParseError -= OnParseError; + } + } + + private void OnParseError(object sender, XmlContainerParserErrorEventArgs e) + { + if (e.IsContainer || e.IsError) + { + e.Continue = false; + ErrorListener.OnInitializationError(new InitializationError + { + GameManager = ToString(), + Message = GetMessage(e) + }); + } + } + + private static string GetMessage(XmlContainerParserErrorEventArgs errorEventArgs) + { + if (errorEventArgs.IsError) + return $"Error while parsing SFXEvent XML file '{errorEventArgs.File}': {errorEventArgs.Exception.Message}"; + return "Could not find SFXEventFiles.xml"; + } + + private void VerifyFilePathLength(string filePath) + { + if (filePath.Length > PGConstants.MaxSFXEventDatabaseFileName) + { + ErrorListener.OnInitializationError(new InitializationError + { + GameManager = ToString(), + Message = $"SFXEvent file '{filePath}' is longer than {PGConstants.MaxSFXEventDatabaseFileName} characters." + }); + } + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarComponentType.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarComponentType.cs new file mode 100644 index 0000000..793eb44 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarComponentType.cs @@ -0,0 +1,15 @@ +namespace PG.StarWarsGame.Engine.CommandBar; + +public enum CommandBarComponentType +{ + Shell, + Icon, + Button, + Text, + TextButton, + Model, + Bar, + Select, + Count, + None, +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager.cs new file mode 100644 index 0000000..d53808e --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager.cs @@ -0,0 +1,84 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PG.Commons.Collections; +using PG.Commons.Hashing; +using PG.StarWarsGame.Engine.CommandBar.Xml; +using PG.StarWarsGame.Engine.Database; +using PG.StarWarsGame.Engine.Database.ErrorReporting; +using PG.StarWarsGame.Engine.IO.Repositories; +using PG.StarWarsGame.Engine.Xml.Parsers; + +namespace PG.StarWarsGame.Engine.CommandBar; + +public interface ICommandBarGameManager : IGameManager +{ +} + +internal class CommandBarGameManager( + GameRepository repository, + DatabaseErrorListenerWrapper errorListener, + IServiceProvider serviceProvider) + : GameManagerBase(repository, errorListener, serviceProvider), ICommandBarGameManager +{ + protected override async Task InitializeCoreAsync(CancellationToken token) + { + Logger?.LogInformation("PCreating command bar components..."); + + var contentParser = ServiceProvider.GetRequiredService(); + contentParser.XmlParseError += OnParseError; + + var parsedCommandBarComponents = new ValueListDictionary(); + + try + { + await Task.Run(() => contentParser.ParseEntriesFromContainerXml( + "DATA\\XML\\CommandBarComponentFiles.XML", + ErrorListener, + GameRepository, + ".\\DATA\\XML", + parsedCommandBarComponents, + VerifyFilePathLength), + token); + } + finally + { + contentParser.XmlParseError -= OnParseError; + } + } + + private void OnParseError(object sender, XmlContainerParserErrorEventArgs e) + { + if (e.IsContainer || e.IsError) + { + e.Continue = false; + ErrorListener.OnInitializationError(new InitializationError + { + GameManager = ToString(), + Message = GetMessage(e) + }); + } + } + + private static string GetMessage(XmlContainerParserErrorEventArgs errorEventArgs) + { + if (errorEventArgs.IsError) + 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) + { + ErrorListener.OnInitializationError(new InitializationError + { + GameManager = ToString(), + Message = $"CommandBar file '{filePath}' is longer than {PGConstants.MaxCommandBarDatabaseFileName} characters." + }); + } + } +} + diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/Xml/CommandBarComponentData.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/Xml/CommandBarComponentData.cs new file mode 100644 index 0000000..fb06793 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/Xml/CommandBarComponentData.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Numerics; +using PG.Commons.Hashing; +using PG.StarWarsGame.Engine.Xml; +using PG.StarWarsGame.Files.XML; + +namespace PG.StarWarsGame.Engine.CommandBar.Xml; + +public sealed class CommandBarComponentData(string name, Crc32 crc, XmlLocationInfo location) : NamedXmlObject(name, crc, location) +{ + public const float DefaultScale = 1.0f; + public const float DefaultBlinkRate = 0.2f; + public const int DefaultBaseLayer = 2; + public static readonly Vector2 DefaultOffsetWidescreenValue = new(9.9999998e17f, 9.9999998e17f); + + 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; } = []; + + public string? IconTextureName { get; internal set; } + public string? DisabledTextureName { get; internal set; } + public string? FlashTextureName { get; internal set; } + public string? BuildTextureName { get; internal set; } + public string? ModelName { get; internal set; } + public string? BoneName { get; internal set; } + public string? CursorTextureName { get; internal set; } + public string? FontName { get; internal set; } + public string? ClickSfx { get; internal set; } + public string? MouseOverSfx { get; internal set; } + public string? RightClickSfx { get; internal set; } + public string? Type { get; internal set; } + public string? Group { get; internal set; } + public string? AssociatedText { get; internal set; } + + public bool DragAndDrop { get; internal set; } + public bool DragSelect { get; internal set; } + public bool Receptor { get; internal set; } + public bool Toggle { get; internal set; } + public bool Tab { get; internal set; } + public bool Hidden { get; internal set; } + public bool ClearColor { get; internal set; } + public bool Disabled { get; internal set; } + public bool SwapTexture { get; internal set; } + public bool DrawAdditive { get; internal set; } + public bool Editable { get; internal set; } + public bool TextOutline { get; internal set; } + public bool Stackable { get; internal set; } + public bool ModelOffsetX { get; internal set; } + public bool ModelOffsetY { get; internal set; } + public bool ScaleModelX { get; internal set; } + public bool ScaleModelY { get; internal set; } + public bool Collideable { get; internal set; } + public bool TextEmboss { get; internal set; } + public bool ShouldGhost { get; internal set; } + public bool GhostBaseOnly { get; internal set; } + public bool CrossFade { get; internal set; } + public bool LeftJustified { get; internal set; } + public bool RightJustified { get; internal set; } + public bool NoShell { get; internal set; } + public bool SnapDrag { get; internal set; } + public bool SnapLocation { get; internal set; } + public bool OffsetRender { get; internal set; } + public bool BlinkFade { get; internal set; } + public bool NoHiddenCollision { get; internal set; } + public bool ManualOffset { get; internal set; } + public bool SelectedAlpha { get; internal set; } + public bool PixelAlign { get; internal set; } + public bool CanDragStack { get; internal set; } + public bool CanAnimate { get; internal set; } + public bool LoopAnim { get; internal set; } + public bool SmoothBar { get; internal set; } + public bool OutlinedBar { get; internal set; } + public bool DragBack { get; internal set; } + public bool LowerEffectAdditive { get; internal set; } + public bool UpperEffectAdditive { get; internal set; } + public bool ClickShift { get; internal set; } + public bool TutorialScene { get; internal set; } + public bool DialogScene { get; internal set; } + public bool ShouldRenderAtDragPos { get; internal set; } + public bool DisableDarken { get; internal set; } + public bool AnimateBack { get; internal set; } + public bool AnimateUpperEffect { get; internal set; } + + public int BaseLayer { get; internal set; } = DefaultBaseLayer; + public int MaxBarLevel { get; internal set; } + + public uint MaxTextLength { get; internal set; } + public int FontPointSize { get; internal set; } + + public float ScaleDuration { get; internal set; } + public float BlinkDuration { get; internal set; } + public float MaxTextWidth { get; internal set; } + public float BlinkRate { get; internal set; } = DefaultBlinkRate; + public float Scale { get; internal set; } = DefaultScale; + public float AnimFps { get; internal set; } + + public Vector2 Size { get; internal set; } + public Vector2 TextOffset { get; internal set; } + public Vector2 TextOffset2 { get; internal set; } + public Vector2 Offset { get; internal set; } + public Vector2 DefaultOffset { get; internal set; } + public Vector2 DefaultOffsetWidescreen { get; internal set; } = DefaultOffsetWidescreenValue; + public Vector2 IconOffset { get; internal set; } + public Vector2 MouseOverOffset { get; internal set; } + public Vector2 DisabledOffset { get; internal set; } + public Vector2 BuildDialOffset { get; internal set; } + public Vector2 BuildDial2Offset { get; internal set; } + public Vector2 LowerEffectOffset { get; internal set; } + public Vector2 UpperEffectOffset { get; internal set; } + public Vector2 OverlayOffset { get; internal set; } + public Vector2 Overlay2Offset { get; internal set; } + + internal override void CoerceValues() + { + 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]; + + if (current.AsSpan().IndexOf('_') != -1) + newFontNames[i] = current.Replace('_', ' '); + else + newFontNames[i] = current; + } + AlternateFontNames = new ReadOnlyCollection(newFontNames); + } +} + diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/GameConstants.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/GameConstants.cs deleted file mode 100644 index a2a4498..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/GameConstants.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace PG.StarWarsGame.Engine.DataTypes; - -public class GameConstants -{ - -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/GameObject.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/GameObject.cs deleted file mode 100644 index 6cfd4c0..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/GameObject.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using PG.Commons.Hashing; -using PG.StarWarsGame.Files.XML; - -namespace PG.StarWarsGame.Engine.DataTypes; - -public sealed class GameObject : XmlObject -{ - internal GameObject( - string type, - string name, - Crc32 nameCrc, - GameObjectType estimatedType, - IReadOnlyValueListDictionary properties, - XmlLocationInfo location) - : base(name, nameCrc, properties, location) - { - Type = type ?? throw new ArgumentNullException(nameof(type)); - EstimatedType = estimatedType; - } - - public string Type { get; } - - - public GameObjectType EstimatedType { get; } - - /// - /// Gets all model files (including particles) the game object references. - /// - public ISet Models - { - get - { - var models = XmlProperties.AggregateValues - (new HashSet - { - "Galactic_Model_Name", - "Destroyed_Galactic_Model_Name", - "Land_Model_Name", - "Space_Model_Name", - "Model_Name", - "Tactical_Model_Name", - "Galactic_Fleet_Override_Model_Name", - "GUI_Model_Name", - "GUI_Model", - // This can either be a model or a unit reference - "Land_Model_Anim_Override_Name", - "xxxSpace_Model_Name", - "Damaged_Smoke_Asset_Name" - }, v => v.EndsWith(".alo", StringComparison.OrdinalIgnoreCase), - ValueListDictionaryExtensions.AggregateStrategy.LastValuePerKey); - - var terrainMappedModels = LandTerrainModelMapping?.Select(x => x.Model); - if (terrainMappedModels is null) - return new HashSet(models, StringComparer.OrdinalIgnoreCase); - - return new HashSet(models.Concat(terrainMappedModels), StringComparer.OrdinalIgnoreCase); - } - } - - public IList<(string Terrain, string Model)>? LandTerrainModelMapping => - GetLastPropertyOrDefault>("Land_Terrain_Model_Mapping"); -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/SfxEvent.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/SfxEvent.cs deleted file mode 100644 index 9920449..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/SfxEvent.cs +++ /dev/null @@ -1,260 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.Xml.Tags; -using PG.StarWarsGame.Files.XML; - -namespace PG.StarWarsGame.Engine.DataTypes; - -public sealed class SfxEvent : XmlObject -{ - public const byte MaxVolumeValue = 100; - public const byte MaxPitchValue = 200; - public const byte MinPitchValue = 50; - 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; - - // Default values which are not the default value of the type - public const byte DefaultPriority = 3; - public const bool DefaultIs3d = true; - public const byte DefaultProbability = 100; - public const sbyte DefaultPlayCount = 1; - public const sbyte DefaultMaxInstances = 1; - public const byte DefaultMinVolume = 100; - public const byte DefaultMaxVolume = 100; - public const byte DefaultMinPitch = 100; - public const byte DefaultMaxPitch = 100; - public const byte DefaultMinPan2d = 50; - public const byte DefaultMaxPan2d = 50; - public const float DefaultVolumeSaturationDistance = 300.0f; - - private SfxEvent? _preset; - private string? _presetName; - private string? _overlapTestName; - private string? _chainedSfxEvent; - private IReadOnlyList? _textIds; - private IReadOnlyList? _preSamples; - private IReadOnlyList? _samples; - private IReadOnlyList? _postSamples; - private bool? _isPreset; - private bool? _is2D; - private bool? _is3D; - private bool? _isGui; - private bool? _isHudVo; - private bool? _isUnitResponseVo; - private bool? _isAmbientVo; - private bool? _isLocalized; - private bool? _playSequentially; - private bool? _killsPreviousObjectsSfx; - private byte? _priority; - private byte? _probability; - private sbyte? _playCount; - private sbyte? _maxInstances; - private uint? _minPredelay; - private uint? _maxPredelay; - private uint? _minPostdelay; - private uint? _maxPostdelay; - private float? _loopFadeInSeconds; - private float? _loopFadeOutSeconds; - private float? _volumeSaturationDistance; - - private static readonly Func PriorityCoercion = priority => - { - if (!priority.HasValue) - return DefaultPriority; - if (priority < MinPriorityValue) - return MinPriorityValue; - if (priority > MaxPriorityValue) - return MaxPriorityValue; - return priority; - }; - - private static readonly Func MaxInstancesCoercion = maxInstances => - { - if (!maxInstances.HasValue) - return DefaultMaxInstances; - if (maxInstances < MinMaxInstances) - return MinMaxInstances; - return maxInstances; - }; - - private static readonly Func ProbabilityCoercion = probability => - { - if (!probability.HasValue) - return DefaultProbability; - if (probability > MaxProbability) - return MaxProbability; - return probability; - }; - - private static readonly Func PlayCountCoercion = playCount => - { - if (!playCount.HasValue) - return DefaultPlayCount; - if (playCount < InfinitivePlayCount) - return InfinitivePlayCount; - return playCount; - }; - - private static readonly Func LoopAndSaturationCoercion = loopSeconds => - { - if (!loopSeconds.HasValue) - return DefaultVolumeSaturationDistance; - if (loopSeconds < MinLoopSeconds) - return MinLoopSeconds; - return loopSeconds; - }; - - public bool IsPreset => LazyInitValue(ref _isPreset, SfxEventXmlTags.IsPreset, false)!.Value; - - public bool Is3D => LazyInitValue(ref _is3D, SfxEventXmlTags.Is3D, DefaultIs3d)!.Value; - - public bool Is2D => LazyInitValue(ref _is2D, SfxEventXmlTags.Is2D, false)!.Value; - - public bool IsGui => LazyInitValue(ref _isGui, SfxEventXmlTags.IsGui, false)!.Value; - - public bool IsHudVo => LazyInitValue(ref _isHudVo, SfxEventXmlTags.IsHudVo, false)!.Value; - - public bool IsUnitResponseVo => LazyInitValue(ref _isUnitResponseVo, SfxEventXmlTags.IsUnitResponseVo, false)!.Value; - - public bool IsAmbientVo => LazyInitValue(ref _isAmbientVo, SfxEventXmlTags.IsAmbientVo, false)!.Value; - - public bool IsLocalized => LazyInitValue(ref _isLocalized, SfxEventXmlTags.Localize, false)!.Value; - - public SfxEvent? Preset => LazyInitValue(ref _preset, SfxEventXmlTags.PresetXRef, null); - - public string? UsePresetName => LazyInitValue(ref _presetName, SfxEventXmlTags.UsePreset, null); - - public bool PlaySequentially => LazyInitValue(ref _playSequentially, SfxEventXmlTags.PlaySequentially, false)!.Value; - - public IEnumerable AllSamples => PreSamples.Concat(Samples).Concat(PostSamples); - - public IReadOnlyList PreSamples => LazyInitValue(ref _preSamples, SfxEventXmlTags.PreSamples, Array.Empty()); - - public IReadOnlyList Samples => LazyInitValue(ref _samples, SfxEventXmlTags.Samples, Array.Empty()); - - public IReadOnlyList PostSamples => LazyInitValue(ref _postSamples, SfxEventXmlTags.PostSamples, Array.Empty()); - - public IReadOnlyList LocalizedTextIDs => LazyInitValue(ref _textIds, SfxEventXmlTags.TextID, Array.Empty()); - - public byte Priority => LazyInitValue(ref _priority, SfxEventXmlTags.Priority, DefaultPriority, PriorityCoercion)!.Value; - - public byte Probability => LazyInitValue(ref _probability, SfxEventXmlTags.Probability, DefaultProbability, ProbabilityCoercion)!.Value; - - public sbyte PlayCount => LazyInitValue(ref _playCount, SfxEventXmlTags.PlayCount, DefaultPlayCount, PlayCountCoercion)!.Value; - - public float LoopFadeInSeconds => LazyInitValue(ref _loopFadeInSeconds, SfxEventXmlTags.LoopFadeInSeconds, 0f, LoopAndSaturationCoercion)!.Value; - - public float LoopFadeOutSeconds => LazyInitValue(ref _loopFadeOutSeconds, SfxEventXmlTags.LoopFadeOutSeconds, 0f, LoopAndSaturationCoercion)!.Value; - - public sbyte MaxInstances => LazyInitValue(ref _maxInstances, SfxEventXmlTags.MaxInstances, DefaultMaxInstances, MaxInstancesCoercion)!.Value; - - public uint MinPredelay => LazyInitValue(ref _minPredelay, SfxEventXmlTags.MinPredelay, 0u)!.Value; - - public uint MaxPredelay => LazyInitValue(ref _maxPredelay, SfxEventXmlTags.MaxPredelay, 0u)!.Value; - - public uint MinPostdelay => LazyInitValue(ref _minPostdelay, SfxEventXmlTags.MinPostdelay, 0u)!.Value; - - public uint MaxPostdelay => LazyInitValue(ref _maxPostdelay, SfxEventXmlTags.MaxPostdelay, 0u)!.Value; - - public float VolumeSaturationDistance => LazyInitValue(ref _volumeSaturationDistance, - SfxEventXmlTags.VolumeSaturationDistance, DefaultVolumeSaturationDistance, LoopAndSaturationCoercion)!.Value; - - public bool KillsPreviousObjectsSfx => LazyInitValue(ref _killsPreviousObjectsSfx, SfxEventXmlTags.KillsPreviousObjectSFX, false)!.Value; - - public string? OverlapTestName => LazyInitValue(ref _overlapTestName, SfxEventXmlTags.OverlapTest, null); - - public string? ChainedSfxEventName => LazyInitValue(ref _chainedSfxEvent, SfxEventXmlTags.ChainedSfxEvent, null); - - public byte MinVolume { get; } - - public byte MaxVolume { get; } - - public byte MinPitch { get; } - - public byte MaxPitch { get; } - - public byte MinPan2D { get; } - - public byte MaxPan2D { get; } - - internal SfxEvent(string name, Crc32 nameCrc, IReadOnlyValueListDictionary properties, - XmlLocationInfo location) - : base(name, nameCrc, properties, location) - { - var minMaxVolume = GetMinMaxVolume(properties); - MinVolume = minMaxVolume.min; - MaxVolume = minMaxVolume.max; - - var minMaxPitch = GetMinMaxPitch(properties); - MinPitch = minMaxPitch.min; - MaxPitch = minMaxPitch.max; - - var minMaxPan = GetMinMaxPan2d(properties); - MinPan2D = minMaxPan.min; - MaxPan2D = minMaxPan.max; - } - - private static (byte min, byte max) GetMinMaxVolume(IReadOnlyValueListDictionary properties) - { - return GetMinMaxValues(properties, SfxEventXmlTags.MinVolume, SfxEventXmlTags.MaxVolume, DefaultMinVolume, - DefaultMaxVolume, null, MaxVolumeValue); - } - - private static (byte min, byte max) GetMinMaxPitch(IReadOnlyValueListDictionary properties) - { - return GetMinMaxValues(properties, SfxEventXmlTags.MinPitch, SfxEventXmlTags.MaxPitch, DefaultMinPitch, - DefaultMaxPitch, MinPitchValue, MaxPitchValue); - } - - private static (byte min, byte max) GetMinMaxPan2d(IReadOnlyValueListDictionary properties) - { - return GetMinMaxValues(properties, SfxEventXmlTags.MinPan2D, SfxEventXmlTags.MaxPan2D, DefaultMinPan2d, - DefaultMaxPan2d, null, MaxPan2dValue); - } - - - private static (byte min, byte max) GetMinMaxValues( - IReadOnlyValueListDictionary properties, - string minTag, - string maxTag, - byte defaultMin, - byte defaultMax, - byte? totalMinValue, - byte? totalMaxValue) - { - var minValue = !properties.TryGetLastValue(minTag, out var minObj) ? defaultMin : Convert.ToByte(minObj); - var maxValue = !properties.TryGetLastValue(maxTag, out var maxObj) ? defaultMax : Convert.ToByte(maxObj); - - if (totalMaxValue.HasValue) - { - if (minValue > totalMaxValue) - minValue = totalMaxValue.Value; - if (maxValue > totalMaxValue) - maxValue = totalMaxValue.Value; - } - - if (totalMinValue.HasValue) - { - if (minValue < totalMinValue) - minValue = totalMinValue.Value; - if (maxValue < totalMinValue) - maxValue = totalMinValue.Value; - } - - if (minValue > maxValue) - minValue = maxValue; - - if (maxValue < minValue) - maxValue = minValue; - - return (minValue, maxValue); - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/XmlObject.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/XmlObject.cs deleted file mode 100644 index b8161b1..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/XmlObject.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using PG.Commons.DataTypes; -using PG.Commons.Hashing; -using PG.StarWarsGame.Files.XML; - -namespace PG.StarWarsGame.Engine.DataTypes; - -public abstract class XmlObject( - string name, - Crc32 nameCrc, - IReadOnlyValueListDictionary properties, - XmlLocationInfo location) - : IHasCrc32 -{ - public XmlLocationInfo Location { get; } = location; - - public Crc32 Crc32 { get; } = nameCrc; - - public IReadOnlyValueListDictionary XmlProperties { get; } = properties ?? throw new ArgumentNullException(nameof(properties)); - - public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); - - public T? GetLastPropertyOrDefault(string tagName, T? defaultValue = default) - { - if (!XmlProperties.TryGetLastValue(tagName, out var value)) - return defaultValue; - return (T)value; - } - - protected T LazyInitValue(ref T? field, string tag, T defaultValue, Func? coerceFunc = null) - { - if (field is null) - { - if (XmlProperties.TryGetLastValue(tag, out var value)) - { - var tValue = (T)value; - if (coerceFunc is not null) - tValue = coerceFunc(tValue); - field = tValue; - } - else - field = defaultValue; - } - - return field; - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/ErrorReporting/DatabaseErrorListener.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/ErrorReporting/DatabaseErrorListener.cs new file mode 100644 index 0000000..d123437 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/ErrorReporting/DatabaseErrorListener.cs @@ -0,0 +1,12 @@ +namespace PG.StarWarsGame.Engine.Database.ErrorReporting; + +public abstract class DatabaseErrorListener : IDatabaseErrorListener +{ + public virtual void OnXmlError(XmlError error) + { + } + + public virtual void OnInitializationError(InitializationError error) + { + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/ErrorReporting/DatabaseErrorListenerWrapper.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/ErrorReporting/DatabaseErrorListenerWrapper.cs new file mode 100644 index 0000000..b68cfe8 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/ErrorReporting/DatabaseErrorListenerWrapper.cs @@ -0,0 +1,61 @@ +using System; +using AnakinRaW.CommonUtilities; +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.Parsers; + +namespace PG.StarWarsGame.Engine.Database.ErrorReporting; + +internal class DatabaseErrorListenerWrapper : DisposableObject, IDatabaseErrorListener, IXmlParserErrorListener +{ + internal event EventHandler? InitializationError; + + private readonly IDatabaseErrorListener? _errorListener; + private IPrimitiveXmlErrorParserProvider? _primitiveXmlParserErrorProvider; + + public DatabaseErrorListenerWrapper(IDatabaseErrorListener? errorListener, IServiceProvider serviceProvider) + { + _errorListener = errorListener; + if (_errorListener is null) + return; + _primitiveXmlParserErrorProvider = serviceProvider.GetRequiredService(); + _primitiveXmlParserErrorProvider.XmlParseError += ((IXmlParserErrorListener)this).OnXmlParseError; + } + + public void OnXmlError(XmlError error) + { + _errorListener?.OnXmlError(error); + } + + public void OnInitializationError(InitializationError error) + { + InitializationError?.Invoke(this, error); + if (_errorListener is null) + return; + _errorListener.OnInitializationError(error); + } + + protected override void DisposeResources() + { + base.DisposeResources(); + if (_primitiveXmlParserErrorProvider is null) + return; + _primitiveXmlParserErrorProvider.XmlParseError -= ((IXmlParserErrorListener)this).OnXmlParseError; + _primitiveXmlParserErrorProvider = null!; + } + + void IXmlParserErrorListener.OnXmlParseError(IPetroglyphXmlParser parser, XmlParseErrorEventArgs error) + { + if (_errorListener is null) + return; + + OnXmlError(new XmlError + { + FileLocation = error.Location, + Parser = parser.ToString(), + Message = error.Message, + ErrorKind = error.ErrorKind, + Element = error.Element + }); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/ErrorReporting/IDatabaseErrorListener.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/ErrorReporting/IDatabaseErrorListener.cs new file mode 100644 index 0000000..2df54c5 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/ErrorReporting/IDatabaseErrorListener.cs @@ -0,0 +1,7 @@ +namespace PG.StarWarsGame.Engine.Database.ErrorReporting; + +public interface IDatabaseErrorListener +{ + void OnXmlError(XmlError error); + void OnInitializationError(InitializationError error); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/ErrorReporting/InitializationError.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/ErrorReporting/InitializationError.cs new file mode 100644 index 0000000..55262ea --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/ErrorReporting/InitializationError.cs @@ -0,0 +1,8 @@ +namespace PG.StarWarsGame.Engine.Database.ErrorReporting; + +public sealed class InitializationError +{ + public required string GameManager { get; init; } + + public required string Message { get; init; } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/ErrorReporting/XmlError.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/ErrorReporting/XmlError.cs new file mode 100644 index 0000000..60b16bd --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/ErrorReporting/XmlError.cs @@ -0,0 +1,18 @@ +using System.Xml.Linq; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.ErrorHandling; + +namespace PG.StarWarsGame.Engine.Database.ErrorReporting; + +public sealed class XmlError +{ + public required XmlLocationInfo FileLocation { get; init; } + + public required string 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/Database/GameDatabase.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabase.cs index f32f347..a72feb1 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabase.cs @@ -1,19 +1,27 @@ using System.Collections.Generic; -using PG.StarWarsGame.Engine.DataTypes; -using PG.StarWarsGame.Engine.Language; -using PG.StarWarsGame.Engine.Repositories; +using PG.StarWarsGame.Engine.Audio.Sfx; +using PG.StarWarsGame.Engine.CommandBar; +using PG.StarWarsGame.Engine.GameConstants; +using PG.StarWarsGame.Engine.GameObjects; +using PG.StarWarsGame.Engine.GuiDialog; +using PG.StarWarsGame.Engine.IO.Repositories; +using PG.StarWarsGame.Engine.Localization; namespace PG.StarWarsGame.Engine.Database; internal class GameDatabase : IGameDatabase { + public required ICommandBarGameManager CommandBarManager { get; init; } + public required IGameRepository GameRepository { get; init; } - public required GameConstants GameConstants { get; init; } + public required IGameConstants GameConstants { get; init; } + + public required IGuiDialogManager GuiDialogManager { get; init; } - public required IXmlDatabase GameObjects { get; init; } + public required IGameObjectTypeGameManager GameObjectTypeManager { get; init; } - public required IXmlDatabase SfxEvents { get; init; } + public required ISfxEventGameManager SfxGameManager { get; init; } - public IEnumerable InstalledLanguages { get; init; } + public required IEnumerable InstalledLanguages { get; init; } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabaseService.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabaseService.cs index fc1f5ca..e2122bf 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabaseService.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabaseService.cs @@ -1,51 +1,24 @@ using System; using System.Threading; using System.Threading.Tasks; -using AnakinRaW.CommonUtilities; using Microsoft.Extensions.DependencyInjection; -using PG.StarWarsGame.Engine.Database.Initialization; -using PG.StarWarsGame.Engine.Repositories; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; +using PG.StarWarsGame.Engine.Database.ErrorReporting; +using PG.StarWarsGame.Engine.IO.Repositories; namespace PG.StarWarsGame.Engine.Database; -internal class GameDatabaseService : DisposableObject, IXmlParserErrorListener, IGameDatabaseService +internal class GameDatabaseService(IServiceProvider serviceProvider) : IGameDatabaseService { - public event XmlErrorEventHandler? XmlParseError; - - private readonly IServiceProvider _serviceProvider; - private IPrimitiveXmlErrorParserProvider _primitiveXmlParserErrorProvider; - - public GameDatabaseService(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - _primitiveXmlParserErrorProvider = serviceProvider.GetRequiredService(); - _primitiveXmlParserErrorProvider.XmlParseError += OnXmlParseError; - } - - public async Task CreateDatabaseAsync( - GameEngineType targetEngineType, - GameLocations locations, + public Task InitializeGameAsync( + GameInitializationOptions gameInitializationOptions, CancellationToken cancellationToken = default) { - var repoFactory = _serviceProvider.GetRequiredService(); - var repository = repoFactory.Create(targetEngineType, locations, this); + var repoFactory = serviceProvider.GetRequiredService(); - var pipeline = new GameDatabaseCreationPipeline(repository, this, _serviceProvider); - await pipeline.RunAsync(cancellationToken); - return pipeline.GameDatabase; - } + using var errorListenerWrapper = new DatabaseErrorListenerWrapper(gameInitializationOptions.ErrorListener, serviceProvider); + var repository = repoFactory.Create(gameInitializationOptions.TargetEngineType, gameInitializationOptions.Locations, errorListenerWrapper); - protected override void DisposeManagedResources() - { - base.DisposeManagedResources(); - _primitiveXmlParserErrorProvider.XmlParseError -= OnXmlParseError; - _primitiveXmlParserErrorProvider = null!; - } - - public void OnXmlParseError(IPetroglyphXmlParser parser, XmlParseErrorEventArgs error) - { - XmlParseError?.Invoke(parser, error); + var gameInitializer = new GameInitializer(repository, gameInitializationOptions.CancelOnError, serviceProvider); + return gameInitializer.InitializeAsync(errorListenerWrapper, cancellationToken); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameInitializationOptions.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameInitializationOptions.cs new file mode 100644 index 0000000..8c11841 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameInitializationOptions.cs @@ -0,0 +1,14 @@ +using PG.StarWarsGame.Engine.Database.ErrorReporting; + +namespace PG.StarWarsGame.Engine.Database; + +public class GameInitializationOptions +{ + public required GameEngineType TargetEngineType { get; init; } + + public required GameLocations Locations { get; init; } + + public bool CancelOnError { get; init; } + + public IDatabaseErrorListener? ErrorListener { get; init; } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameInitializer.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameInitializer.cs new file mode 100644 index 0000000..7ad3ee9 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameInitializer.cs @@ -0,0 +1,109 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine.Audio.Sfx; +using PG.StarWarsGame.Engine.CommandBar; +using PG.StarWarsGame.Engine.Database.ErrorReporting; +using PG.StarWarsGame.Engine.GameObjects; +using PG.StarWarsGame.Engine.GuiDialog; +using PG.StarWarsGame.Engine.IO.Repositories; + +namespace PG.StarWarsGame.Engine.Database; + +internal class GameInitializer(GameRepository repository, bool cancelOnError, IServiceProvider serviceProvider) +{ + private readonly ILogger? _logger = serviceProvider.GetService()?.CreateLogger(typeof(GameInitializer)); + + private CancellationTokenSource? _cancellationTokenSource; + + public async Task InitializeAsync(DatabaseErrorListenerWrapper errorListener, CancellationToken token) + { + _logger?.LogInformation("Initializing Game Database..."); + errorListener.InitializationError += OnInitializationError; + + _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); + + try + { + // LensFlares.xml + // SurfaceFX.xml + // TerrainDecalFX.xml + // GraphicDetails.xml + // DynamicTrackFX.xml + // ShadowBlobMaterials.xml + // TacticalCameras.xml + // LightSources.xml + // StarWars3DTextCrawl.xml + // MusicEvents.xml + // SpeechEvents.xml + // GameConstantsXml.xml + // Audio.xml + // WeatherAudio.xml + // HeroClash.xml + // TradeRouteLines.xml + // RadarMap.xml + // WeatherModifiers.xml + // Movies.xml + // LightningEffectTypes.xml + // DifficultyAdjustments.xml + // WeatherScenarios.xml + // UnitAbilityTypes.xml + // BlackMarketItems.xml + // MovementClassTypeDefs.xml + // AITerrainEffectiveness.xml + + + // CONTAINER FILES: + // GameObjectFiles.xml + // TradeRouteFiles.xml + // HardPointDataFiles.xml + // CampaignFiles.xml + // FactionFiles.xml + // TargetingPrioritySetFiles.xml + // MousePointerFiles.xml + + var gameConstants = new GameConstants.GameConstants(repository, errorListener, serviceProvider); + await gameConstants.InitializeAsync( _cancellationTokenSource.Token); + + var guiDialogs = new GuiDialogGameManager(repository, errorListener, serviceProvider); + await guiDialogs.InitializeAsync(_cancellationTokenSource.Token); + + var sfxGameManager = new SfxEventGameManager(repository, errorListener, serviceProvider); + await sfxGameManager.InitializeAsync( _cancellationTokenSource.Token); + + var commandBarManager = new CommandBarGameManager(repository, errorListener, serviceProvider); + await commandBarManager.InitializeAsync( _cancellationTokenSource.Token); + + var gameObjetTypeManager = new GameObjectTypeTypeGameManager(repository, errorListener, serviceProvider); + await gameObjetTypeManager.InitializeAsync( _cancellationTokenSource.Token); + + repository.Seal(); + + return new GameDatabase + { + GameRepository = repository, + GameConstants = gameConstants, + GuiDialogManager = guiDialogs, + CommandBarManager = commandBarManager, + GameObjectTypeManager = gameObjetTypeManager, + SfxGameManager = sfxGameManager, + InstalledLanguages = sfxGameManager.InstalledLanguages + }; + } + finally + { + errorListener.InitializationError -= OnInitializationError; + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = null; + _logger?.LogInformation("Finished initializing game database"); + } + } + + private void OnInitializationError(object sender, InitializationError e) + { + if(cancelOnError) + _cancellationTokenSource?.Cancel(); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameManagerBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameManagerBase.cs new file mode 100644 index 0000000..3e1c18d --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameManagerBase.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Threading; +using System.Threading.Tasks; +using AnakinRaW.CommonUtilities.Collections; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PG.Commons.Collections; +using PG.Commons.Hashing; +using PG.StarWarsGame.Engine.Database.ErrorReporting; +using PG.StarWarsGame.Engine.IO.Repositories; + +namespace PG.StarWarsGame.Engine.Database; + +internal abstract class GameManagerBase(GameRepository repository, DatabaseErrorListenerWrapper errorListener, IServiceProvider serviceProvider) + : GameManagerBase(repository, errorListener, serviceProvider), IGameManager +{ + protected readonly ValueListDictionary NamedEntries = new(); + + public ICollection Entries => NamedEntries.Values; + + public ICollection EntryKeys => NamedEntries.Keys; + + public ReadOnlyFrugalList GetEntries(Crc32 key) + { + return NamedEntries.GetValues(key); + } +} + +internal abstract class GameManagerBase +{ + public event EventHandler? Initialized; + + private bool _initialized; + private protected readonly GameRepository GameRepository; + protected readonly IServiceProvider ServiceProvider; + protected readonly IFileSystem FileSystem; + protected readonly ILogger? Logger; + + protected readonly DatabaseErrorListenerWrapper ErrorListener; + + public bool IsInitialized => _initialized; + + protected GameManagerBase(GameRepository repository, DatabaseErrorListenerWrapper errorListener, IServiceProvider serviceProvider) + { + GameRepository = repository ?? throw new ArgumentNullException(nameof(repository)); + ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + Logger = serviceProvider.GetService()?.CreateLogger(GetType()); + FileSystem = serviceProvider.GetRequiredService(); + ErrorListener = errorListener ?? throw new ArgumentNullException(nameof(errorListener)); + } + + public async Task InitializeAsync(CancellationToken token) + { + ThrowIfAlreadyInitialized(); + token.ThrowIfCancellationRequested(); + try + { + await InitializeCoreAsync(token); + _initialized = true; + } + catch (Exception e) + { + Logger?.LogError(e, $"Initialization of {this} failed: {e.Message}"); + throw; + } + OnInitialized(); + } + + public override string ToString() + { + return GetType().Name; + } + + protected abstract Task InitializeCoreAsync(CancellationToken token); + + protected void ThrowIfAlreadyInitialized() + { + if (_initialized) + throw new InvalidOperationException("Game manager is already initialized."); + } + + protected void ThrowIfNotInitialized() + { + if (!_initialized) + throw new InvalidOperationException("Game manager not initialized."); + } + + private void OnInitialized() + { + Initialized?.Invoke(this, EventArgs.Empty); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabase.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabase.cs index 15dd8a1..087b7f8 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabase.cs @@ -1,7 +1,10 @@ using System.Collections.Generic; -using PG.StarWarsGame.Engine.DataTypes; -using PG.StarWarsGame.Engine.Language; -using PG.StarWarsGame.Engine.Repositories; +using PG.StarWarsGame.Engine.Audio.Sfx; +using PG.StarWarsGame.Engine.GameConstants; +using PG.StarWarsGame.Engine.GameObjects; +using PG.StarWarsGame.Engine.GuiDialog; +using PG.StarWarsGame.Engine.IO.Repositories; +using PG.StarWarsGame.Engine.Localization; namespace PG.StarWarsGame.Engine.Database; @@ -9,11 +12,13 @@ public interface IGameDatabase { IGameRepository GameRepository { get; } - GameConstants GameConstants { get; } + IGameConstants GameConstants { get; } - IXmlDatabase GameObjects { get; } + IGuiDialogManager GuiDialogManager { get; } - IXmlDatabase SfxEvents { get; } + IGameObjectTypeGameManager GameObjectTypeManager { get; } + + ISfxEventGameManager SfxGameManager { get; } IEnumerable InstalledLanguages { get; } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabaseService.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabaseService.cs index 575c521..46cfda2 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabaseService.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabaseService.cs @@ -1,14 +1,9 @@ -using System; -using System.Threading; +using System.Threading; using System.Threading.Tasks; -using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Engine.Database; -public interface IGameDatabaseService : IXmlParserErrorProvider, IDisposable +public interface IGameDatabaseService { - Task CreateDatabaseAsync( - GameEngineType targetEngineType, - GameLocations locations, - CancellationToken cancellationToken = default); + Task InitializeGameAsync(GameInitializationOptions gameInitializationOptions, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IXmlDatabase.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameManager.cs similarity index 75% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IXmlDatabase.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameManager.cs index c2edb8e..f331a24 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IXmlDatabase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameManager.cs @@ -1,11 +1,10 @@ using System.Collections.Generic; using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.DataTypes; namespace PG.StarWarsGame.Engine.Database; -public interface IXmlDatabase where T : XmlObject +public interface IGameManager { ICollection Entries { get; } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/CreateDatabaseStep.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/CreateDatabaseStep.cs deleted file mode 100644 index 2c1f20e..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/CreateDatabaseStep.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Threading; -using AnakinRaW.CommonUtilities.SimplePipeline.Steps; -using Microsoft.Extensions.Logging; -using PG.StarWarsGame.Engine.Repositories; - -namespace PG.StarWarsGame.Engine.Database.Initialization; - -internal abstract class CreateDatabaseStep(IGameRepository repository, IServiceProvider serviceProvider) - : PipelineStep(serviceProvider) where T : class -{ - public T Database { get; private set; } = null!; - - protected abstract string Name { get; } - - protected IGameRepository GameRepository { get; } = repository; - - protected sealed override void RunCore(CancellationToken token) - { - Logger?.LogDebug($"Creating {Name} database..."); - Database = CreateDatabase(); - } - - protected abstract T CreateDatabase(); -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs deleted file mode 100644 index 05f6e21..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using AnakinRaW.CommonUtilities.SimplePipeline; -using AnakinRaW.CommonUtilities.SimplePipeline.Runners; -using AnakinRaW.CommonUtilities.SimplePipeline.Steps; -using Microsoft.Extensions.Logging; -using PG.StarWarsGame.Engine.DataTypes; -using PG.StarWarsGame.Engine.Repositories; -using PG.StarWarsGame.Files.XML.ErrorHandling; - -namespace PG.StarWarsGame.Engine.Database.Initialization; - -internal class GameDatabaseCreationPipeline(GameRepository repository, IXmlParserErrorListener xmlParserErrorListener, IServiceProvider serviceProvider) - : Pipeline(serviceProvider) -{ - private ParseSingletonXmlStep _parseGameConstants = null!; - private ParseXmlDatabaseFromContainerStep _parseGameObjects = null!; - private ParseXmlDatabaseFromContainerStep _parseSfxEvents = null!; - - private StepRunner _parseXmlRunner = null!; - - public GameDatabase GameDatabase { get; private set; } = null!; - - protected override Task PrepareCoreAsync() - { - _parseXmlRunner = new ParallelRunner(4, ServiceProvider); - foreach (var xmlParserStep in CreateXmlParserSteps()) - _parseXmlRunner.AddStep(xmlParserStep); - - return Task.FromResult(true); - } - - private IEnumerable CreateXmlParserSteps() - { - // TODO: Use same load order as the engine! - - yield return _parseGameConstants = new ParseSingletonXmlStep("GameConstants", - "DATA\\XML\\GAMECONSTANTS.XML", repository, xmlParserErrorListener, ServiceProvider); - - yield return _parseGameObjects = new ParseXmlDatabaseFromContainerStep("GameObjects", - "DATA\\XML\\GAMEOBJECTFILES.XML", repository, xmlParserErrorListener, ServiceProvider); - - yield return _parseSfxEvents = new ParseXmlDatabaseFromContainerStep("SFXEvents", - "DATA\\XML\\SFXEventFiles.XML", repository, xmlParserErrorListener, ServiceProvider); - - // GUIDialogs.xml - // LensFlares.xml - // SurfaceFX.xml - // TerrainDecalFX.xml - // GraphicDetails.xml - // DynamicTrackFX.xml - // ShadowBlobMaterials.xml - // TacticalCameras.xml - // LightSources.xml - // StarWars3DTextCrawl.xml - // MusicEvents.xml - // SpeechEvents.xml - // GameConstants.xml - // Audio.xml - // WeatherAudio.xml - // HeroClash.xml - // TradeRouteLines.xml - // RadarMap.xml - // WeatherModifiers.xml - // Movies.xml - // LightningEffectTypes.xml - // DifficultyAdjustments.xml - // WeatherScenarios.xml - // UnitAbilityTypes.xml - // BlackMarketItems.xml - // MovementClassTypeDefs.xml - // AITerrainEffectiveness.xml - - - // CONTAINER FILES: - // GameObjectFiles.xml - // CommandBarComponentFiles.xml - // TradeRouteFiles.xml - // HardPointDataFiles.xml - // CampaignFiles.xml - // FactionFiles.xml - // TargetingPrioritySetFiles.xml - // MousePointerFiles.xml - } - - protected override async Task RunCoreAsync(CancellationToken token) - { - Logger?.LogInformation("Creating Game Database..."); - - try - { - try - { - Logger?.LogInformation("Parsing XML Files..."); - _parseXmlRunner.Error += OnError; - await _parseXmlRunner.RunAsync(token); - } - finally - { - Logger?.LogInformation("Finished parsing XML Files"); - _parseXmlRunner.Error -= OnError; - } - - ThrowIfAnyStepsFailed(_parseXmlRunner.Steps); - - token.ThrowIfCancellationRequested(); - - - Logger?.LogInformation("Initializing Language files..."); - var installedLanguages = repository.InitializeInstalledSfxMegFiles(); - Logger?.LogInformation("Finished initializing Language files"); - - - repository.Seal(); - - GameDatabase = new GameDatabase - { - GameRepository = repository, - GameConstants = _parseGameConstants.Database, - GameObjects = _parseGameObjects.Database, - SfxEvents = _parseSfxEvents.Database, - InstalledLanguages = installedLanguages - }; - } - finally - { - Logger?.LogInformation("Finished creating game database"); - } - } - - private sealed class ParseSingletonXmlStep(string name, string xmlFile, IGameRepository repository, IXmlParserErrorListener? listener, IServiceProvider serviceProvider) - : ParseXmlDatabaseStep(xmlFile, repository, listener, serviceProvider) where T : class - { - protected override string Name => name; - - protected override T CreateDatabase(IList parsedDatabaseEntries) - { - if (parsedDatabaseEntries.Count != 1) - throw new InvalidOperationException($"There can be only one {Name} model."); - - return parsedDatabaseEntries.First(); - } - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/ParseXmlDatabaseFromContainerStep.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/ParseXmlDatabaseFromContainerStep.cs deleted file mode 100644 index 5dcde76..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/ParseXmlDatabaseFromContainerStep.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.IO.Abstractions; -using System.Linq; -using System.Xml; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.DataTypes; -using PG.StarWarsGame.Engine.Repositories; -using PG.StarWarsGame.Engine.Xml; -using PG.StarWarsGame.Files.XML; -using PG.StarWarsGame.Files.XML.ErrorHandling; - -namespace PG.StarWarsGame.Engine.Database.Initialization; - -internal class ParseXmlDatabaseFromContainerStep( - string name, - string xmlFile, - IGameRepository repository, - IXmlParserErrorListener? listener, - IServiceProvider serviceProvider) - : CreateDatabaseStep>(repository, serviceProvider) - where T : XmlObject -{ - private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); - - protected readonly IPetroglyphXmlFileParserFactory FileParserFactory = serviceProvider.GetRequiredService(); - - protected override string Name => name; - - protected sealed override IXmlDatabase CreateDatabase() - { - using var containerStream = GameRepository.OpenFile(xmlFile); - var containerParser = FileParserFactory.GetFileParser(listener); - Logger?.LogDebug($"Parsing container data '{xmlFile}'"); - var container = containerParser.ParseFile(containerStream); - - var xmlFiles = container.Files.Select(x => _fileSystem.Path.Combine("DATA\\XML", x)).ToList(); - - var parsedEntries = new ValueListDictionary(); - - foreach (var file in xmlFiles) - { - using var fileStream = GameRepository.TryOpenFile(file); - - var parser = FileParserFactory.GetFileParser(listener); - - if (fileStream is null) - { - listener?.OnXmlParseError(parser, XmlParseErrorEventArgs.FromMissingFile(file)); - Logger?.LogWarning($"Could not find XML file '{file}'"); - continue; - } - - Logger?.LogDebug($"Parsing File '{file}'"); - - try - { - parser.ParseFile(fileStream, parsedEntries); - } - catch (XmlException e) - { - listener?.OnXmlParseError(parser, new XmlParseErrorEventArgs(file, null, XmlParseErrorKind.Unknown, e.Message)); - } - } - - return new XmlDatabase(parsedEntries, Services); - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/ParseXmlDatabaseStep.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/ParseXmlDatabaseStep.cs deleted file mode 100644 index 8050f02..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/ParseXmlDatabaseStep.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using PG.StarWarsGame.Engine.Repositories; -using PG.StarWarsGame.Engine.Xml; -using PG.StarWarsGame.Files.XML.ErrorHandling; - -namespace PG.StarWarsGame.Engine.Database.Initialization; - -internal abstract class ParseXmlDatabaseStep( - IList xmlFiles, - IGameRepository repository, - IXmlParserErrorListener? listener, - IServiceProvider serviceProvider) - : CreateDatabaseStep(repository, serviceProvider) - where T : class -{ - protected readonly IPetroglyphXmlFileParserFactory FileParserFactory = serviceProvider.GetRequiredService(); - - protected ParseXmlDatabaseStep(string xmlFile, IGameRepository repository, IXmlParserErrorListener? listener, IServiceProvider serviceProvider) - : this([xmlFile], repository, listener, serviceProvider) - { - } - - protected sealed override T CreateDatabase() - { - var parsedDatabaseEntries = new List(); - foreach (var xmlFile in xmlFiles) - { - using var fileStream = GameRepository.OpenFile(xmlFile); - - var parser = FileParserFactory.GetFileParser(listener); - Logger?.LogDebug($"Parsing File '{xmlFile}'"); - var parsedData = parser.ParseFile(fileStream)!; - parsedDatabaseEntries.Add(parsedData); - } - return CreateDatabase(parsedDatabaseEntries); - } - - protected abstract T CreateDatabase(IList parsedDatabaseEntries); -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/XmlDatabase.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/XmlDatabase.cs deleted file mode 100644 index 38d0a78..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/XmlDatabase.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; -using AnakinRaW.CommonUtilities.Collections; -using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.DataTypes; -using PG.StarWarsGame.Files.XML; - -namespace PG.StarWarsGame.Engine.Database; - -internal class XmlDatabase(IReadOnlyValueListDictionary parsedObjects, IServiceProvider serviceProvider) : IXmlDatabase - where T : XmlObject -{ - - private readonly IServiceProvider _serviceProvider = - serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - - private readonly IReadOnlyValueListDictionary _parsedObjects = parsedObjects ?? throw new ArgumentNullException(nameof(parsedObjects)); - - public ICollection Entries => _parsedObjects.Values; - - public ICollection EntryKeys => _parsedObjects.Keys; - - public ReadOnlyFrugalList GetEntries(Crc32 key) - { - return _parsedObjects.GetValues(key); - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/FocHardcodedConstants.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/FocHardcodedConstants.cs index 72996da..d7eb4af 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/FocHardcodedConstants.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/FocHardcodedConstants.cs @@ -5,7 +5,7 @@ namespace PG.StarWarsGame.Engine; public static class FocHardcodedConstants { /// - /// These models are hardcoded into StarWarsG.exe. + /// These models / particles are hardcoded into StarWarsG.exe. /// public static IList HardcodedModels { get; } = new List { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/GameConstants.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/GameConstants.cs new file mode 100644 index 0000000..508323c --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/GameConstants.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PG.StarWarsGame.Engine.Database; +using PG.StarWarsGame.Engine.Database.ErrorReporting; +using PG.StarWarsGame.Engine.IO.Repositories; + +namespace PG.StarWarsGame.Engine.GameConstants; + +internal class GameConstants(GameRepository repository, DatabaseErrorListenerWrapper errorListener, IServiceProvider serviceProvider) + : GameManagerBase(repository, errorListener, serviceProvider), IGameConstants +{ + protected override Task InitializeCoreAsync(CancellationToken token) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/GameConstantsXml.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/GameConstantsXml.cs new file mode 100644 index 0000000..e98ed52 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/GameConstantsXml.cs @@ -0,0 +1,3 @@ +namespace PG.StarWarsGame.Engine.GameConstants; + +public class GameConstantsXml; \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/IGameConstants.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/IGameConstants.cs new file mode 100644 index 0000000..552e406 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/IGameConstants.cs @@ -0,0 +1,3 @@ +namespace PG.StarWarsGame.Engine.GameConstants; + +public interface IGameConstants; \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObject.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObject.cs new file mode 100644 index 0000000..de7ed01 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObject.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using PG.Commons.Hashing; +using PG.StarWarsGame.Engine.Xml; +using PG.StarWarsGame.Files.XML; + +namespace PG.StarWarsGame.Engine.GameObjects; + +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); + } + + public string Type { get; } + + public GameObjectType EstimatedType { get; } + + public string? GalacticModel { get; internal set; } + + public string? DestroyedGalacticModel { get; internal set; } + + public string? LandModel { get; internal set; } + + public string? SpaceModel { get; internal set; } + + public string? TacticalModel { get; internal set; } + + public string? GalacticFleetOverrideModel { get; internal set; } + + public string? GuiModel { get; internal set; } + + public string? ModelName { get; internal set; } + + public string? LandAnimOverrideModel { get; internal set; } + + public string? XxxSpaceModeModel { get; internal set; } + + public string? DamagedSmokeAssetModel { get; internal set; } + + public IReadOnlyDictionary LandTerrainModelMapping { get; } + + internal Dictionary InternalLandTerrainModelMapping { get; } = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Gets all model files (including particles) the game object references. + /// + public IEnumerable Models + { + 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; + } + + } + + private static void AddNotEmpty(ISet set, string? value, Predicate? predicate = null) + { + if (value is null) + return; + if (predicate is null || predicate(value)) + set.Add(value); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/GameObjectType.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectType.cs similarity index 95% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/GameObjectType.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectType.cs index 116fa29..0b84517 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/GameObjectType.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectType.cs @@ -1,4 +1,4 @@ -namespace PG.StarWarsGame.Engine.DataTypes; +namespace PG.StarWarsGame.Engine.GameObjects; public enum GameObjectType { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectTypeTypeGameManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectTypeTypeGameManager.cs new file mode 100644 index 0000000..8b0abb1 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectTypeTypeGameManager.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine.Database; +using PG.StarWarsGame.Engine.Database.ErrorReporting; +using PG.StarWarsGame.Engine.IO.Repositories; +using PG.StarWarsGame.Engine.Xml.Parsers; + +namespace PG.StarWarsGame.Engine.GameObjects; + +internal class GameObjectTypeTypeGameManager(GameRepository repository, DatabaseErrorListenerWrapper errorListener, IServiceProvider serviceProvider) + : GameManagerBase(repository, errorListener, serviceProvider), IGameObjectTypeGameManager +{ + protected override async Task InitializeCoreAsync(CancellationToken token) + { + Logger?.LogInformation("Parsing GameObjects..."); + + var contentParser = ServiceProvider.GetRequiredService(); + + await Task.Run(() => contentParser.ParseEntriesFromContainerXml( + "DATA\\XML\\GAMEOBJECTFILES.XML", + ErrorListener, + GameRepository, + ".\\DATA\\XML", + NamedEntries, + VerifyFilePathLength), token); + } + + private void VerifyFilePathLength(string filePath) + { + if (filePath.Length > PGConstants.MaxGameObjectDatabaseFileName) + { + ErrorListener.OnInitializationError(new InitializationError + { + GameManager = ToString(), + Message = $"Game object file '{filePath}' is longer than {PGConstants.MaxGameObjectDatabaseFileName} characters." + }); + } + } +} \ 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 new file mode 100644 index 0000000..ae22ca1 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/IGameObjectTypeGameManager.cs @@ -0,0 +1,5 @@ +using PG.StarWarsGame.Engine.Database; + +namespace PG.StarWarsGame.Engine.GameObjects; + +public interface IGameObjectTypeGameManager : IGameManager; \ 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 new file mode 100644 index 0000000..f1ffb04 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/ComponentTextureEntry.cs @@ -0,0 +1,10 @@ +namespace PG.StarWarsGame.Engine.GuiDialog; + +public readonly struct ComponentTextureEntry(GuiComponentType componentType, string texture, bool isOverride) +{ + public string Texture { get; } = texture; + + public GuiComponentType ComponentType { get; } = componentType; + + public bool IsOverride { get; } = isOverride; +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiComponentType.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiComponentType.cs new file mode 100644 index 0000000..548cfee --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiComponentType.cs @@ -0,0 +1,93 @@ +namespace PG.StarWarsGame.Engine.GuiDialog; + +public enum GuiComponentType +{ + ButtonLeft = 0x0, + ButtonMiddle = 0x1, + ButtonRight = 0x2, + ButtonLeftMouseOver = 0x3, + ButtonMiddleMouseOver = 0x4, + ButtonRightMouseOver = 0x5, + ButtonLeftPressed = 0x6, + ButtonMiddlePressed = 0x7, + ButtonRightPressed = 0x8, + ButtonLeftDisabled = 0x9, + ButtonMiddleDisabled = 0xA, + ButtonRightDisabled = 0xB, + CheckOff = 0xC, + CheckOn = 0xD, + DialLeft = 0xE, + DialMiddle = 0xF, + DialRight = 0x10, + DialPlus = 0x11, + DialPlusMouseOver = 0x12, + DialPlusPressed = 0x13, + DialMinus = 0x14, + DialMinusMouseOver = 0x15, + DialMinusPressed = 0x16, + DialTab = 0x17, + FrameBottom = 0x18, + FrameBottomLeft = 0x19, + FrameBottomRight = 0x1A, + FrameBackground = 0x1B, + FrameLeft = 0x1C, + FrameRight = 0x1D, + FrameTop = 0x1E, + FrameTopLeft = 0x1F, + FrameTopRight = 0x20, + FrameTopTransitionLeft = 0x21, + FrameTopTransitionRight = 0x22, + FrameBottomTransitionLeft = 0x23, + FrameBottomTransitionRight = 0x24, + FrameLeftTransitionTop = 0x25, + FrameLeftTransitionBottom = 0x26, + FrameRightTransitionTop = 0x27, + FrameRightTransitionBottom = 0x28, + RadioOff = 0x29, + RadioOn = 0x2A, + RadioDisabled = 0x2B, + RadioMouseOver = 0x2C, + ScrollDownButton = 0x2D, + ScrollDownButtonPressed = 0x2E, + ScrollDownButtonMouseOver = 0x2F, + ScrollMiddle = 0x30, + ScrollTab = 0x31, + ScrollUpButton = 0x32, + ScrollUpButtonPressed = 0x33, + ScrollUpButtonMouseOver = 0x34, + ScrollUpButtonDisabled = 0x35, + ScrollDownButtonDisabled = 0x36, + ScrollMiddleDisabled = 0x37, + ScrollTabDisabled = 0x38, + TrackbarScrollDownButton = 0x39, + TrackbarScrollDownButtonPressed = 0x3A, + TrackbarScrollDownButtonMouseOver = 0x3B, + TrackbarScrollMiddle = 0x3C, + TrackbarScrollTab = 0x3D, + TrackbarScrollUpButton = 0x3E, + TrackbarScrollUpButtonPressed = 0x3F, + TrackbarScrollUpButtonMouseOver = 0x40, + TrackbarScrollUpButtonDisabled = 0x41, + TrackbarScrollDownButtonDisabled = 0x42, + TrackbarScrollMiddleDisabled = 0x43, + TrackbarScrollTabDisabled = 0x44, + SmallFrameBottom = 0x45, + SmallFrameBottomLeft = 0x46, + SmallFrameBottomRight = 0x47, + SmallFrameMiddleLeft = 0x48, + SmallFrameMiddleRight = 0x49, + SmallFrameTop = 0x4A, + SmallFrameTopLeft = 0x4B, + SmallFrameTopRight = 0x4C, + SmallFrameBackground = 0x4D, + ComboboxPopdown = 0x4E, + ComboboxPopdownMouseOver = 0x4F, + ComboboxPopdownPressed = 0x50, + ComboboxTextBox = 0x51, + ComboboxLeftCap = 0x52, + ProgressLeft = 0x53, + ProgressMiddleOff = 0x54, + ProgressMiddleOn = 0x55, + ProgressRight = 0x56, + Scanlines = 0x57, +} \ 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 new file mode 100644 index 0000000..5f20f0f --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PG.Commons.Hashing; +using PG.StarWarsGame.Engine.Database; +using PG.StarWarsGame.Engine.Database.ErrorReporting; +using PG.StarWarsGame.Engine.GuiDialog.Xml; +using PG.StarWarsGame.Engine.IO.Repositories; +using PG.StarWarsGame.Files.MTD.Binary; +using PG.StarWarsGame.Files.MTD.Files; +using PG.StarWarsGame.Files.MTD.Services; + +namespace PG.StarWarsGame.Engine.GuiDialog; + +internal partial class GuiDialogGameManager(GameRepository repository, DatabaseErrorListenerWrapper errorListener, IServiceProvider serviceProvider) + : GameManagerBase(repository, errorListener, 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 IMtdFile? _megaTexture; + private GuiDialogsXml? _guiDialogsXml; + private string? _megaTextureFileName; + private bool _megaTextureExists; + + + public IMtdFile? MtdFile + { + get + { + ThrowIfNotInitialized(); + return _megaTexture; + } + private set + { + ThrowIfAlreadyInitialized(); + _megaTexture = value; + } + } + + public GuiDialogsXml? GuiDialogsXml + { + get + { + ThrowIfNotInitialized(); + return _guiDialogsXml; + } + private set + { + ThrowIfAlreadyInitialized(); + _guiDialogsXml = value; + } + } + + public IReadOnlyCollection Components + { + get + { + ThrowIfNotInitialized(); + return _perComponentTextures.Keys; + } + } + + public IReadOnlyDictionary DefaultTextureEntries + { + get + { + ThrowIfNotInitialized(); + return _defaultTexturesRo; + } + } + + + public IReadOnlyDictionary GetTextureEntries(string component, out bool componentExist) + { + if (!_perComponentTextures.TryGetValue(component, out var textures)) + { + Logger?.LogDebug($"The component '{component}' has no overrides. Using default textures."); + componentExist = false; + return DefaultTextureEntries; + } + + componentExist = true; + return new ReadOnlyDictionary(textures); + } + + public bool TryGetTextureEntry(string component, GuiComponentType key, out ComponentTextureEntry texture) + { + if (!_perComponentTextures.TryGetValue(component, out var textures)) + { + Logger?.LogDebug($"The component '{component}' has no overrides. Using default textures."); + textures = _defaultTextures; + } + + return textures.TryGetValue(key, out texture); + } + + public bool TextureExists( + in ComponentTextureEntry textureInfo, + out GuiTextureOrigin textureOrigin, + out bool isNone, + bool buttonMiddleInRepoMode = false) + { + if (textureInfo.Texture == "none") + { + textureOrigin = default; + isNone = true; + return false; + } + + isNone = false; + + // Apparently, Scanlines only use the repository and not the MTD. + if (textureInfo.ComponentType == GuiComponentType.Scanlines) + { + textureOrigin = GuiTextureOrigin.Repository; + return GameRepository.TextureRepository.FileExists(textureInfo.Texture); + } + + // The engine uses ButtonMiddle to switch to the special button mode. + // It searches first in the repo and then falls back to MTD + // (but only for this very type; the variants do not fallback to MTD). + if (textureInfo.ComponentType == GuiComponentType.ButtonMiddle) + { + if (GameRepository.TextureRepository.FileExists(textureInfo.Texture)) + { + textureOrigin = GuiTextureOrigin.Repository; + return true; + } + } + + // The engine does not fallback to MTD once it is in this special Button mode. + if (buttonMiddleInRepoMode && textureInfo.ComponentType is + GuiComponentType.ButtonMiddleDisabled or + GuiComponentType.ButtonMiddleMouseOver or + GuiComponentType.ButtonMiddlePressed) + { + textureOrigin = GuiTextureOrigin.Repository; + return GameRepository.TextureRepository.FileExists(textureInfo.Texture); + } + + if (textureInfo.Texture.Length <= 63 && MtdFile is not null && _megaTextureExists) + { + var crc32 = _hashingService.GetCrc32Upper(textureInfo.Texture.AsSpan(), MtdFileConstants.NameEncoding); + if (MtdFile.Content.Contains(crc32)) + { + textureOrigin = GuiTextureOrigin.MegaTexture; + return true; + } + } + + // The background image for frames include a fallback the repository. + if (textureInfo.ComponentType == GuiComponentType.FrameBackground) + { + textureOrigin = GuiTextureOrigin.Repository; + return GameRepository.FileExists(textureInfo.Texture); + } + + textureOrigin = default; + return false; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager_Initialization.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager_Initialization.cs new file mode 100644 index 0000000..b96c4d7 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager_Initialization.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AnakinRaW.CommonUtilities.Collections; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine.Database.ErrorReporting; +using PG.StarWarsGame.Engine.GuiDialog.Xml; +using PG.StarWarsGame.Engine.Xml; +using PG.StarWarsGame.Engine.Xml.Tags; + +namespace PG.StarWarsGame.Engine.GuiDialog; + +partial class GuiDialogGameManager +{ + public const int MegaTextureMaxFilePathLength = 255; + + protected override Task InitializeCoreAsync(CancellationToken token) + { + return Task.Run(() => + { + var parserFactory = ServiceProvider.GetRequiredService(); + var guiDialogParser = parserFactory.CreateFileParser(); + + _defaultTexturesRo = new ReadOnlyDictionary(_defaultTextures); + + Logger?.LogInformation("Parsing GuiDialogs..."); + using var fileStream = GameRepository.TryOpenFile("DATA\\XML\\GUIDIALOGS.XML"); + + if (fileStream is null) + { + ErrorListener.OnInitializationError(new InitializationError + { + GameManager = ToString(), + Message = "Unable to find GuiDialogs.xml" + }); + return; + } + + var guiDialogs = guiDialogParser.ParseFile(fileStream); + if (guiDialogs is null) + { + ErrorListener.OnInitializationError(new InitializationError + { + GameManager = ToString(), + Message = "Unable to parse GuiDialogs.xml" + }); + return; + } + + GuiDialogsXml = guiDialogs; + + InitializeTextures(guiDialogs.TextureData, ErrorListener); + + }, token); + } + + private void InitializeTextures(GuiDialogsXmlTextureData textureData, DatabaseErrorListenerWrapper errorListener) + { + InitializeMegaTextures(textureData, ErrorListener); + + var textures = textureData.Textures; + + if (textures.Count == 0) + { + errorListener.OnInitializationError(new InitializationError + { + GameManager = ToString(), + Message = "No Textures defined in GuiDialogs.xml" + }); + } + 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); + + ReportInvalidComponent(in invalidKeys); + } + + foreach (var componentTextureData in textures.Skip(1)) + { + // The game only uses the *first* entry. + if (_perComponentTextures.ContainsKey(componentTextureData.Component)) + continue; + + _perComponentTextures.Add(componentTextureData.Component, InitializeComponentTextures(componentTextureData, false, out var invalidKeys)); + ReportInvalidComponent(in invalidKeys); + } + } + + private Dictionary InitializeComponentTextures(XmlComponentTextureData textureData, bool isDefaultComponent, out FrugalList invalidKeys) + { + invalidKeys = new FrugalList(); + + var result = new Dictionary(); + + if (!isDefaultComponent) + { + // This assumes that _defaultTextures is already filled + foreach (var key in _defaultTextures.Keys) + result.Add(key, _defaultTextures[key]); + } + + + foreach (var keyText in textureData.Textures.Keys) + { + if (!ComponentTextureKeyExtensions.TryConvertToKey(keyText.AsSpan(), out var key)) + { + invalidKeys.Add(keyText); + continue; + } + + var textureValue = textureData.Textures.GetLastValue(keyText); + result[key] = new ComponentTextureEntry(key, textureValue, !isDefaultComponent); + } + + return result; + } + + private void InitializeMegaTextures(GuiDialogsXmlTextureData guiDialogs, DatabaseErrorListenerWrapper errorListener) + { + if (guiDialogs.MegaTexture is null) + { + errorListener.OnInitializationError(new InitializationError + { + GameManager = ToString(), + Message = "MtdFile is not defined in GuiDialogs.xml" + }); + } + else + { + var mtdPath = FileSystem.Path.Combine("DATA\\ART\\TEXTURES", $"{guiDialogs.MegaTexture}.mtd"); + + if (mtdPath.Length > MegaTextureMaxFilePathLength) + { + errorListener.OnInitializationError(new InitializationError + { + GameManager = ToString(), + Message = $"Mtd file path is longer than {MegaTextureMaxFilePathLength}." + }); + } + + using var megaTexture = GameRepository.TryOpenFile(mtdPath); + MtdFile = megaTexture is null ? null : _mtdFileService.Load(megaTexture); + } + + if (guiDialogs.CompressedMegaTexture is null) + { + errorListener.OnInitializationError(new InitializationError + { + GameManager = ToString(), + Message = "CompressedMegaTexture is not defined in GuiDialogs.xml" + }); + } + + + // TODO: Support using the correct texture based on desired low-RAM flag + _megaTextureFileName = guiDialogs.MegaTexture; + var textureFileNameWithExtension = $"{guiDialogs.MegaTexture}.tga"; + _megaTextureExists = GameRepository.TextureRepository.FileExists(textureFileNameWithExtension); + + if (textureFileNameWithExtension.Length > MegaTextureMaxFilePathLength) + { + errorListener.OnInitializationError(new InitializationError + { + GameManager = ToString(), + Message = $"MegaTexture file path is longer than {MegaTextureMaxFilePathLength}." + }); + } + } + + private void ReportInvalidComponent(in FrugalList invalidKeys) + { + if (invalidKeys.Count == 0) + return; + + ErrorListener.OnInitializationError(new InitializationError + { + GameManager = ToString(), + Message = $"The following XML keys are not valid to describe a GUI component: {string.Join(",", invalidKeys)}" + }); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiTextureOrigin.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiTextureOrigin.cs new file mode 100644 index 0000000..751afea --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiTextureOrigin.cs @@ -0,0 +1,7 @@ +namespace PG.StarWarsGame.Engine.GuiDialog; + +public enum GuiTextureOrigin +{ + MegaTexture, + Repository +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/IGuiDialogManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/IGuiDialogManager.cs new file mode 100644 index 0000000..1ac7632 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/IGuiDialogManager.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using PG.StarWarsGame.Engine.GuiDialog.Xml; +using PG.StarWarsGame.Files.MTD.Files; + +namespace PG.StarWarsGame.Engine.GuiDialog; + +public interface IGuiDialogManager +{ + IMtdFile? MtdFile { get; } + + GuiDialogsXml? GuiDialogsXml { get; } + + IReadOnlyCollection Components { get; } + + IReadOnlyDictionary DefaultTextureEntries { get; } + + IReadOnlyDictionary GetTextureEntries(string component, out bool componentExist); + + bool TryGetTextureEntry(string component, GuiComponentType key, out ComponentTextureEntry texture); + + bool TextureExists( + in ComponentTextureEntry textureInfo, + out GuiTextureOrigin textureOrigin, + out bool isNone, + bool buttonMiddleInRepoMode = false); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/TypeBasedComponentTextureEntryComparer.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/TypeBasedComponentTextureEntryComparer.cs new file mode 100644 index 0000000..eda572e --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/TypeBasedComponentTextureEntryComparer.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace PG.StarWarsGame.Engine.GuiDialog; + +public sealed class TypeBasedComponentTextureEntryComparer : IEqualityComparer +{ + public static readonly TypeBasedComponentTextureEntryComparer Instance = new(); + + private TypeBasedComponentTextureEntryComparer() + { + } + + public bool Equals(ComponentTextureEntry x, ComponentTextureEntry y) + { + return x.ComponentType.Equals(y.ComponentType); + } + + public int GetHashCode(ComponentTextureEntry obj) + { + return obj.ComponentType.GetHashCode(); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/GuiDialogsXml.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/GuiDialogsXml.cs new file mode 100644 index 0000000..72cc5a0 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/GuiDialogsXml.cs @@ -0,0 +1,9 @@ +using PG.StarWarsGame.Engine.Xml; +using PG.StarWarsGame.Files.XML; + +namespace PG.StarWarsGame.Engine.GuiDialog.Xml; + +public class GuiDialogsXml(GuiDialogsXmlTextureData textureData, XmlLocationInfo location) : XmlObject(location) +{ + public GuiDialogsXmlTextureData TextureData { get; } = textureData; +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/GuiDialogsXmlTextureData.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/GuiDialogsXmlTextureData.cs new file mode 100644 index 0000000..6496417 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/GuiDialogsXmlTextureData.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Linq; +using PG.StarWarsGame.Engine.Xml; +using PG.StarWarsGame.Files.XML; + +namespace PG.StarWarsGame.Engine.GuiDialog.Xml; + +public class GuiDialogsXmlTextureData(IEnumerable textures, XmlLocationInfo location) : XmlObject(location) +{ + public string? MegaTexture { get; init; } + + public string? CompressedMegaTexture { get; init; } + + public IReadOnlyList Textures { get; } = textures.ToList(); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/XmlComponentTextureData.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/XmlComponentTextureData.cs new file mode 100644 index 0000000..2ed95ce --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/XmlComponentTextureData.cs @@ -0,0 +1,14 @@ +using System; +using PG.Commons.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) + : XmlObject(location) +{ + public string Component { get; } = componentId ?? throw new ArgumentNullException(componentId); + + public IReadOnlyValueListDictionary Textures { get; } = textures ?? throw new ArgumentNullException(nameof(textures)); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/EffectsRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/EffectsRepository.cs new file mode 100644 index 0000000..32e08dd --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/EffectsRepository.cs @@ -0,0 +1,103 @@ +using System; +using PG.StarWarsGame.Engine.IO.Utilities; +using PG.StarWarsGame.Engine.Utilities; + +namespace PG.StarWarsGame.Engine.IO.Repositories; + +internal class EffectsRepository(GameRepository baseRepository, IServiceProvider serviceProvider) : MultiPassRepository(baseRepository, serviceProvider) +{ + private static readonly string[] LookupPaths = + [ + "DATA\\ART\\SHADERS", + "DATA\\ART\\SHADERS\\TERRAIN", + // This path is not coded to the engine + "DATA\\ART\\SHADERS\\ENGINE", + ]; + + // The engine does not support ".fxh" as a shader lookup, but as there might be some pre-compiling going on, this should be OK. + private static readonly string[] ShaderExtensions = [".fx", ".fxo", ".fxh"]; + + private protected override FileFoundInfo MultiPassAction( + ReadOnlySpan filePath, + ref ValueStringBuilder reusableStringBuilder, + ref ValueStringBuilder destination, + bool megFileOnly) + { + var strippedName = StripFileName(filePath); + + if (strippedName.Length > PGConstants.MaxEffectFileName) + return default; + + foreach (var ext in ShaderExtensions) + { + var extSpan = ext.AsSpan(); + + + var fileFoundInfo = FindEffect( + strippedName, + extSpan, + ReadOnlySpan.Empty, + ref reusableStringBuilder, + ref destination); + + if (fileFoundInfo.FileFound) + return fileFoundInfo; + + + foreach (var directory in LookupPaths) + { + fileFoundInfo = FindEffect( + strippedName, + extSpan, + directory.AsSpan(), + ref reusableStringBuilder, + ref destination); + + if (fileFoundInfo.FileFound) + return fileFoundInfo; + } + } + + + return default; + } + + private FileFoundInfo FindEffect( + ReadOnlySpan strippedName, + ReadOnlySpan extension, + ReadOnlySpan directory, + ref ValueStringBuilder multiPassStringBuilder, + ref ValueStringBuilder filePathStringBuilder) + { + multiPassStringBuilder.Length = 0; + + if (directory != ReadOnlySpan.Empty) + FileSystem.Path.Join(directory, strippedName, ref multiPassStringBuilder); + else + multiPassStringBuilder.Append(strippedName); + + multiPassStringBuilder.Append(extension); + + if (multiPassStringBuilder.Length > PGConstants.MaxEffectFileName) + return default; + + return BaseRepository.FindFile(multiPassStringBuilder.AsSpan(), ref filePathStringBuilder); + } + + private static ReadOnlySpan StripFileName(ReadOnlySpan src) + { + var destination = src; + + for (var i = src.Length - 1; i >= 0; --i) + { + + if (src[i] == '.') + destination = src.Slice(0, i); + + if (src[i] == '/' || src[i] == '\\') + break; + } + + return destination; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/FileFoundInfo.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/FileFoundInfo.cs new file mode 100644 index 0000000..eb1af79 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/FileFoundInfo.cs @@ -0,0 +1,27 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using PG.StarWarsGame.Files.MEG.Data.Entries; + +namespace PG.StarWarsGame.Engine.IO.Repositories; + +internal readonly ref struct FileFoundInfo +{ + public bool FileFound => FilePath != ReadOnlySpan.Empty || InMeg; + + public ReadOnlySpan FilePath { get; } + + public MegDataEntryReference? MegDataEntryReference { get; } + + [MemberNotNullWhen(true, nameof(MegDataEntryReference))] + public bool InMeg => MegDataEntryReference is not null; + + public FileFoundInfo(ReadOnlySpan filePath) + { + FilePath = filePath; + } + + public FileFoundInfo(MegDataEntryReference? megDataReference) + { + MegDataEntryReference = megDataReference; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/FocGameRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/FocGameRepository.cs similarity index 56% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/FocGameRepository.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/FocGameRepository.cs index a36382c..6e16e59 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/FocGameRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/FocGameRepository.cs @@ -1,18 +1,19 @@ using System; using System.Collections.Generic; using System.Linq; +using PG.StarWarsGame.Engine.Database.ErrorReporting; +using PG.StarWarsGame.Engine.Utilities; using PG.StarWarsGame.Files.MEG.Files; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; -namespace PG.StarWarsGame.Engine.Repositories; +namespace PG.StarWarsGame.Engine.IO.Repositories; // EaW file lookup works slightly different! internal class FocGameRepository : GameRepository { public override GameEngineType EngineType => GameEngineType.Foc; - public FocGameRepository(GameLocations gameLocations, IXmlParserErrorListener? listener, IServiceProvider serviceProvider) : base(gameLocations, listener, serviceProvider) + public FocGameRepository(GameLocations gameLocations, DatabaseErrorListenerWrapper errorListener, IServiceProvider serviceProvider) + : base(gameLocations, errorListener, serviceProvider) { if (gameLocations == null) throw new ArgumentNullException(nameof(gameLocations)); @@ -52,49 +53,31 @@ public FocGameRepository(GameLocations gameLocations, IXmlParserErrorListener? l AddMegFiles(megsToConsider); } - - - protected override T? RepositoryFileLookup(string filePath, Func> pathAction, Func> megAction, bool megFileOnly, T? defaultValue = default) where T: default + + protected internal override FileFoundInfo FindFile(ReadOnlySpan filePath, ref ValueStringBuilder pathStringBuilder, bool megFileOnly = false) { if (!megFileOnly) { - foreach (var modPath in ModPaths) - { - var modFilePath = FileSystem.Path.Combine(modPath, filePath); + var fileFoundInfo = FileFromAltExists(filePath, ModPaths, ref pathStringBuilder); + if (fileFoundInfo.FileFound) + return fileFoundInfo; - var result = pathAction(modFilePath); - if (result.ShallReturn) - return result.Result; - } - - { - var normalFilePath = FileSystem.Path.Combine(GameDirectory, filePath); - var result = pathAction(normalFilePath); - if (result.ShallReturn) - return result.Result; - } - + fileFoundInfo = FindFileCore(filePath, ref pathStringBuilder); + if (fileFoundInfo.FileFound) + return fileFoundInfo; } + if (MasterMegArchive is not null) { - var result = megAction(filePath); - if (result.ShallReturn) - return result.Result; + var fileFoundInfo = GetFileInfoFromMasterMeg(filePath); + if (fileFoundInfo.InMeg) + return fileFoundInfo; } if (!megFileOnly) - { - foreach (var fallbackPath in FallbackPaths) - { - var fallbackFilePath = FileSystem.Path.Combine(fallbackPath, filePath); - - var result = pathAction(fallbackFilePath); - if (result.ShallReturn) - return result.Result; - } - } + return FileFromAltExists(filePath, FallbackPaths, ref pathStringBuilder); - return defaultValue; + return default; } } \ 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 new file mode 100644 index 0000000..8b151c9 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +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; + +internal partial class GameRepository +{ + private static readonly string[] DataPathPrefixes = ["DATA/", "DATA\\", "./DATA/", ".\\DATA\\"]; + + public bool FileExists(string filePath, string[] extensions, bool megFileOnly = false) + { + foreach (var extension in extensions) + { + var newPath = FileSystem.Path.ChangeExtension(filePath, extension); + if (FileExists(newPath, megFileOnly)) + return true; + } + return false; + } + + public bool FileExists(string filePath, bool megFileOnly = false) + { + return FileExists(filePath.AsSpan(), megFileOnly); + } + + public bool FileExists(ReadOnlySpan filePath, bool megFileOnly = false) + { + var sb = new ValueStringBuilder(stackalloc char[PGConstants.MaxMegEntryPathLength]); + var fileFound = FindFile(filePath, ref sb, megFileOnly); + var fileExists = fileFound.FileFound; + sb.Dispose(); + return fileExists; + } + + public Stream OpenFile(string filePath, bool megFileOnly = false) + { + return OpenFile(filePath.AsSpan(), megFileOnly); + } + + + public Stream OpenFile(ReadOnlySpan filePath, bool megFileOnly = false) + { + var stream = TryOpenFile(filePath, megFileOnly); + if (stream is null) + throw new FileNotFoundException($"Unable to find game data: {filePath.ToString()}"); + return stream; + } + + public Stream? TryOpenFile(string filePath, bool megFileOnly = false) + { + return TryOpenFile(filePath.AsSpan(), megFileOnly); + } + + public Stream? TryOpenFile(ReadOnlySpan filePath, bool megFileOnly) + { + var sb = new ValueStringBuilder(stackalloc char[PGConstants.MaxMegEntryPathLength]); + var fileFoundInfo = FindFile(filePath, ref sb, megFileOnly); + var fileStream = OpenFileCore(fileFoundInfo); + sb.Dispose(); + return fileStream; + } + + protected internal abstract FileFoundInfo FindFile(ReadOnlySpan filePath, + ref ValueStringBuilder pathStringBuilder, bool megFileOnly = false); + + protected FileFoundInfo GetFileInfoFromMasterMeg(ReadOnlySpan filePath) + { + Debug.Assert(MasterMegArchive is not null); + + if (filePath.Length > PGConstants.MaxMegEntryPathLength) + { + Logger.LogWarning($"Trying to open a MEG entry which is longer than 259 characters: '{filePath.ToString()}'"); + return default; + } + + Span fileNameSpan = stackalloc char[PGConstants.MaxMegEntryPathLength]; + + if (!_megPathNormalizer.TryNormalize(filePath, fileNameSpan, out var length)) + return default; + + var fileName = fileNameSpan.Slice(0, length); + + if (fileName.Length > PGConstants.MaxMegEntryPathLength) + { + Logger.LogWarning($"Trying to open a MEG entry which is longer than 259 characters after normalization: '{fileName.ToString()}'"); + return default; + } + + var crc = _crc32HashingService.GetCrc32(fileName, MegFileConstants.MegDataEntryPathEncoding); + + var entry = MasterMegArchive!.FirstEntryWithCrc(crc); + + return new FileFoundInfo(entry); + } + + protected FileFoundInfo FindFileCore(ReadOnlySpan filePath, ref ValueStringBuilder stringBuilder) + { + bool exists; + + stringBuilder.Length = 0; + + if (FileSystem.Path.IsPathFullyQualified(filePath)) + stringBuilder.Append(filePath); + else + FileSystem.Path.Join(GameDirectory.AsSpan(), filePath, ref stringBuilder); + + var actualFilePath = stringBuilder.AsSpan(); + + // We accept a *possible* difference here between platforms, + // unless it's proven the differences are too significant. + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + exists = FileSystem.File.Exists(actualFilePath.ToString()); + else + { + // We *could* also use the slightly faster GetFileAttributesA. + // However, CreateFileA and GetFileAttributesA are implemented complete independent. + // The game uses CreateFileA. + // Thus, we should stick to what the game uses in order to be as close to the engine as possible + // NB: It's also important that the string builder is zero-terminated, as otherwise CreateFileA might get invalid data. + var fileHandle = CreateFile( + in stringBuilder.GetPinnableReference(true), + FileAccess.Read, + FileShare.Read, + IntPtr.Zero, + FileMode.Open, + FileAttributes.Normal, IntPtr.Zero); + + exists = IsValidAndClose(fileHandle); + } + return !exists ? new FileFoundInfo() : new FileFoundInfo(actualFilePath); + } + + protected FileFoundInfo FileFromAltExists(ReadOnlySpan filePath, IList fallbackPaths, ref ValueStringBuilder pathStringBuilder) + { + if (fallbackPaths.Count == 0) + return default; + if (!PathStartsWithDataDirectory(filePath, out var prefixLength)) + return default; + + var pathWithNormalizedData = filePath.Slice(prefixLength); + + foreach (var fallbackPath in fallbackPaths) + { + pathStringBuilder.Length = 0; + + FileSystem.Path.Join(fallbackPath.AsSpan(), pathWithNormalizedData, ref pathStringBuilder); + var newPath = pathStringBuilder.AsSpan(); + + var fileFoundInfo = FindFileCore(newPath, ref pathStringBuilder); + if (fileFoundInfo.FileFound) + return fileFoundInfo; + } + + return default; + } + + private static bool PathStartsWithDataDirectory(ReadOnlySpan path, out int cutoffLength) + { + cutoffLength = 0; + if (path.Length < 5) + return false; + foreach (var prefix in DataPathPrefixes) + { + if (path.StartsWith(prefix.AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + if (path[0] == '.') + cutoffLength = 2; + return true; + } + } + return false; + } + + internal Stream? OpenFileCore(FileFoundInfo fileFoundInfo) + { + if (!fileFoundInfo.FileFound) + return null; + + if (fileFoundInfo.InMeg) + return _megExtractor.GetFileData(fileFoundInfo.MegDataEntryReference.Location); + + return FileSystem.FileStream.New(fileFoundInfo.FilePath.ToString(), FileMode.Open, FileAccess.Read, FileShare.Read); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsValidAndClose(IntPtr handle) + { + var isValid = handle != IntPtr.Zero && handle != new IntPtr(-1); + if (isValid) + CloseHandle(handle); + return isValid; + } + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern IntPtr CreateFile( + in char lpFileName, + [MarshalAs(UnmanagedType.U4)] FileAccess access, + [MarshalAs(UnmanagedType.U4)] FileShare share, + IntPtr securityAttributes, + [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition, + [MarshalAs(UnmanagedType.U4)] FileAttributes flagsAndAttributes, + IntPtr templateFile); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CloseHandle(IntPtr hObject); +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.cs similarity index 62% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.cs index 48c99f0..cd6f68a 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.cs @@ -1,17 +1,14 @@ using System; using System.Collections.Generic; -using System.IO; using System.IO.Abstractions; using System.Linq; -using System.Runtime.InteropServices; using AnakinRaW.CommonUtilities.FileSystem; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.FileSystemGlobbing; using Microsoft.Extensions.Logging; using PG.Commons.Hashing; using PG.Commons.Services; -using PG.StarWarsGame.Engine.Language; -using PG.StarWarsGame.Engine.Utilities; +using PG.StarWarsGame.Engine.Database.ErrorReporting; +using PG.StarWarsGame.Engine.Localization; using PG.StarWarsGame.Engine.Xml; using PG.StarWarsGame.Files.MEG.Data.Archives; using PG.StarWarsGame.Files.MEG.Data.Entries; @@ -20,11 +17,10 @@ using PG.StarWarsGame.Files.MEG.Services; using PG.StarWarsGame.Files.MEG.Services.Builder.Normalization; using PG.StarWarsGame.Files.XML; -using PG.StarWarsGame.Files.XML.ErrorHandling; -namespace PG.StarWarsGame.Engine.Repositories; +namespace PG.StarWarsGame.Engine.IO.Repositories; -internal abstract class GameRepository : ServiceBase, IGameRepository +internal abstract partial class GameRepository : ServiceBase, IGameRepository { private readonly IMegFileService _megFileService; private readonly IMegFileExtractor _megExtractor; @@ -32,7 +28,7 @@ internal abstract class GameRepository : ServiceBase, IGameRepository private readonly ICrc32HashingService _crc32HashingService; private readonly IVirtualMegArchiveBuilder _virtualMegBuilder; private readonly IGameLanguageManagerProvider _languageManagerProvider; - private readonly IXmlParserErrorListener? _xmlParserErrorListener; + private readonly DatabaseErrorListenerWrapper _errorListener; protected readonly string GameDirectory; @@ -51,7 +47,7 @@ internal abstract class GameRepository : ServiceBase, IGameRepository private readonly List _loadedMegFiles = new(); protected IVirtualMegArchive? MasterMegArchive { get; private set; } - protected GameRepository(GameLocations gameLocations, IXmlParserErrorListener? errorListener, IServiceProvider serviceProvider) : base(serviceProvider) + protected GameRepository(GameLocations gameLocations, DatabaseErrorListenerWrapper errorListener, IServiceProvider serviceProvider) : base(serviceProvider) { if (gameLocations == null) throw new ArgumentNullException(nameof(gameLocations)); @@ -60,9 +56,9 @@ protected GameRepository(GameLocations gameLocations, IXmlParserErrorListener? e _megFileService = serviceProvider.GetRequiredService(); _virtualMegBuilder = serviceProvider.GetRequiredService(); _crc32HashingService = serviceProvider.GetRequiredService(); - _megPathNormalizer = new PetroglyphDataEntryPathNormalizer(serviceProvider); + _megPathNormalizer = EmpireAtWarMegDataEntryPathNormalizer.Instance; _languageManagerProvider = serviceProvider.GetRequiredService(); - _xmlParserErrorListener = errorListener; + _errorListener = errorListener; foreach (var mod in gameLocations.ModPaths) { @@ -126,93 +122,6 @@ public void AddMegFile(string megFile) AddMegFiles([megArchive]); } - public bool FileExists(string filePath, bool megFileOnly = false) - { - return RepositoryFileLookup(filePath, fp => - { - if (FileSystem.File.Exists(fp)) - return new ActionResult(true, true); - return ActionResult.DoNotReturn; - }, - fp => - { - var entry = FindFileInMasterMeg(fp); - if (entry is not null) - return new ActionResult(true, true); - return ActionResult.DoNotReturn; - }, megFileOnly); - } - - public Stream? TryOpenFile(string filePath, bool megFileOnly = false) - { - return RepositoryFileLookup(filePath, fp => - { - if (FileSystem.File.Exists(fp)) - return new ActionResult(true, OpenFileRead(fp)); - return ActionResult.DoNotReturn; - }, - fp => - { - var entry = FindFileInMasterMeg(fp); - if (entry is not null) - return new ActionResult(true, _megExtractor.GetFileData(entry.Location)); - return ActionResult.DoNotReturn; - }, megFileOnly); - } - - - public Stream OpenFile(string filePath, bool megFileOnly = false) - { - var stream = TryOpenFile(filePath, megFileOnly); - if (stream is null) - throw new FileNotFoundException($"Unable to find game data: {filePath}"); - return stream; - } - - public bool FileExists(string filePath, string[] extensions, bool megFileOnly = false) - { - foreach (var extension in extensions) - { - var newPath = FileSystem.Path.ChangeExtension(filePath, extension); - if (FileExists(newPath, megFileOnly)) - return true; - } - return false; - } - - public IEnumerable FindFiles(string searchPattern, bool megFileOnly = false) - { - var files = new HashSet(); - - var matcher = new Matcher(); - matcher.AddInclude(searchPattern); - - RepositoryFileLookup(searchPattern, - pattern => - { - var path = pattern.AsSpan().TrimEnd(searchPattern.AsSpan()); - - var matcherResult = matcher.Execute(FileSystem, path.ToString()); - - foreach (var matchedFile in matcherResult.Files) - { - var normalizedFile = _megPathNormalizer.Normalize(matchedFile.Path); - files.Add(normalizedFile); - } - - return ActionResult.DoNotReturn; - }, - _ => - { - var foundFiles = MasterMegArchive!.FindAllEntries(searchPattern, true); - foreach (var x in foundFiles) - files.Add(x.FilePath); - - return ActionResult.DoNotReturn; - }, megFileOnly); - - return files; - } public bool IsLanguageInstalled(LanguageType language) { @@ -311,9 +220,11 @@ protected IList LoadMegArchivesFromXml(string lookupPath) return Array.Empty(); } - var parser = fileParserFactory.GetFileParser(_xmlParserErrorListener); + var parser = fileParserFactory.CreateFileParser(_errorListener); var megaFilesXml = parser.ParseFile(xmlStream); + if (megaFilesXml is null) + return []; var megs = new List(megaFilesXml.Files.Count); @@ -333,14 +244,14 @@ internal void Seal() _sealed = true; } - protected abstract T? RepositoryFileLookup(string filePath, Func> pathAction, - Func> megAction, bool megFileOnly, T? defaultValue = default); - protected IMegFile? LoadMegArchive(string megPath) { using var megFileStream = TryOpenFile(megPath); if (megFileStream is not FileSystemStream fileSystemStream) + { + Logger.LogWarning($"Unable to find MEG file '{megPath}'"); return null; + } var megFile = _megFileService.Load(fileSystemStream); @@ -350,63 +261,12 @@ internal void Seal() return megFile; } - protected MegDataEntryReference? FindFileInMasterMeg(string filePath) - { - Span fileNameBuffer = stackalloc char[PGConstants.MaxPathLength]; - - // TODO: Is the engine really "to-uppering" the input??? - var length = _megPathNormalizer.Normalize(filePath.AsSpan(), fileNameBuffer); - var fileName = fileNameBuffer.Slice(0, length); - var crc = _crc32HashingService.GetCrc32(fileName, PGConstants.PGCrc32Encoding); - - return MasterMegArchive?.FirstEntryWithCrc(crc); - } - - protected FileSystemStream OpenFileRead(string filePath) - { - if (!AllowOpenFile(filePath)) - throw new UnauthorizedAccessException("The data is not part of the Games!"); - return FileSystem.FileStream.New(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); - } - - private bool AllowOpenFile(string filePath) - { - foreach (var modPath in ModPaths) - { - if (FileSystem.Path.IsChildOf(modPath, filePath)) - return true; - } - - if (FileSystem.Path.IsChildOf(GameDirectory, filePath)) - return true; - - foreach (var fallbackPath in FallbackPaths) - { - if (FileSystem.Path.IsChildOf(fallbackPath, filePath)) - return true; - } - - return false; - } - private void ThrowIfSealed() { if (_sealed) throw new InvalidOperationException("The object is sealed for modifications"); } - - protected readonly struct ActionResult(bool shallReturn, T? result) - { - public T? Result { get; } = result; - - public bool ShallReturn { get; } = shallReturn; - - public static ActionResult DoNotReturn => default; - } - - [StructLayout(LayoutKind.Explicit)] - private readonly struct EmptyStruct; - + private sealed class LanguageFiles { public LanguageType Language { get; } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepositoryFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepositoryFactory.cs similarity index 52% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepositoryFactory.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepositoryFactory.cs index fe64368..fa7c760 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepositoryFactory.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepositoryFactory.cs @@ -1,15 +1,14 @@ using System; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; +using PG.StarWarsGame.Engine.Database.ErrorReporting; -namespace PG.StarWarsGame.Engine.Repositories; +namespace PG.StarWarsGame.Engine.IO.Repositories; internal sealed class GameRepositoryFactory(IServiceProvider serviceProvider) : IGameRepositoryFactory { - public GameRepository Create(GameEngineType engineType, GameLocations gameLocations, IXmlParserErrorListener? xmlParserErrorListener) + public GameRepository Create(GameEngineType engineType, GameLocations gameLocations, DatabaseErrorListenerWrapper errorListener) { if (engineType == GameEngineType.Eaw) throw new NotImplementedException("Empire at War is currently not supported."); - return new FocGameRepository(gameLocations, xmlParserErrorListener, serviceProvider); + return new FocGameRepository(gameLocations, errorListener, serviceProvider); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/IGameRepository.cs similarity index 70% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepository.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/IGameRepository.cs index 62e6619..ca6bfd5 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/IGameRepository.cs @@ -1,7 +1,6 @@ -using System.Collections.Generic; -using PG.StarWarsGame.Engine.Language; +using PG.StarWarsGame.Engine.Localization; -namespace PG.StarWarsGame.Engine.Repositories; +namespace PG.StarWarsGame.Engine.IO.Repositories; public interface IGameRepository : IRepository { @@ -18,7 +17,5 @@ public interface IGameRepository : IRepository bool FileExists(string filePath, string[] extensions, bool megFileOnly = false); - IEnumerable FindFiles(string searchPattern, bool megFileOnly = false); - bool IsLanguageInstalled(LanguageType languageType); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/IGameRepositoryFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/IGameRepositoryFactory.cs new file mode 100644 index 0000000..fed8536 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/IGameRepositoryFactory.cs @@ -0,0 +1,8 @@ +using PG.StarWarsGame.Engine.Database.ErrorReporting; + +namespace PG.StarWarsGame.Engine.IO.Repositories; + +internal interface IGameRepositoryFactory +{ + GameRepository Create(GameEngineType engineType, GameLocations gameLocations, DatabaseErrorListenerWrapper errorListener); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/IRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/IRepository.cs new file mode 100644 index 0000000..8054340 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/IRepository.cs @@ -0,0 +1,17 @@ +using System; +using System.IO; + +namespace PG.StarWarsGame.Engine.IO.Repositories; + +public interface IRepository +{ + Stream OpenFile(string filePath, bool megFileOnly = false); + Stream OpenFile(ReadOnlySpan filePath, bool megFileOnly = false); + + bool FileExists(string filePath, bool megFileOnly = false); + bool FileExists(ReadOnlySpan filePath, bool megFileOnly = false); + + Stream? TryOpenFile(string filePath, bool megFileOnly = false); + + Stream? TryOpenFile(ReadOnlySpan filePath, bool megFileOnly = false); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/MultiPassRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/MultiPassRepository.cs new file mode 100644 index 0000000..7dd0b6e --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/MultiPassRepository.cs @@ -0,0 +1,64 @@ +using System; +using System.IO; +using System.IO.Abstractions; +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Engine.Utilities; + +namespace PG.StarWarsGame.Engine.IO.Repositories; + +internal abstract class MultiPassRepository(GameRepository baseRepository, IServiceProvider serviceProvider) : IRepository +{ + protected readonly IFileSystem FileSystem = serviceProvider.GetRequiredService(); + protected readonly GameRepository BaseRepository = baseRepository; + + public Stream OpenFile(string filePath, bool megFileOnly = false) + { + return OpenFile(filePath.AsSpan(), megFileOnly); + } + + public Stream OpenFile(ReadOnlySpan filePath, bool megFileOnly = false) + { + var fileStream = TryOpenFile(filePath, megFileOnly); + if (fileStream is null) + throw new FileNotFoundException($"Unable to find game file: {filePath.ToString()}"); + return fileStream; + } + + public bool FileExists(string filePath, bool megFileOnly = false) + { + return FileExists(filePath.AsSpan(), megFileOnly); + } + + public bool FileExists(ReadOnlySpan filePath, bool megFileOnly = false) + { + var multiPassSb = new ValueStringBuilder(stackalloc char[PGConstants.MaxMegEntryPathLength]); + var destinationSb = new ValueStringBuilder(stackalloc char[PGConstants.MaxMegEntryPathLength]); + var fileFound = MultiPassAction(filePath, ref multiPassSb, ref destinationSb, megFileOnly); + var result = fileFound.FileFound; + multiPassSb.Dispose(); + destinationSb.Dispose(); + return result; + } + + private protected abstract FileFoundInfo MultiPassAction( + ReadOnlySpan filePath, + ref ValueStringBuilder reusableStringBuilder, + ref ValueStringBuilder destination, + bool megFileOnly); + + public Stream? TryOpenFile(string filePath, bool megFileOnly = false) + { + return TryOpenFile(filePath.AsSpan(), megFileOnly); + } + + public Stream? TryOpenFile(ReadOnlySpan filePath, bool megFileOnly = false) + { + var multiPassSb = new ValueStringBuilder(stackalloc char[PGConstants.MaxMegEntryPathLength]); + var destinationSb = new ValueStringBuilder(stackalloc char[PGConstants.MaxMegEntryPathLength]); + var fileFound = MultiPassAction(filePath, ref multiPassSb, ref destinationSb, megFileOnly); + var result = BaseRepository.OpenFileCore(fileFound); + multiPassSb.Dispose(); + destinationSb.Dispose(); + return result; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/TextureRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/TextureRepository.cs new file mode 100644 index 0000000..af714f2 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/TextureRepository.cs @@ -0,0 +1,69 @@ +using System; +using PG.StarWarsGame.Engine.Utilities; + +namespace PG.StarWarsGame.Engine.IO.Repositories; + +internal class TextureRepository(GameRepository baseRepository, IServiceProvider serviceProvider) : MultiPassRepository(baseRepository, serviceProvider) +{ + private static readonly string DdsExtension = ".dds"; + private static readonly string TexturePath = "DATA\\ART\\TEXTURES\\"; + + + private protected override FileFoundInfo MultiPassAction( + ReadOnlySpan filePath, + ref ValueStringBuilder reusableStringBuilder, + ref ValueStringBuilder destination, + bool megFileOnly) + { + if (filePath.Length > PGConstants.MaxTextureFileName) + return default; + + reusableStringBuilder.Append(filePath); + + var foundInfo = FindTexture(ref reusableStringBuilder, ref destination); + + if (foundInfo.FileFound) + return foundInfo; + + reusableStringBuilder.Length = 0; + reusableStringBuilder.Append(filePath); + + ChangeExtensionTo(ref reusableStringBuilder, DdsExtension.AsSpan()); + + return FindTexture(ref reusableStringBuilder, ref destination); + } + + private FileFoundInfo FindTexture(ref ValueStringBuilder multiPassStringBuilder, ref ValueStringBuilder pathStringBuilder) + { + var fileInfo = BaseRepository.FindFile(multiPassStringBuilder.AsSpan(), ref pathStringBuilder); + if (fileInfo.FileFound) + return fileInfo; + + + // Only PG knows why they only search for backslash and not also forward slash, + // when in fact in other methods, they handle both. + var separatorIndex = multiPassStringBuilder.AsSpan().LastIndexOf('\\'); + if (separatorIndex != -1) + multiPassStringBuilder.Remove(0 , separatorIndex + 1); + + multiPassStringBuilder.Insert(0, TexturePath); + + return BaseRepository.FindFile(multiPassStringBuilder.AsSpan(), ref pathStringBuilder); + } + + private static void ChangeExtensionTo(ref ValueStringBuilder stringBuilder, ReadOnlySpan extension) + { + // We cannot use Path.ChangeExtension as the PG implementation supports some strange things + // like that a string "c:\\file.tga\\" ending with a directory separator. The PG result will be + // "c:\\file.dds" while Path.ChangeExtension would return "c:\\file.tga\\.dds" + + // Also, while there are many cases, where this method breaks (such as "c:/test.abc/path.dds"), + // it's the way how the engine works 🤷‍ + var firstPeriod = stringBuilder.AsSpan().IndexOf('.'); + if (firstPeriod == -1) + return; + + stringBuilder.Length = firstPeriod; + stringBuilder.Append(extension); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/DirectoryInfoGlobbingWrapper.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Utilities/DirectoryInfoGlobbingWrapper.cs similarity index 95% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/DirectoryInfoGlobbingWrapper.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Utilities/DirectoryInfoGlobbingWrapper.cs index 2f2114a..f012fdc 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/DirectoryInfoGlobbingWrapper.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Utilities/DirectoryInfoGlobbingWrapper.cs @@ -3,14 +3,14 @@ using System.IO; using System.IO.Abstractions; -namespace PG.StarWarsGame.Engine.Utilities; +namespace PG.StarWarsGame.Engine.IO.Utilities; // Taken from https://github.com/vipentti/Vipentti.IO.Abstractions.FileSystemGlobbing /// /// Wraps to be used with /// -public sealed class DirectoryInfoGlobbingWrapper : Microsoft.Extensions.FileSystemGlobbing.Abstractions.DirectoryInfoBase +internal sealed class DirectoryInfoGlobbingWrapper : Microsoft.Extensions.FileSystemGlobbing.Abstractions.DirectoryInfoBase { private readonly IFileSystem _fileSystem; private readonly IDirectoryInfo _directoryInfo; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/FileInfoGlobbingWrapper.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Utilities/FileInfoGlobbingWrapper.cs similarity index 95% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/FileInfoGlobbingWrapper.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Utilities/FileInfoGlobbingWrapper.cs index 744c377..df99faa 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/FileInfoGlobbingWrapper.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Utilities/FileInfoGlobbingWrapper.cs @@ -1,6 +1,6 @@ using System.IO.Abstractions; -namespace PG.StarWarsGame.Engine.Utilities; +namespace PG.StarWarsGame.Engine.IO.Utilities; // Taken from https://github.com/vipentti/Vipentti.IO.Abstractions.FileSystemGlobbing diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/MatcherExtensions.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Utilities/MatcherExtensions.cs similarity index 97% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/MatcherExtensions.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Utilities/MatcherExtensions.cs index e2bde1e..48ddfa1 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/MatcherExtensions.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Utilities/MatcherExtensions.cs @@ -4,14 +4,14 @@ using System.Linq; using Microsoft.Extensions.FileSystemGlobbing; -namespace PG.StarWarsGame.Engine.Utilities; +namespace PG.StarWarsGame.Engine.IO.Utilities; // Taken from https://github.com/vipentti/Vipentti.IO.Abstractions.FileSystemGlobbing /// /// Provides extensions for to support /// -public static class MatcherExtensions +internal static class MatcherExtensions { /// /// Searches the directory specified for all files matching patterns added to this instance of diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Utilities/PathExtensions.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Utilities/PathExtensions.cs new file mode 100644 index 0000000..471b5a8 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Utilities/PathExtensions.cs @@ -0,0 +1,30 @@ +using System; +using System.IO.Abstractions; +using AnakinRaW.CommonUtilities.FileSystem; +using PG.StarWarsGame.Engine.Utilities; + +namespace PG.StarWarsGame.Engine.IO.Utilities; + +internal static class PathExtensions +{ + public static void Join(this IPath _, ReadOnlySpan path1, ReadOnlySpan path2, ref ValueStringBuilder stringBuilder) + { + if (path1.Length == 0 && path2.Length == 0) + return; + + if (path1.Length == 0 || path2.Length == 0) + { + ref var pathToUse = ref path1.Length == 0 ? ref path2 : ref path1; + stringBuilder.Append(pathToUse); + return; + } + + var needsSeparator = !(_.HasTrailingDirectorySeparator(path1) || _.HasLeadingDirectorySeparator(path2)); + + stringBuilder.Append(path1); + if (needsSeparator) + stringBuilder.Append(_.DirectorySeparatorChar); + + stringBuilder.Append(path2); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/EawGameLanguageManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Localization/EawGameLanguageManager.cs similarity index 92% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Language/EawGameLanguageManager.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/Localization/EawGameLanguageManager.cs index 2c720b4..ddf2be7 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/EawGameLanguageManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Localization/EawGameLanguageManager.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace PG.StarWarsGame.Engine.Language; +namespace PG.StarWarsGame.Engine.Localization; internal sealed class EawGameLanguageManager(IServiceProvider serviceProvider) : GameLanguageManager(serviceProvider) { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/FocGameLanguageManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Localization/FocGameLanguageManager.cs similarity index 94% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Language/FocGameLanguageManager.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/Localization/FocGameLanguageManager.cs index 3636c40..363b1e7 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/FocGameLanguageManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Localization/FocGameLanguageManager.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace PG.StarWarsGame.Engine.Language; +namespace PG.StarWarsGame.Engine.Localization; internal sealed class FocGameLanguageManager(IServiceProvider serviceProvider) : GameLanguageManager(serviceProvider) { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Localization/GameLanguageManager.cs similarity index 85% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManager.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/Localization/GameLanguageManager.cs index ec4d6b2..64b2222 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Localization/GameLanguageManager.cs @@ -5,8 +5,9 @@ using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; using PG.Commons.Services; +using PG.StarWarsGame.Engine.Utilities; -namespace PG.StarWarsGame.Engine.Language; +namespace PG.StarWarsGame.Engine.Localization; internal abstract class GameLanguageManager(IServiceProvider serviceProvider) : ServiceBase(serviceProvider), IGameLanguageManager { @@ -113,9 +114,6 @@ private CultureInfo GetParentCultureRecursive(CultureInfo culture) public string LocalizeFileName(string fileName, LanguageType language, out bool localized) { - if (fileName.Length > PGConstants.MaxPathLength) - throw new ArgumentOutOfRangeException(nameof(fileName), "fileName is too long"); - // The game assumes that all localized audio files are referenced by using their english name. // Thus, PG takes this shortcut if (language == LanguageType.English) @@ -124,33 +122,37 @@ public string LocalizeFileName(string fileName, LanguageType language, out bool return fileName; } - - Span localizedName = stackalloc char[fileName.Length]; - var length = LocalizeFileName(fileName.AsSpan(), language, localizedName, out localized); + var stringBuilder = new ValueStringBuilder(stackalloc char[PGConstants.MaxMegEntryPathLength]); + LocalizeFileName(fileName.AsSpan(), language, ref stringBuilder, out localized); if (!localized) + { + stringBuilder.Dispose(); return fileName; + } - Debug.Assert(localizedName.Length == length); - return localizedName.ToString(); + Debug.Assert(stringBuilder.Length == fileName.Length); + return stringBuilder.ToString(); } - public int LocalizeFileName(ReadOnlySpan fileName, LanguageType language, Span destination, out bool localized) { - if (fileName.Length > PGConstants.MaxPathLength) - throw new ArgumentOutOfRangeException(nameof(fileName), "fileName is too long"); - - if (destination.Length < fileName.Length) - throw new ArgumentException("destination is too short", nameof(destination)); + var sb = new ValueStringBuilder(destination.Length); + LocalizeFileName(fileName, language, ref sb, out localized); + sb.TryCopyTo(destination, out var written); + sb.Dispose(); + return written; + } + public void LocalizeFileName(ReadOnlySpan fileName, LanguageType language, ref ValueStringBuilder stringBuilder, out bool localized) + { localized = true; // The game assumes that all localized audio files are referenced by using their english name. // Thus, PG takes this shortcut if (language == LanguageType.English) { - fileName.CopyTo(destination); - return fileName.Length; + stringBuilder.Append(fileName); + return; } var isWav = false; @@ -175,8 +177,8 @@ public int LocalizeFileName(ReadOnlySpan fileName, LanguageType language, { localized = false; Logger.LogWarning($"Unable to localize '{fileName.ToString()}'"); - fileName.CopyTo(destination); - return fileName.Length; + stringBuilder.Append(fileName); + return; } var withoutSuffix = fileName.Slice(0, engSuffixIndex); @@ -189,10 +191,8 @@ public int LocalizeFileName(ReadOnlySpan fileName, LanguageType language, else throw new InvalidOperationException(); - withoutSuffix.CopyTo(destination); - newLocalizedSuffix.CopyTo(destination.Slice(withoutSuffix.Length, newLocalizedSuffix.Length)); - - return fileName.Length; + stringBuilder.Append(withoutSuffix); + stringBuilder.Append(newLocalizedSuffix); } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManagerProvider.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Localization/GameLanguageManagerProvider.cs similarity index 93% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManagerProvider.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/Localization/GameLanguageManagerProvider.cs index b6b00e8..6430c02 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManagerProvider.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Localization/GameLanguageManagerProvider.cs @@ -1,6 +1,6 @@ using System; -namespace PG.StarWarsGame.Engine.Language; +namespace PG.StarWarsGame.Engine.Localization; internal class GameLanguageManagerProvider(IServiceProvider serviceProvider) : IGameLanguageManagerProvider { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/IGameLanguageManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Localization/IGameLanguageManager.cs similarity index 92% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Language/IGameLanguageManager.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/Localization/IGameLanguageManager.cs index b0e03da..de44657 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/IGameLanguageManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Localization/IGameLanguageManager.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace PG.StarWarsGame.Engine.Language; +namespace PG.StarWarsGame.Engine.Localization; public interface IGameLanguageManager { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/IGameLanguageManagerProvider.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Localization/IGameLanguageManagerProvider.cs similarity index 70% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Language/IGameLanguageManagerProvider.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/Localization/IGameLanguageManagerProvider.cs index c47d0a4..fa1e3b8 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/IGameLanguageManagerProvider.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Localization/IGameLanguageManagerProvider.cs @@ -1,4 +1,4 @@ -namespace PG.StarWarsGame.Engine.Language; +namespace PG.StarWarsGame.Engine.Localization; public interface IGameLanguageManagerProvider { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/SupportedLanguage.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Localization/LanguageType.cs similarity index 80% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Language/SupportedLanguage.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/Localization/LanguageType.cs index 50e1aad..5e67d9e 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/SupportedLanguage.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Localization/LanguageType.cs @@ -1,4 +1,4 @@ -namespace PG.StarWarsGame.Engine.Language; +namespace PG.StarWarsGame.Engine.Localization; public enum LanguageType { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj index 705e571..7b1bb16 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 + netstandard2.0;netstandard2.1;net8.0 PG.StarWarsGame.Engine PG.StarWarsGame.Engine AlamoEngineTools.PG.StarWarsGame.Engine @@ -15,14 +15,12 @@ true snupkg true + preview - - - - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -37,8 +35,10 @@ - + + + \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj.DotSettings b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj.DotSettings new file mode 100644 index 0000000..57b1fa3 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj.DotSettings @@ -0,0 +1,7 @@ + + False + True + True + 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 486aa40..c7e3de2 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PGConstants.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PGConstants.cs @@ -4,7 +4,16 @@ namespace PG.StarWarsGame.Engine; public static class PGConstants { - public static readonly Encoding PGCrc32Encoding = Encoding.ASCII; + public static readonly Encoding DefaultPGEncoding = Encoding.ASCII; - public const int MaxPathLength = 260; + // Always reserve one character for the null-terminator + public const int MaxMegEntryPathLength = 259; + public const int MaxEffectFileName = 259; + public const int MaxTextureFileName = 259; + public const int MaxSFXEventDatabaseFileName = 259; + public const int MaxSFXEventName = 255; + public const int MaxGameObjectDatabaseFileName = 127; + public const int MaxCommandBarDatabaseFileName = 259; + + public const int MaxGuiDialogMegaTextureFileName = 255; } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs index ae4269b..e43a693 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs @@ -1,8 +1,9 @@ using Microsoft.Extensions.DependencyInjection; using PG.StarWarsGame.Engine.Database; -using PG.StarWarsGame.Engine.Language; -using PG.StarWarsGame.Engine.Repositories; +using PG.StarWarsGame.Engine.IO.Repositories; +using PG.StarWarsGame.Engine.Localization; using PG.StarWarsGame.Engine.Xml; +using PG.StarWarsGame.Engine.Xml.Parsers; namespace PG.StarWarsGame.Engine; @@ -10,10 +11,14 @@ public static class PetroglyphEngineServiceContribution { 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.AddTransient(sp => new GameDatabaseService(sp)); + serviceCollection.AddSingleton(sp => new GameDatabaseService(sp)); + + // Transients + serviceCollection.AddTransient(sp => new XmlContainerContentParser(sp)); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/RgbaColor.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/RgbaColor.cs new file mode 100644 index 0000000..183441b --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/RgbaColor.cs @@ -0,0 +1,63 @@ +using PG.Commons.Numerics; +using System; + +namespace PG.StarWarsGame.Engine.Rendering; + +public readonly struct RgbaColor(float r, float g, float b, float a) : IEquatable +{ + private readonly float _r = r; + private readonly float _g = g; + private readonly float _b = b; + private readonly float _a = a; + + public uint R => unchecked((uint)(_r * 255.0f)); + public uint G => unchecked((uint)(_g * 255.0f)); + public uint B => unchecked((uint)(_b * 255.0f)); + public uint A => unchecked((uint)(_a * 255.0f)); + + public float Rf => _r; + public float Gf => _g; + public float Bf => _b; + public float Af => _a; + + public RgbaColor() : this(1.0f, 1.0f, 1.0f, 1.0f) + { + } + + public RgbaColor(Vector4Int rgbaVector) : this(rgbaVector.First, rgbaVector.Second, rgbaVector.Third, rgbaVector.Fourth) + { + } + + public RgbaColor(int r, int g, int b, int a) : this(r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f) + { + } + + public override string ToString() + { + return $"{nameof(RgbaColor)} [R={R}, G={G}, B={B}, A={A}]"; + } + + public RgbaColor Lerp(RgbaColor a, RgbaColor b, float t) + { + var red = a.Rf + (b.Rf - a.Rf) * t; + var green = a.Gf + (b.Gf - a.Gf) * t; + var blue = a.Bf + (b.Bf - a.Bf) * t; + var alpha = a.Af + (b.Af - a.Af) * t; + return new RgbaColor(red, green, blue, alpha); + } + + public bool Equals(RgbaColor other) + { + return _r.Equals(other._r) && _g.Equals(other._g) && _b.Equals(other._b) && _a.Equals(other._a); + } + + public override bool Equals(object? obj) + { + return obj is RgbaColor other && Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(_r, _g, _b, _a); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/EffectsRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/EffectsRepository.cs deleted file mode 100644 index 4421c88..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/EffectsRepository.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Linq; - -namespace PG.StarWarsGame.Engine.Repositories; - -public class EffectsRepository(IGameRepository baseRepository, IServiceProvider serviceProvider) : MultiPassRepository(baseRepository, serviceProvider) -{ - private static readonly string[] LookupPaths = - [ - "Data\\Art\\Shaders", - "Data\\Art\\Shaders\\Terrain", - "Data\\Art\\Shaders\\Engine", - ]; - - // The engine does not support ".fxh" as a shader lookup, but as there might be som pre-compiling going on, this should be OK. - private static readonly string[] ShaderExtensions = [".fx", ".fxo", ".fxh"]; - - [return:MaybeNull] - protected override T MultiPassAction(string inputPath, Func fileAction) - { - var currExt = FileSystem.Path.GetExtension(inputPath); - if (!ShaderExtensions.Contains(currExt, StringComparer.OrdinalIgnoreCase)) - throw new ArgumentException("Invalid data extension for shader. Must be .fx, .fxh or .fxo", nameof(inputPath)); - - foreach (var directory in LookupPaths) - { - var lookupPath = FileSystem.Path.Combine(directory, inputPath); - - foreach (var ext in ShaderExtensions) - { - lookupPath = FileSystem.Path.ChangeExtension(lookupPath, ext); - - var actionResult = fileAction(lookupPath); - if (actionResult.success) - return actionResult.result; - } - } - - return default; - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepositoryFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepositoryFactory.cs deleted file mode 100644 index e7f25e2..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepositoryFactory.cs +++ /dev/null @@ -1,9 +0,0 @@ -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Engine.Repositories; - -internal interface IGameRepositoryFactory -{ - GameRepository Create(GameEngineType engineType, GameLocations gameLocations, IXmlParserErrorListener? xmlParserErrorListener); -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IRepository.cs deleted file mode 100644 index fe5764f..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IRepository.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.IO; - -namespace PG.StarWarsGame.Engine.Repositories; - -public interface IRepository -{ - Stream OpenFile(string filePath, bool megFileOnly = false); - - bool FileExists(string filePath, bool megFileOnly = false); - - Stream? TryOpenFile(string filePath, bool megFileOnly = false); -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/MultiPassRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/MultiPassRepository.cs deleted file mode 100644 index c4238bb..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/MultiPassRepository.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.IO; -using System.IO.Abstractions; -using Microsoft.Extensions.DependencyInjection; - -namespace PG.StarWarsGame.Engine.Repositories; - -public abstract class MultiPassRepository(IGameRepository baseRepository, IServiceProvider serviceProvider) : IRepository -{ - protected readonly IFileSystem FileSystem = serviceProvider.GetRequiredService(); - - public Stream OpenFile(string filePath, bool megFileOnly = false) - { - var fileStream = TryOpenFile(filePath, megFileOnly); - if (fileStream is null) - throw new FileNotFoundException($"Unable to find game file: {filePath}"); - return fileStream; - } - - public bool FileExists(string filePath, bool megFileOnly = false) - { - return MultiPassAction(filePath, actualPath => baseRepository.FileExists(actualPath, megFileOnly)); - } - - public Stream? TryOpenFile(string filePath, bool megFileOnly = false) - { - return MultiPassAction(filePath, path => - { - var stream = baseRepository.TryOpenFile(path, megFileOnly); - if (stream is null) - return (false, null); - return (true, stream); - }); - } - - protected abstract T? MultiPassAction(string inputPath, Func fileAction); - - protected bool MultiPassAction(string inputPath, Predicate fileAction) - { - return MultiPassAction(inputPath, path => - { - var result = fileAction(path); - return (result, result); - }); - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/TextureRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/TextureRepository.cs deleted file mode 100644 index e5253e0..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/TextureRepository.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; - -namespace PG.StarWarsGame.Engine.Repositories; - -public class TextureRepository(IGameRepository baseRepository, IServiceProvider serviceProvider) : MultiPassRepository(baseRepository, serviceProvider) -{ - protected override T? MultiPassAction(string inputPath, Func fileAction) where T : default - { - if (FindTexture(inputPath, fileAction, out var result)) - return result; - - var ddsFilePath = ChangeExtensionTo(inputPath, ".dds"); - - if (FindTexture(ddsFilePath, fileAction, out result)) - return result; - - return default; - } - - private bool FindTexture(string inputPath, Func fileAction, out T? result) - { - result = default; - - var actionResult = fileAction(inputPath); - if (actionResult.success) - { - result = actionResult.result; - return true; - } - - var newInput = inputPath; - - // Only PG knows why they only search for backslash and not also forward slash, - // when in fact in other methods, they handle both. - var separatorIndex = inputPath.LastIndexOf('\\'); - if (separatorIndex != -1 && separatorIndex + 1 < inputPath.Length) - newInput = inputPath.Substring(separatorIndex + 1); - - var pathWithFolders = FileSystem.Path.Combine("DATA\\ART\\TEXTURES", newInput); - actionResult = fileAction(pathWithFolders); - - if (actionResult.success) - { - result = actionResult.result; - return true; - } - - - return false; - } - - - private static string ChangeExtensionTo(string input, string extension) - { - // We cannot use Path.ChangeExtension as the PG implementation supports some strange things - // like that a string "c:\\file.tga\\" ending with a directory separator. The PG result will be - // "c:\\file.dds" while Path.ChangeExtension would return "c:\\file.tga\\.dds" - - // Also, while there are many cases, where method breaks (such as "c:/test.abc/path.dds"), - // it's the way how the engine works... - var firstPart = input.Split('.')[0]; - return firstPart + extension; - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/ValueStringBuilder.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/ValueStringBuilder.cs index e210518..8701551 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/ValueStringBuilder.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/ValueStringBuilder.cs @@ -1,25 +1,358 @@ using System; +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace PG.StarWarsGame.Engine.Utilities; -// From https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/System/Text/ValueStringBuilder.cs -internal ref struct ValueStringBuilder(Span initialBuffer) +internal ref struct ValueStringBuilder { - private readonly Span _chars = initialBuffer; - private int _pos = 0; + private char[]? _arrayToReturnToPool; + private Span _chars; + private int _pos; + + public ValueStringBuilder(Span initialBuffer) + { + _arrayToReturnToPool = null; + _chars = initialBuffer; + _pos = 0; + } + + public ValueStringBuilder(int initialCapacity) + { + _arrayToReturnToPool = ArrayPool.Shared.Rent(initialCapacity); + _chars = _arrayToReturnToPool; + _pos = 0; + } + + public int Length + { + get => _pos; + set + { + Debug.Assert(value >= 0); + Debug.Assert(value <= _chars.Length); + _pos = value; + } + } + + public int Capacity => _chars.Length; + + public void EnsureCapacity(int capacity) + { + // This is not expected to be called this with negative capacity + Debug.Assert(capacity >= 0); + + // If the caller has a bug and calls this with negative capacity, make sure to call Grow to throw an exception. + if ((uint)capacity > (uint)_chars.Length) + Grow(capacity - _pos); + } + + /// + /// Get a pinnable reference to the builder. + /// Does not ensure there is a null char after + /// This overload is pattern matched in the C# 7.3+ compiler so you can omit + /// the explicit method call, and write eg "fixed (char* c = builder)" + /// + public ref char GetPinnableReference() + { + return ref MemoryMarshal.GetReference(_chars); + } + + /// + /// Get a pinnable reference to the builder. + /// + /// Ensures that the builder has a null char after + public ref char GetPinnableReference(bool terminate) + { + if (terminate) + { + EnsureCapacity(Length + 1); + _chars[Length] = '\0'; + } + return ref MemoryMarshal.GetReference(_chars); + } + + public ref char this[int index] + { + get + { + Debug.Assert(index < _pos); + return ref _chars[index]; + } + } public override string ToString() { - return _chars.Slice(0, _pos).ToString(); + string s = _chars.Slice(0, _pos).ToString(); + Dispose(); + return s; + } + + /// Returns the underlying storage of the builder. + public Span RawChars => _chars; + + /// + /// Returns a span around the contents of the builder. + /// + /// Ensures that the builder has a null char after + public ReadOnlySpan AsSpan(bool terminate) + { + if (terminate) + { + EnsureCapacity(Length + 1); + _chars[Length] = '\0'; + } + return _chars.Slice(0, _pos); + } + + public ReadOnlySpan AsSpan() => _chars.Slice(0, _pos); + public ReadOnlySpan AsSpan(int start) => _chars.Slice(start, _pos - start); + public ReadOnlySpan AsSpan(int start, int length) => _chars.Slice(start, length); + + public bool TryCopyTo(Span destination, out int charsWritten) + { + if (_chars.Slice(0, _pos).TryCopyTo(destination)) + { + charsWritten = _pos; + Dispose(); + return true; + } + else + { + charsWritten = 0; + Dispose(); + return false; + } + } + + public void Insert(int index, char value, int count) + { + if (_pos > _chars.Length - count) + { + Grow(count); + } + + int remaining = _pos - index; + _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); + _chars.Slice(index, count).Fill(value); + _pos += count; + } + + public void Insert(int index, string? s) + { + if (s == null) + { + return; + } + + int count = s.Length; + + if (_pos > (_chars.Length - count)) + { + Grow(count); + } + + int remaining = _pos - index; + _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); + s +#if !NETCOREAPP + .AsSpan() +#endif + .CopyTo(_chars.Slice(index)); + _pos += count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(char c) + { + int pos = _pos; + Span chars = _chars; + if ((uint)pos < (uint)chars.Length) + { + chars[pos] = c; + _pos = pos + 1; + } + else + { + GrowAndAppend(c); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(string? s) + { + if (s == null) + { + return; + } + + int pos = _pos; + if (s.Length == 1 && (uint)pos < (uint)_chars.Length) // very common case, e.g. appending strings from NumberFormatInfo like separators, percent symbols, etc. + { + _chars[pos] = s[0]; + _pos = pos + 1; + } + else + { + AppendSlow(s); + } + } + + private void AppendSlow(string s) + { + int pos = _pos; + if (pos > _chars.Length - s.Length) + { + Grow(s.Length); + } + + s +#if !NETCOREAPP + .AsSpan() +#endif + .CopyTo(_chars.Slice(pos)); + _pos += s.Length; + } + + public void Append(char c, int count) + { + if (_pos > _chars.Length - count) + { + Grow(count); + } + + Span dst = _chars.Slice(_pos, count); + for (int i = 0; i < dst.Length; i++) + { + dst[i] = c; + } + _pos += count; + } + + public unsafe void Append(char* value, int length) + { + int pos = _pos; + if (pos > _chars.Length - length) + { + Grow(length); + } + + Span dst = _chars.Slice(_pos, length); + for (int i = 0; i < dst.Length; i++) + { + dst[i] = *value++; + } + _pos += length; } public void Append(scoped ReadOnlySpan value) { - var pos = _pos; + int pos = _pos; if (pos > _chars.Length - value.Length) - throw new InvalidOperationException("Value string builder is too small."); + { + Grow(value.Length); + } value.CopyTo(_chars.Slice(_pos)); _pos += value.Length; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span AppendSpan(int length) + { + int origPos = _pos; + if (origPos > _chars.Length - length) + { + Grow(length); + } + + _pos = origPos + length; + return _chars.Slice(origPos, length); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void GrowAndAppend(char c) + { + Grow(1); + Append(c); + } + + /// + /// Resize the internal buffer either by doubling current buffer size or + /// by adding to + /// whichever is greater. + /// + /// + /// Number of chars requested beyond current position. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private void Grow(int additionalCapacityBeyondPos) + { + Debug.Assert(additionalCapacityBeyondPos > 0); + Debug.Assert(_pos > _chars.Length - additionalCapacityBeyondPos, "Grow called incorrectly, no resize is needed."); + + const uint ArrayMaxLength = 0x7FFFFFC7; // same as Array.MaxLength + + // Increase to at least the required size (_pos + additionalCapacityBeyondPos), but try + // to double the size if possible, bounding the doubling to not go beyond the max array length. + int newCapacity = (int)Math.Max( + (uint)(_pos + additionalCapacityBeyondPos), + Math.Min((uint)_chars.Length * 2, ArrayMaxLength)); + + // Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative. + // This could also go negative if the actual required length wraps around. + char[] poolArray = ArrayPool.Shared.Rent(newCapacity); + + _chars.Slice(0, _pos).CopyTo(poolArray); + + char[]? toReturn = _arrayToReturnToPool; + _chars = _arrayToReturnToPool = poolArray; + if (toReturn != null) + { + ArrayPool.Shared.Return(toReturn); + } + } + + /// + /// Removes a range of characters from this builder. + /// + /// The inclusive index from where the string gets removed. + /// The length of the slice to remove. + /// + /// This method will not affect the internal size of the string. + /// + public void Remove(int startIndex, int length) + { + if (length == 0) + return; + if (length < 0) + throw new ArgumentOutOfRangeException(nameof(length), "The given length can't be negative."); + if (startIndex < 0) + throw new ArgumentOutOfRangeException(nameof(startIndex), "The given start index can't be negative."); + if (length > Length - startIndex) + throw new ArgumentOutOfRangeException(nameof(length), $"The given Span ({startIndex}..{length})length is outside the the represented string."); + if (Length == length && startIndex == 0) + { + Length = 0; + return; + } + var currentLength = Length; + var remaining = _chars.Slice(startIndex + length, currentLength - length); + var toOverwrite = _chars.Slice(startIndex); + remaining.CopyTo(toOverwrite); + _pos = currentLength - length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() + { + char[]? toReturn = _arrayToReturnToPool; + this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again + if (toReturn != null) + { + ArrayPool.Shared.Return(toReturn); + } + } } \ 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 36cb12c..d5106c2 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/IPetroglyphXmlFileParserFactory.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/IPetroglyphXmlFileParserFactory.cs @@ -5,5 +5,5 @@ namespace PG.StarWarsGame.Engine.Xml; public interface IPetroglyphXmlFileParserFactory { - IPetroglyphXmlFileParser GetFileParser(IXmlParserErrorListener? listener = null); + IPetroglyphXmlFileParser CreateFileParser(IXmlParserErrorListener? listener = null); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/NamedXmlObject.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/NamedXmlObject.cs new file mode 100644 index 0000000..7cf9c4e --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/NamedXmlObject.cs @@ -0,0 +1,13 @@ +using System; +using PG.Commons.Data; +using PG.Commons.Hashing; +using PG.StarWarsGame.Files.XML; + +namespace PG.StarWarsGame.Engine.Xml; + +public abstract class NamedXmlObject(string name, Crc32 nameCrc, XmlLocationInfo location) : XmlObject(location), IHasCrc32 +{ + public Crc32 Crc32 { get; } = nameCrc; + + public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); +} \ 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 new file mode 100644 index 0000000..ca35b6e --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/CommandBarComponentParser.cs @@ -0,0 +1,342 @@ +using System; +using System.Collections.ObjectModel; +using System.Xml.Linq; +using PG.Commons.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; + +namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; + +public sealed class CommandBarComponentParser( + IReadOnlyValueListDictionary parsedElements, + IServiceProvider serviceProvider, + IXmlParserErrorListener? listener = null) + : XmlObjectParser(parsedElements, serviceProvider, listener) +{ + public override CommandBarComponentData Parse(XElement element, out Crc32 nameCrc) + { + var name = GetXmlObjectName(element, out nameCrc); + var component = new CommandBarComponentData(name, nameCrc, XmlLocationInfo.FromElement(element)); + Parse(component, element, default); + component.CoerceValues(); + return component; + } + + protected override bool ParseTag(XElement tag, CommandBarComponentData componentData) + { + switch (tag.Name.LocalName) + { + case CommandBarComponentTags.SelectedTextureName: + componentData.SelectedTextureNames = new ReadOnlyCollection(PrimitiveParserProvider.LooseStringListParser.Parse(tag)); + return true; + case CommandBarComponentTags.BlankTextureName: + componentData.BlankTextureNames = new ReadOnlyCollection(PrimitiveParserProvider.LooseStringListParser.Parse(tag)); + return true; + case CommandBarComponentTags.IconAlternateTextureName: + componentData.IconAlternateTextureNames = new ReadOnlyCollection(PrimitiveParserProvider.LooseStringListParser.Parse(tag)); + return true; + case CommandBarComponentTags.MouseOverTextureName: + componentData.MouseOverTextureNames = new ReadOnlyCollection(PrimitiveParserProvider.LooseStringListParser.Parse(tag)); + return true; + case CommandBarComponentTags.BarTextureName: + componentData.BarTextureNames = new ReadOnlyCollection(PrimitiveParserProvider.LooseStringListParser.Parse(tag)); + return true; + case CommandBarComponentTags.BarOverlayName: + componentData.BarOverlayNames = new ReadOnlyCollection(PrimitiveParserProvider.LooseStringListParser.Parse(tag)); + return true; + case CommandBarComponentTags.AlternateFontName: + componentData.AlternateFontNames = new ReadOnlyCollection(PrimitiveParserProvider.LooseStringListParser.Parse(tag)); + return true; + case CommandBarComponentTags.TooltipText: + componentData.TooltipTexts = new ReadOnlyCollection(PrimitiveParserProvider.LooseStringListParser.Parse(tag)); + return true; + case CommandBarComponentTags.LowerEffectTextureName: + componentData.LowerEffectTextureNames = new ReadOnlyCollection(PrimitiveParserProvider.LooseStringListParser.Parse(tag)); + return true; + case CommandBarComponentTags.UpperEffectTextureName: + componentData.UpperEffectTextureNames = new ReadOnlyCollection(PrimitiveParserProvider.LooseStringListParser.Parse(tag)); + return true; + case CommandBarComponentTags.OverlayTextureName: + componentData.OverlayTextureNames = new ReadOnlyCollection(PrimitiveParserProvider.LooseStringListParser.Parse(tag)); + return true; + case CommandBarComponentTags.Overlay2TextureName: + componentData.Overlay2TextureNames = new ReadOnlyCollection(PrimitiveParserProvider.LooseStringListParser.Parse(tag)); + return true; + + case CommandBarComponentTags.IconTextureName: + componentData.IconTextureName = PrimitiveParserProvider.StringParser.Parse(tag); + return true; + case CommandBarComponentTags.DisabledTextureName: + componentData.DisabledTextureName = PrimitiveParserProvider.StringParser.Parse(tag); + return true; + case CommandBarComponentTags.FlashTextureName: + componentData.FlashTextureName = PrimitiveParserProvider.StringParser.Parse(tag); + return true; + case CommandBarComponentTags.BuildTextureName: + componentData.BuildTextureName = PrimitiveParserProvider.StringParser.Parse(tag); + return true; + case CommandBarComponentTags.ModelName: + componentData.ModelName = PrimitiveParserProvider.StringParser.Parse(tag); + return true; + case CommandBarComponentTags.BoneName: + componentData.BoneName = PrimitiveParserProvider.StringParser.Parse(tag); + return true; + case CommandBarComponentTags.CursorTextureName: + componentData.CursorTextureName = PrimitiveParserProvider.StringParser.Parse(tag); + return true; + case CommandBarComponentTags.FontName: + componentData.FontName = PrimitiveParserProvider.StringParser.Parse(tag); + return true; + case CommandBarComponentTags.ClickSfx: + componentData.ClickSfx = PrimitiveParserProvider.StringParser.Parse(tag); + return true; + case CommandBarComponentTags.MouseOverSfx: + componentData.MouseOverSfx = PrimitiveParserProvider.StringParser.Parse(tag); + return true; + case CommandBarComponentTags.RightClickSfx: + componentData.RightClickSfx = PrimitiveParserProvider.StringParser.Parse(tag); + return true; + case CommandBarComponentTags.Type: + componentData.Type = PrimitiveParserProvider.StringParser.Parse(tag); + return true; + case CommandBarComponentTags.Group: + componentData.Group = PrimitiveParserProvider.StringParser.Parse(tag); + return true; + case CommandBarComponentTags.AssociatedText: + componentData.AssociatedText = PrimitiveParserProvider.StringParser.Parse(tag); + return true; + + case CommandBarComponentTags.DragAndDrop: + componentData.DragAndDrop = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.DragSelect: + componentData.DragSelect = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.Receptor: + componentData.Receptor = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.Toggle: + componentData.Toggle = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.Tab: + componentData.Tab = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.Hidden: + componentData.Hidden = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.ClearColor: + componentData.ClearColor = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.Disabled: + componentData.Disabled = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.SwapTexture: + componentData.SwapTexture = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.DrawAdditive: + componentData.DrawAdditive = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.Editable: + componentData.Editable = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.TextOutline: + componentData.TextOutline = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.Stackable: + componentData.Stackable = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.ModelOffsetX: + componentData.ModelOffsetX = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.ModelOffsetY: + componentData.ModelOffsetY = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.ScaleModelX: + componentData.ScaleModelX = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.ScaleModelY: + componentData.ScaleModelY = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.Collideable: + componentData.Collideable = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.TextEmboss: + componentData.TextEmboss = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.ShouldGhost: + componentData.ShouldGhost = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.GhostBaseOnly: + componentData.GhostBaseOnly = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.CrossFade: + componentData.CrossFade = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.LeftJustified: + componentData.LeftJustified = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.RightJustified: + componentData.RightJustified = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.NoShell: + componentData.NoShell = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.SnapDrag: + componentData.SnapDrag = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.SnapLocation: + componentData.SnapLocation = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.OffsetRender: + componentData.OffsetRender = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.BlinkFade: + componentData.BlinkFade = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.NoHiddenCollision: + componentData.NoHiddenCollision = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.ManualOffset: + componentData.ManualOffset = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.SelectedAlpha: + componentData.SelectedAlpha = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.PixelAlign: + componentData.PixelAlign = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.CanDragStack: + componentData.CanDragStack = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.CanAnimate: + componentData.CanAnimate = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.LoopAnim: + componentData.LoopAnim = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.SmoothBar: + componentData.SmoothBar = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.OutlinedBar: + componentData.OutlinedBar = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.DragBack: + componentData.DragBack = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.LowerEffectAdditive: + componentData.LowerEffectAdditive = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.UpperEffectAdditive: + componentData.UpperEffectAdditive = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.ClickShift: + componentData.ClickShift = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.TutorialScene: + componentData.TutorialScene = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.DialogScene: + componentData.DialogScene = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.ShouldRenderAtDragPos: + componentData.ShouldRenderAtDragPos = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.DisableDarken: + componentData.DisableDarken = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.AnimateBack: + componentData.AnimateBack = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case CommandBarComponentTags.AnimateUpperEffect: + componentData.AnimateUpperEffect = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + + case CommandBarComponentTags.Size: + componentData.Size = PrimitiveParserProvider.Vector2FParser.Parse(tag); + return true; + case CommandBarComponentTags.TextOffset: + componentData.TextOffset = PrimitiveParserProvider.Vector2FParser.Parse(tag); + return true; + case CommandBarComponentTags.TextOffset2: + componentData.TextOffset2 = PrimitiveParserProvider.Vector2FParser.Parse(tag); + return true; + case CommandBarComponentTags.Offset: + componentData.Offset = PrimitiveParserProvider.Vector2FParser.Parse(tag); + return true; + case CommandBarComponentTags.DefaultOffset: + componentData.DefaultOffset = PrimitiveParserProvider.Vector2FParser.Parse(tag); + return true; + case CommandBarComponentTags.DefaultOffsetWidescreen: + componentData.DefaultOffsetWidescreen = PrimitiveParserProvider.Vector2FParser.Parse(tag); + return true; + case CommandBarComponentTags.IconOffset: + componentData.IconOffset = PrimitiveParserProvider.Vector2FParser.Parse(tag); + return true; + case CommandBarComponentTags.MouseOverOffset: + componentData.MouseOverOffset = PrimitiveParserProvider.Vector2FParser.Parse(tag); + return true; + case CommandBarComponentTags.DisabledOffset: + componentData.DisabledOffset = PrimitiveParserProvider.Vector2FParser.Parse(tag); + return true; + case CommandBarComponentTags.BuildDialOffset: + componentData.BuildDialOffset = PrimitiveParserProvider.Vector2FParser.Parse(tag); + return true; + case CommandBarComponentTags.BuildDial2Offset: + componentData.BuildDial2Offset = PrimitiveParserProvider.Vector2FParser.Parse(tag); + return true; + case CommandBarComponentTags.LowerEffectOffset: + componentData.LowerEffectOffset = PrimitiveParserProvider.Vector2FParser.Parse(tag); + return true; + case CommandBarComponentTags.UpperEffectOffset: + componentData.UpperEffectOffset = PrimitiveParserProvider.Vector2FParser.Parse(tag); + return true; + case CommandBarComponentTags.OverlayOffset: + componentData.OverlayOffset = PrimitiveParserProvider.Vector2FParser.Parse(tag); + return true; + case CommandBarComponentTags.Overlay2Offset: + componentData.Overlay2Offset = PrimitiveParserProvider.Vector2FParser.Parse(tag); + return true; + + case CommandBarComponentTags.MaxTextLength: + componentData.MaxTextLength = PrimitiveParserProvider.UIntParser.Parse(tag); + return true; + case CommandBarComponentTags.FontPointSize: + componentData.FontPointSize = (int)PrimitiveParserProvider.UIntParser.Parse(tag); + return true; + + case CommandBarComponentTags.Scale: + componentData.Scale = PrimitiveParserProvider.FloatParser.Parse(tag); + return true; + case CommandBarComponentTags.BlinkRate: + componentData.BlinkRate = PrimitiveParserProvider.FloatParser.Parse(tag); + return true; + case CommandBarComponentTags.MaxTextWidth: + componentData.MaxTextWidth = PrimitiveParserProvider.FloatParser.Parse(tag); + return true; + case CommandBarComponentTags.BlinkDuration: + componentData.BlinkDuration = PrimitiveParserProvider.FloatParser.Parse(tag); + return true; + case CommandBarComponentTags.ScaleDuration: + componentData.ScaleDuration = PrimitiveParserProvider.FloatParser.Parse(tag); + return true; + case CommandBarComponentTags.AnimFps: + componentData.AnimFps = PrimitiveParserProvider.FloatParser.Parse(tag); + return true; + + case CommandBarComponentTags.BaseLayer: + componentData.BaseLayer = (int)PrimitiveParserProvider.UIntParser.Parse(tag); + return true; + case CommandBarComponentTags.MaxBarLevel: + componentData.MaxBarLevel = (int)PrimitiveParserProvider.UIntParser.Parse(tag); + return true; + + + default: return true; + } + } + + 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/GameConstantsParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameConstantsParser.cs index 212bab1..7ec0cbe 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameConstantsParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameConstantsParser.cs @@ -1,22 +1,22 @@ using System; using System.Xml.Linq; +using PG.Commons.Collections; using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.DataTypes; -using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Engine.GameConstants; using PG.StarWarsGame.Files.XML.ErrorHandling; using PG.StarWarsGame.Files.XML.Parsers; namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; internal class GameConstantsParser(IServiceProvider serviceProvider, IXmlParserErrorListener? listener = null) : - PetroglyphXmlFileParser(serviceProvider, listener) + PetroglyphXmlFileParser(serviceProvider, listener) { - public override GameConstants Parse(XElement element) + protected override GameConstantsXml Parse(XElement element, string fileName) { - return new GameConstants(); + return new GameConstantsXml(); } - protected override void Parse(XElement element, IValueListDictionary parsedElements) + protected override void Parse(XElement element, IValueListDictionary parsedElements, string fileName) { throw new NotSupportedException(); } 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 831cc55..ee97771 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs @@ -1,54 +1,97 @@ using System; using System.Xml.Linq; +using PG.Commons.Collections; using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.DataTypes; +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( IReadOnlyValueListDictionary parsedElements, IServiceProvider serviceProvider, IXmlParserErrorListener? listener = null) : XmlObjectParser(parsedElements, serviceProvider, listener) -{ - protected override IPetroglyphXmlElementParser? GetParser(string tag) - { - switch (tag) - { - case "Land_Terrain_Model_Mapping": - return PrimitiveParserProvider.CommaSeparatedStringKeyValueListParser; - case "Galactic_Model_Name": - case "Destroyed_Galactic_Model_Name": - case "Land_Model_Name": - case "Space_Model_Name": - case "Model_Name": - case "Tactical_Model_Name": - case "Galactic_Fleet_Override_Model_Name": - case "GUI_Model_Name": - case "GUI_Model": - case "Land_Model_Anim_Override_Name": - case "xxxSpace_Model_Name": - case "Damaged_Smoke_Asset_Name": - return PrimitiveParserProvider.StringParser; - default: - return null; - } - } - +{ public override GameObject Parse(XElement element, out Crc32 nameCrc) { - var properties = ParseXmlElement(element); - var name = GetNameAttributeValue(element); - nameCrc = HashingService.GetCrc32Upper(name.AsSpan(), PGConstants.PGCrc32Encoding); + var name = GetXmlObjectName(element, out nameCrc); var type = GetTagName(element); var objectType = EstimateType(type); - var gameObject = new GameObject(type, name, nameCrc, objectType, properties, XmlLocationInfo.FromElement(element)); + var gameObject = new GameObject(type, name, nameCrc, 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 = PrimitiveParserProvider.CommaSeparatedStringKeyValueListParser.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 = PrimitiveParserProvider.StringParser.Parse(tag); + return true; + case GameObjectXmlTags.DestroyedGalacticModelName: + xmlObject.DestroyedGalacticModel = PrimitiveParserProvider.StringParser.Parse(tag); + return true; + case GameObjectXmlTags.LandModelName: + xmlObject.LandModel = PrimitiveParserProvider.StringParser.Parse(tag); + return true; + case GameObjectXmlTags.SpaceModelName: + xmlObject.SpaceModel = PrimitiveParserProvider.StringParser.Parse(tag); + return true; + case GameObjectXmlTags.ModelName: + xmlObject.ModelName = PrimitiveParserProvider.StringParser.Parse(tag); + return true; + case GameObjectXmlTags.TacticalModelName: + xmlObject.TacticalModel = PrimitiveParserProvider.StringParser.Parse(tag); + return true; + case GameObjectXmlTags.GalacticFleetOverrideModelName: + xmlObject.GalacticFleetOverrideModel = PrimitiveParserProvider.StringParser.Parse(tag); + return true; + case GameObjectXmlTags.GuiModelName: + xmlObject.GuiModel = PrimitiveParserProvider.StringParser.Parse(tag); + return true; + case GameObjectXmlTags.LandModelAnimOverrideName: + xmlObject.LandAnimOverrideModel = PrimitiveParserProvider.StringParser.Parse(tag); + return true; + case GameObjectXmlTags.XxxSpaceModelName: + xmlObject.XxxSpaceModeModel = PrimitiveParserProvider.StringParser.Parse(tag); + return true; + case GameObjectXmlTags.DamagedSmokeAssetName: + xmlObject.DamagedSmokeAssetModel = PrimitiveParserProvider.StringParser.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_")) 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 ffb7e18..6df3ecb 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs @@ -1,11 +1,12 @@ using System; +using System.Collections.ObjectModel; using System.Xml.Linq; +using PG.Commons.Collections; using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.DataTypes; +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; @@ -14,109 +15,188 @@ public sealed class SfxEventParser( IServiceProvider serviceProvider, IXmlParserErrorListener? listener = null) : XmlObjectParser(parsedElements, serviceProvider, listener) -{ - protected override IPetroglyphXmlElementParser? GetParser(string tag) +{ + public override SfxEvent Parse(XElement element, out Crc32 nameCrc) + { + var name = GetXmlObjectName(element, out nameCrc); + var sfxEvent = new SfxEvent(name, nameCrc, XmlLocationInfo.FromElement(element)); + Parse(sfxEvent, element, default); + ValidateValues(sfxEvent, element); + sfxEvent.CoerceValues(); + return sfxEvent; + } + + private void ValidateValues(SfxEvent sfxEvent, XElement element) { - switch (tag) + 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.UsePreset: case SfxEventXmlTags.OverlapTest: + sfxEvent.OverlapTestName = PrimitiveParserProvider.StringParser.Parse(tag); + return true; case SfxEventXmlTags.ChainedSfxEvent: - return PrimitiveParserProvider.StringParser; + sfxEvent.ChainedSfxEventName = PrimitiveParserProvider.StringParser.Parse(tag); + return true; + case SfxEventXmlTags.UsePreset: + { + var presetName = PrimitiveParserProvider.StringParser.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 = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; case SfxEventXmlTags.Is3D: + sfxEvent.Is3D = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; case SfxEventXmlTags.Is2D: + sfxEvent.Is2D = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; case SfxEventXmlTags.IsGui: + sfxEvent.IsGui = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; case SfxEventXmlTags.IsHudVo: + sfxEvent.IsHudVo = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; case SfxEventXmlTags.IsUnitResponseVo: + sfxEvent.IsUnitResponseVo = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; case SfxEventXmlTags.IsAmbientVo: + sfxEvent.IsAmbientVo = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; case SfxEventXmlTags.Localize: + sfxEvent.IsLocalized = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; case SfxEventXmlTags.PlaySequentially: + sfxEvent.PlaySequentially = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; case SfxEventXmlTags.KillsPreviousObjectSFX: - return PrimitiveParserProvider.BooleanParser; + sfxEvent.KillsPreviousObjectsSfx = PrimitiveParserProvider.BooleanParser.Parse(tag); + return true; + case SfxEventXmlTags.Samples: + sfxEvent.Samples = new ReadOnlyCollection(PrimitiveParserProvider.LooseStringListParser.Parse(tag)); + return true; case SfxEventXmlTags.PreSamples: + sfxEvent.PreSamples = new ReadOnlyCollection(PrimitiveParserProvider.LooseStringListParser.Parse(tag)); + return true; case SfxEventXmlTags.PostSamples: + sfxEvent.PostSamples = new ReadOnlyCollection(PrimitiveParserProvider.LooseStringListParser.Parse(tag)); + return true; case SfxEventXmlTags.TextID: - return PrimitiveParserProvider.LooseStringListParser; + sfxEvent.LocalizedTextIDs = new ReadOnlyCollection(PrimitiveParserProvider.LooseStringListParser.Parse(tag)); + return true; + case SfxEventXmlTags.Priority: + sfxEvent.Priority = (byte)PrimitiveParserProvider.IntParser.ParseWithRange(tag, SfxEvent.MinPriorityValue, SfxEvent.MaxPriorityValue); + return true; case SfxEventXmlTags.MinPitch: + sfxEvent.MinPitch = (byte)PrimitiveParserProvider.IntParser.ParseWithRange(tag, SfxEvent.MinPitchValue, SfxEvent.MaxPitchValue); + return true; case SfxEventXmlTags.MaxPitch: + sfxEvent.MaxPitch = (byte)PrimitiveParserProvider.IntParser.ParseWithRange(tag, SfxEvent.MinPitchValue, SfxEvent.MaxPitchValue); + return true; case SfxEventXmlTags.MinPan2D: + sfxEvent.MinPan2D = (byte)PrimitiveParserProvider.IntParser.ParseWithRange(tag, byte.MinValue, SfxEvent.MaxPan2dValue); + return true; case SfxEventXmlTags.MaxPan2D: + sfxEvent.MaxPan2D = (byte)PrimitiveParserProvider.IntParser.ParseWithRange(tag, byte.MinValue, SfxEvent.MaxPan2dValue); + return true; case SfxEventXmlTags.PlayCount: + sfxEvent.PlayCount = (sbyte)PrimitiveParserProvider.IntParser.ParseWithRange(tag, SfxEvent.InfinitivePlayCount, sbyte.MaxValue); + return true; case SfxEventXmlTags.MaxInstances: - return PrimitiveParserProvider.IntParser; + sfxEvent.MaxInstances = (sbyte)PrimitiveParserProvider.IntParser.ParseWithRange(tag, SfxEvent.MinMaxInstances, sbyte.MaxValue); + return true; + case SfxEventXmlTags.Probability: + sfxEvent.Probability = PrimitiveParserProvider.Max100ByteParser.ParseWithRange(tag, byte.MinValue, SfxEvent.MaxProbability); + return true; case SfxEventXmlTags.MinVolume: + sfxEvent.MinVolume = PrimitiveParserProvider.Max100ByteParser.ParseWithRange(tag, byte.MinValue, SfxEvent.MaxVolumeValue); + return true; case SfxEventXmlTags.MaxVolume: - return PrimitiveParserProvider.Max100ByteParser; + sfxEvent.MaxVolume = PrimitiveParserProvider.Max100ByteParser.ParseWithRange(tag, byte.MinValue, SfxEvent.MaxVolumeValue); + return true; + case SfxEventXmlTags.MinPredelay: + sfxEvent.MinPredelay = PrimitiveParserProvider.UIntParser.Parse(tag); + return true; case SfxEventXmlTags.MaxPredelay: + sfxEvent.MaxPredelay = PrimitiveParserProvider.UIntParser.Parse(tag); + return true; case SfxEventXmlTags.MinPostdelay: + sfxEvent.MinPostdelay = PrimitiveParserProvider.UIntParser.Parse(tag); + return true; case SfxEventXmlTags.MaxPostdelay: - return PrimitiveParserProvider.UIntParser; + sfxEvent.MaxPostdelay = PrimitiveParserProvider.UIntParser.Parse(tag); + return true; + case SfxEventXmlTags.LoopFadeInSeconds: + sfxEvent.LoopFadeInSeconds = PrimitiveParserProvider.FloatParser.ParseAtLeast(tag, SfxEvent.MinLoopSeconds); + return true; case SfxEventXmlTags.LoopFadeOutSeconds: + sfxEvent.LoopFadeOutSeconds = PrimitiveParserProvider.FloatParser.ParseAtLeast(tag, SfxEvent.MinLoopSeconds); + return true; case SfxEventXmlTags.VolumeSaturationDistance: - return PrimitiveParserProvider.FloatParser; - default: - return null; - } - } - - public override SfxEvent Parse(XElement element, out Crc32 nameCrc) - { - var name = GetNameAttributeValue(element); - nameCrc = HashingService.GetCrc32Upper(name.AsSpan(), PGConstants.PGCrc32Encoding); - - var properties = ParseXmlElement(element); - - return new SfxEvent(name, nameCrc, properties, XmlLocationInfo.FromElement(element)); - } - - protected override bool OnParsed(XElement element, string tag, object value, ValueListDictionary properties, string? outerElementName) - { - if (tag == SfxEventXmlTags.UsePreset) - { - var presetName = value as string; - var presetNameCrc = HashingService.GetCrc32Upper(presetName.AsSpan(), PGConstants.PGCrc32Encoding); - if (presetNameCrc != default && ParsedElements.TryGetFirstValue(presetNameCrc, out var preset)) - CopySfxPreset(properties, preset); - else - { - var location = XmlLocationInfo.FromElement(element); - OnParseError(new XmlParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.MissingReference, - $"Cannot to find preset '{presetName}' for SFXEvent '{outerElementName ?? "NONE"}'")); - } + // 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 = PrimitiveParserProvider.FloatParser.ParseAtLeast(tag, SfxEvent.MinVolumeSaturation); + return true; + default: return false; } - return true; - } - - private static void CopySfxPreset(ValueListDictionary currentXmlProperties, SfxEvent preset) - { - /* - * The engine also copies the Use_Preset *of* the preset, (which almost most cases is null) - * As this would cause that the SfxEvent using the preset, would not have a reference to its original preset, we do not copy the preset - * Example: - * - * - * Preset Yes - * 90 - * - * - * Engine Behavior: SFXEvent instance(Name: A, Use_Preset: null, Min_Volume: 90) - * PG.StarWarsGame.Engine Behavior: SFXEvent instance(Name: A, Use_Preset: Preset, Min_Volume: 90) - */ - - foreach (var keyValuePair in preset.XmlProperties) - { - if (keyValuePair.Key is SfxEventXmlTags.UsePreset or SfxEventXmlTags.IsPreset) - continue; - currentXmlProperties.Add(keyValuePair.Key, keyValuePair.Value); - } - - currentXmlProperties.Add(SfxEventXmlTags.PresetXRef, preset); } public override SfxEvent Parse(XElement element) => throw new NotSupportedException(); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/CommandBarComponentFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/CommandBarComponentFileParser.cs new file mode 100644 index 0000000..9c82a16 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/CommandBarComponentFileParser.cs @@ -0,0 +1,38 @@ +using System; +using System.Xml.Linq; +using PG.Commons.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, IXmlParserErrorListener? listener = null) + : PetroglyphXmlFileParser(serviceProvider, listener) +{ + private readonly IXmlParserErrorListener? _listener = listener; + + protected override void Parse(XElement element, IValueListDictionary parsedElements, string fileName) + { + var parser = new CommandBarComponentParser(parsedElements, ServiceProvider, _listener); + + 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); + } + } + + protected override CommandBarComponentData Parse(XElement element, string fileName) + { + throw new NotSupportedException(); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileFileParser.cs index 4eaedc7..358077a 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileFileParser.cs @@ -1,9 +1,9 @@ using System; using System.Xml.Linq; +using PG.Commons.Collections; using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.DataTypes; +using PG.StarWarsGame.Engine.GameObjects; using PG.StarWarsGame.Engine.Xml.Parsers.Data; -using PG.StarWarsGame.Files.XML; using PG.StarWarsGame.Files.XML.ErrorHandling; using PG.StarWarsGame.Files.XML.Parsers; @@ -14,7 +14,7 @@ internal class GameObjectFileFileParser(IServiceProvider serviceProvider, IXmlPa { private readonly IXmlParserErrorListener? _listener = listener; - protected override void Parse(XElement element, IValueListDictionary parsedElements) + protected override void Parse(XElement element, IValueListDictionary parsedElements, string fileName) { var parser = new GameObjectParser(parsedElements, ServiceProvider, _listener); @@ -25,7 +25,7 @@ protected override void Parse(XElement element, IValueListDictionary(serviceProvider, listener) +{ + 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 ValueListDictionary(); + + foreach (var entry in texture.Elements()) + textures.Add(entry.Name.ToString(), PrimitiveParserProvider.StringParser.Parse(entry)); + + return new XmlComponentTextureData(componentId, textures, XmlLocationInfo.FromElement(texture)); + } + + protected override void Parse(XElement element, IValueListDictionary parsedElements, string fileName) + { + throw new NotSupportedException(); + } +} \ 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 index 7edc637..1de28b8 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs @@ -1,10 +1,10 @@ using System; using System.Xml.Linq; using Microsoft.Extensions.Logging; +using PG.Commons.Collections; using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.DataTypes; +using PG.StarWarsGame.Engine.Audio.Sfx; using PG.StarWarsGame.Engine.Xml.Parsers.Data; -using PG.StarWarsGame.Files.XML; using PG.StarWarsGame.Files.XML.ErrorHandling; using PG.StarWarsGame.Files.XML.Parsers; @@ -15,26 +15,19 @@ internal class SfxEventFileParser(IServiceProvider serviceProvider, IXmlParserEr { private readonly IXmlParserErrorListener? _listener = listener; - protected override void Parse(XElement element, IValueListDictionary parsedElements) + protected override void Parse(XElement element, IValueListDictionary parsedElements, string fileName) { var parser = new SfxEventParser(parsedElements, ServiceProvider, _listener); if (!element.HasElements) { - OnParseError(XmlParseErrorEventArgs.FromEmptyRoot(XmlLocationInfo.FromElement(element).XmlFile, element)); + OnParseError(XmlParseErrorEventArgs.FromEmptyRoot(element)); return; } foreach (var xElement in element.Elements()) { var sfxEvent = parser.Parse(xElement, out var nameCrc); - if (nameCrc == default) - { - var location = XmlLocationInfo.FromElement(xElement); - OnParseError(new XmlParseErrorEventArgs(location.XmlFile, xElement, XmlParseErrorKind.MissingAttribute, - $"SFXEvent has no name at location '{location}'")); - } - parsedElements.Add(nameCrc, sfxEvent); } @@ -42,11 +35,11 @@ protected override void Parse(XElement element, IValueListDictionary? XmlParseError; + + void ParseEntriesFromContainerXml( + string xmlFile, + IXmlParserErrorListener listener, + IGameRepository gameRepository, + string lookupPath, + ValueListDictionary entries, + Action? onFileParseAction = null); +} \ 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 new file mode 100644 index 0000000..f7fbf79 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs @@ -0,0 +1,109 @@ +using System; +using System.Linq; +using System.Xml; +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.Repositories; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.ErrorHandling; + +namespace PG.StarWarsGame.Engine.Xml.Parsers; + +internal sealed class XmlContainerContentParser(IServiceProvider serviceProvider) : ServiceBase(serviceProvider), IXmlContainerContentParser +{ + public event EventHandler? XmlParseError; + + private readonly IPetroglyphXmlFileParserFactory _fileParserFactory = serviceProvider.GetRequiredService(); + + public void ParseEntriesFromContainerXml( + string xmlFile, + IXmlParserErrorListener listener, + IGameRepository gameRepository, + string lookupPath, + ValueListDictionary entries, + Action? onFileParseAction = null) + { + var containerParser = _fileParserFactory.CreateFileParser(listener); + Logger.LogDebug($"Parsing container data '{xmlFile}'"); + + using var containerStream = gameRepository.TryOpenFile(xmlFile); + if (containerStream == null) + { + listener.OnXmlParseError(containerParser, XmlParseErrorEventArgs.FromMissingFile(xmlFile)); + Logger.LogWarning($"Could not find XML file '{xmlFile}'"); + + var args = new XmlContainerParserErrorEventArgs(xmlFile, true) + { + // No reason to continue + Continue = false + }; + XmlParseError?.Invoke(this, args); + return; + } + + XmlFileContainer? container; + try + { + 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(e, xmlFile, 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(listener); + + foreach (var file in xmlFiles) + { + if (onFileParseAction is not null) + onFileParseAction(file); + + using var fileStream = gameRepository.TryOpenFile(file); + + if (fileStream is null) + { + listener.OnXmlParseError(parser, XmlParseErrorEventArgs.FromMissingFile(file)); + Logger.LogWarning($"Could not find XML file '{file}'"); + + var args = new XmlContainerParserErrorEventArgs(file); + XmlParseError?.Invoke(this, args); + + if (args.Continue) + continue; + return; + } + + Logger.LogDebug($"Parsing File '{file}'"); + + try + { + parser.ParseFile(fileStream, entries); + } + catch (XmlException e) + { + listener.OnXmlParseError(parser, new XmlParseErrorEventArgs(new XmlLocationInfo(file, 0), XmlParseErrorKind.Unknown, e.Message)); + + var args = new XmlContainerParserErrorEventArgs(e, file); + 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 new file mode 100644 index 0000000..3661c21 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerParserErrorEventArgs.cs @@ -0,0 +1,40 @@ +using System.Diagnostics.CodeAnalysis; +using System.Xml; + +namespace PG.StarWarsGame.Engine.Xml.Parsers; + +internal class XmlContainerParserErrorEventArgs +{ + private bool _continue = true; + + public bool Continue + { + get => _continue; + // Once this is set to false, there is no way back. + set => _continue &= value; + } + + public bool IsContainer { get; } + + public string File { get; } + + [MemberNotNullWhen(true, nameof(Exception))] + public bool IsError => Exception is not null; + + public bool IsFileNotFound => !IsError; + + public XmlException? Exception { get; } + + public XmlContainerParserErrorEventArgs(XmlException exception, string file, bool container = false) + { + Exception = exception; + File = file; + IsContainer = container; + } + + public XmlContainerParserErrorEventArgs(string file, bool container = false) + { + File = file; + IsContainer = container; + } +} \ 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 index 5c3142c..ce2a89f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs @@ -1,52 +1,76 @@ using System; using System.Xml.Linq; using Microsoft.Extensions.DependencyInjection; +using PG.Commons.Collections; using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.DataTypes; -using PG.StarWarsGame.Files.XML; using PG.StarWarsGame.Files.XML.ErrorHandling; using PG.StarWarsGame.Files.XML.Parsers; namespace PG.StarWarsGame.Engine.Xml.Parsers; -public abstract class XmlObjectParser(IReadOnlyValueListDictionary parsedElements, IServiceProvider serviceProvider, IXmlParserErrorListener? listener = null) - : PetroglyphXmlElementParser(serviceProvider, listener) where T : XmlObject -{ - protected IReadOnlyValueListDictionary ParsedElements { get; } = parsedElements ?? throw new ArgumentNullException(nameof(parsedElements)); +public abstract class XmlObjectParser( + IReadOnlyValueListDictionary parsedElements, + IServiceProvider serviceProvider, + IXmlParserErrorListener? listener = null) + : XmlObjectParser(parsedElements, serviceProvider, listener) where TObject : XmlObject +{ + protected void Parse(TObject xmlObject, XElement element) + { + Parse(xmlObject, element, EmptyParseState.Instance); + } - protected ICrc32HashingService HashingService { get; } = serviceProvider.GetRequiredService(); + protected sealed override bool ParseTag(XElement tag, TObject xmlObject, in EmptyParseState parseState) + { + return ParseTag(tag, xmlObject); + } - public abstract T Parse(XElement element, out Crc32 nameCrc); + protected abstract bool ParseTag(XElement tag, TObject xmlObject); +} - protected abstract IPetroglyphXmlElementParser? GetParser(string tag); +public readonly struct EmptyParseState +{ + public static readonly EmptyParseState Instance = new(); +} - protected ValueListDictionary ParseXmlElement(XElement element, string? name = null) - { - var xmlProperties = new ValueListDictionary(); - foreach (var elm in element.Elements()) - { - var tagName = elm.Name.LocalName; - var parser = GetParser(tagName); +public abstract class XmlObjectParser( + IReadOnlyValueListDictionary parsedElements, + IServiceProvider serviceProvider, + IXmlParserErrorListener? listener = null) + : PetroglyphXmlElementParser(serviceProvider, listener) where TObject : XmlObject +{ + protected IReadOnlyValueListDictionary ParsedElements { get; } = + parsedElements ?? throw new ArgumentNullException(nameof(parsedElements)); + + protected ICrc32HashingService HashingService { get; } = serviceProvider.GetRequiredService(); + + public abstract TObject Parse(XElement element, out Crc32 nameCrc); - if (parser is null) + protected void Parse(TObject xmlObject, XElement element, in TParseState state) + { + foreach (var tag in element.Elements()) + { + if (!ParseTag(tag, xmlObject, state)) { - // TODO - //var nameOrPosition = name ?? XmlLocationInfo.FromElement(element).ToString(); - //Logger?.LogWarning($"Unable to find parser for tag '{tagName}' in element '{nameOrPosition}'"); - continue; + OnParseError(new XmlParseErrorEventArgs(tag, XmlParseErrorKind.UnknownNode, + $"The node '{tag.Name}' is not supported.")); + break; } - - var value = parser.Parse(elm); - - if (OnParsed(elm, tagName, value, xmlProperties, name)) - xmlProperties.Add(tagName, value); } - return xmlProperties; } - protected virtual bool OnParsed(XElement element, string tag, object value, ValueListDictionary properties, string? outerElementName) + protected abstract bool ParseTag(XElement tag, TObject xmlObject, in TParseState parseState); + + protected string GetXmlObjectName(XElement element, out Crc32 nameCrc32) { - return true; + GetNameAttributeValue(element, out var name); + nameCrc32 = HashingService.GetCrc32Upper(name.AsSpan(), PGConstants.DefaultPGEncoding); + if (nameCrc32 == 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/PetroglyphXmlParserFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs index 7a283e5..d608a16 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs @@ -1,5 +1,9 @@ using System; -using PG.StarWarsGame.Engine.DataTypes; +using PG.StarWarsGame.Engine.Audio.Sfx; +using PG.StarWarsGame.Engine.CommandBar.Xml; +using PG.StarWarsGame.Engine.GameConstants; +using PG.StarWarsGame.Engine.GameObjects; +using PG.StarWarsGame.Engine.GuiDialog.Xml; using PG.StarWarsGame.Engine.Xml.Parsers.Data; using PG.StarWarsGame.Engine.Xml.Parsers.File; using PG.StarWarsGame.Files.XML; @@ -11,7 +15,7 @@ namespace PG.StarWarsGame.Engine.Xml; internal sealed class PetroglyphXmlFileParserFactory(IServiceProvider serviceProvider) : IPetroglyphXmlFileParserFactory { - public IPetroglyphXmlFileParser GetFileParser(IXmlParserErrorListener? listener = null) + public IPetroglyphXmlFileParser CreateFileParser(IXmlParserErrorListener? listener = null) { return (IPetroglyphXmlFileParser)GetFileParser(typeof(T), listener); } @@ -21,15 +25,21 @@ private IPetroglyphXmlFileParser GetFileParser(Type type, IXmlParserErrorListene if (type == typeof(XmlFileContainer)) return new XmlFileContainerParser(serviceProvider, listener); - if (type == typeof(GameConstants)) + if (type == typeof(GameConstantsXml)) return new GameConstantsParser(serviceProvider, listener); + if (type == typeof(GuiDialogsXml)) + return new GuiDialogParser(serviceProvider, listener); + if (type == typeof(GameObject)) return new GameObjectFileFileParser(serviceProvider, listener); if (type == typeof(SfxEvent)) return new SfxEventFileParser(serviceProvider, listener); + if (type == typeof(CommandBarComponentData)) + return new CommandBarComponentFileParser(serviceProvider, listener); + throw new ParserNotFoundException(type); } } \ 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 new file mode 100644 index 0000000..bc5498b --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/CommandBarComponentTags.cs @@ -0,0 +1,108 @@ +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 new file mode 100644 index 0000000..c072c90 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/ComponentTextureKeyExtensions.cs @@ -0,0 +1,114 @@ +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/XmlObject.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObject.cs new file mode 100644 index 0000000..71840fe --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObject.cs @@ -0,0 +1,12 @@ +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.Files.ALO/Binary/Identifier/AloContentInfoIdentifier.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Identifier/AloContentInfoIdentifier.cs index 3471ccd..ea13493 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Identifier/AloContentInfoIdentifier.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Identifier/AloContentInfoIdentifier.cs @@ -1,6 +1,6 @@ using System.IO; -using PG.Commons.Binary; using PG.StarWarsGame.Files.ALO.Files; +using PG.StarWarsGame.Files.Binary; using PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; using PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/ModelFileReader.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/ModelFileReader.cs index d43116b..9e2df93 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/ModelFileReader.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/ModelFileReader.cs @@ -3,9 +3,9 @@ using System.Diagnostics; using System.IO; using System.Text; -using PG.Commons.Binary; using PG.StarWarsGame.Files.ALO.Data; using PG.StarWarsGame.Files.ALO.Services; +using PG.StarWarsGame.Files.Binary; using PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; namespace PG.StarWarsGame.Files.ALO.Binary.Reader; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/ParticleReaderV1.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/ParticleReaderV1.cs index 1f487f7..200a918 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/ParticleReaderV1.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/ParticleReaderV1.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.IO; using System.Text; -using PG.Commons.Binary; using PG.StarWarsGame.Files.ALO.Data; using PG.StarWarsGame.Files.ALO.Services; +using PG.StarWarsGame.Files.Binary; using PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; namespace PG.StarWarsGame.Files.ALO.Binary.Reader; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Files/AloFileInformation.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Files/AloFileInformation.cs index 43df83c..4e88947 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Files/AloFileInformation.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Files/AloFileInformation.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics.CodeAnalysis; -using PG.Commons.Files; namespace PG.StarWarsGame.Files.ALO.Files; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Files/IAloFile.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Files/IAloFile.cs index 4812535..db28fe0 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Files/IAloFile.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Files/IAloFile.cs @@ -1,5 +1,4 @@ -using PG.Commons.Files; -using PG.StarWarsGame.Files.ALO.Data; +using PG.StarWarsGame.Files.ALO.Data; using PG.StarWarsGame.Files.ChunkFiles.Files; namespace PG.StarWarsGame.Files.ALO.Files; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Files/Models/AloModelFile.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Files/Models/AloModelFile.cs index 07a8723..48408d1 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Files/Models/AloModelFile.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Files/Models/AloModelFile.cs @@ -1,5 +1,4 @@ using System; -using PG.Commons.Files; using PG.StarWarsGame.Files.ALO.Data; namespace PG.StarWarsGame.Files.ALO.Files.Models; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Files/Particles/AloParticleFile.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Files/Particles/AloParticleFile.cs index 19008ab..2178e2d 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Files/Particles/AloParticleFile.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Files/Particles/AloParticleFile.cs @@ -1,5 +1,4 @@ using System; -using PG.Commons.Files; using PG.StarWarsGame.Files.ALO.Data; namespace PG.StarWarsGame.Files.ALO.Files.Particles; 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 490459c..4fd3dd4 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 @@ -28,4 +28,7 @@ + + + \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Services/AloFileService.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Services/AloFileService.cs index d21191b..d98e3f4 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Services/AloFileService.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Services/AloFileService.cs @@ -1,8 +1,6 @@ using System; using System.IO; -using System.IO.Abstractions; using Microsoft.Extensions.DependencyInjection; -using PG.Commons.Files; using PG.Commons.Services; using PG.Commons.Utilities; using PG.StarWarsGame.Files.ALO.Binary; @@ -56,7 +54,7 @@ public IAloFile Load(Stre var alo = reader.Read(); - var filePath = GetFilePath(stream, out var isInMeg); + var filePath = stream.GetFilePath(out var isInMeg); var fileInfo = new AloFileInformation(filePath, isInMeg, contentInfo); if (alo is AlamoModel model) @@ -67,23 +65,7 @@ public IAloFile Load(Stre throw new InvalidOperationException(); } - - private static string GetFilePath(Stream stream, out bool isInMeg) - { - isInMeg = false; - if (stream is FileStream fileStream) - return fileStream.Name; - if (stream is FileSystemStream fileSystemStream) - return fileSystemStream.Name; - if (stream is IMegFileDataStream megFileDataStream) - { - isInMeg = true; - return megFileDataStream.EntryPath; - } - - throw new InvalidOperationException(); - } - + public AloContentInfo GetAloContentInfo(Stream stream) { return _aloContentIdentifier.GetContentInfo(stream); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Services/IAloFileService.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Services/IAloFileService.cs index ad0c099..e0cd995 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Services/IAloFileService.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Services/IAloFileService.cs @@ -1,5 +1,4 @@ using System.IO; -using PG.Commons.Files; using PG.StarWarsGame.Files.ALO.Data; using PG.StarWarsGame.Files.ALO.Files; using PG.StarWarsGame.Files.ALO.Files.Models; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs index 782be52..01d085a 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs @@ -15,9 +15,9 @@ IChunkData IChunkFileReader.Read() return Read(); } - protected override void DisposeManagedResources() + protected override void DisposeResources() { - base.DisposeManagedResources(); + base.DisposeResources(); ChunkReader.Dispose(); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkReader.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkReader.cs index 60105e5..ca188c3 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkReader.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkReader.cs @@ -2,28 +2,20 @@ using System.IO; using System.Text; using AnakinRaW.CommonUtilities; -using PG.Commons.Utilities; +using PG.StarWarsGame.Files.Binary; using PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; public class ChunkReader : DisposableObject { - private readonly BinaryReader _binaryReader; + private readonly PetroglyphBinaryReader _binaryReader; public ChunkReader(Stream stream, bool leaveOpen = false) { if (stream == null) throw new ArgumentNullException(nameof(stream)); - - // Using default encoding here is OK as we don't read strings using the .NET methods. - _binaryReader = new BinaryReader(stream, Encoding.Default, leaveOpen); - } - - protected override void DisposeManagedResources() - { - base.DisposeManagedResources(); - _binaryReader.Dispose(); + _binaryReader = new PetroglyphBinaryReader(stream, leaveOpen); } public ChunkMetadata ReadChunk() @@ -79,17 +71,29 @@ public void Skip(int bytesToSkip) public string ReadString(int size, Encoding encoding, bool zeroTerminated, ref int readSize) { - var value = _binaryReader.ReadString(size, encoding, zeroTerminated); + var value = ReadString(encoding, size, zeroTerminated); readSize += size; return value; } public string ReadString(int size, Encoding encoding, bool zeroTerminated) { - var value = _binaryReader.ReadString(size, encoding, zeroTerminated); + var value = ReadString(encoding, size, zeroTerminated); return value; } + private string ReadString(Encoding encoding, int size, bool zeroTerminated) + { + try + { + return _binaryReader.ReadString(encoding, size, zeroTerminated); + } + catch (Exception e) + { + throw new BinaryCorruptedException($"Unable to read string: {e.Message}", e); + } + } + public ChunkMetadata? TryReadChunk() { var _ = 0; @@ -100,16 +104,15 @@ public string ReadString(int size, Encoding encoding, bool zeroTerminated) { if (_binaryReader.BaseStream.Position == _binaryReader.BaseStream.Length) return null; - try - { - var chunk = ReadChunk(); - size += 8; - return chunk; - } - catch (EndOfStreamException e) - { - Console.WriteLine(e); - throw; - } + + var chunk = ReadChunk(); + size += 8; + return chunk; + } + + protected override void DisposeResources() + { + base.DisposeResources(); + _binaryReader.Dispose(); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Files/IChunkFile.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Files/IChunkFile.cs index 619e0ba..8b03a3b 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Files/IChunkFile.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Files/IChunkFile.cs @@ -1,5 +1,4 @@ -using PG.Commons.Files; -using PG.StarWarsGame.Files.ChunkFiles.Data; +using PG.StarWarsGame.Files.ChunkFiles.Data; namespace PG.StarWarsGame.Files.ChunkFiles.Files; 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 d909270..8daab61 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 @@ -16,6 +16,9 @@ snupkg - + + + + \ 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 index 1ea7d70..afaa2ce 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlParseErrorEventArgs.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlParseErrorEventArgs.cs @@ -6,7 +6,7 @@ namespace PG.StarWarsGame.Files.XML.ErrorHandling; public class XmlParseErrorEventArgs : EventArgs { - public string File { get; } + public XmlLocationInfo Location { get; } public XElement? Element { get; } @@ -14,24 +14,29 @@ public class XmlParseErrorEventArgs : EventArgs public string Message { get; } - public XmlParseErrorEventArgs(string file, XElement? element, XmlParseErrorKind errorKind, string message) + public XmlParseErrorEventArgs(XElement element, XmlParseErrorKind errorKind, string message) { - ThrowHelper.ThrowIfNullOrEmpty(file); - File = file; - Element = element; + 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(file, null, XmlParseErrorKind.MissingFile, $"XML file '{file}' not found."); + return new XmlParseErrorEventArgs(new XmlLocationInfo(file, null), XmlParseErrorKind.MissingFile, "XML file not found."); } - public static XmlParseErrorEventArgs FromEmptyRoot(string file, XElement element) + public static XmlParseErrorEventArgs FromEmptyRoot(XElement element) { - ThrowHelper.ThrowIfNullOrEmpty(file); - return new XmlParseErrorEventArgs(file, element, XmlParseErrorKind.EmptyRoot, $"XML file '{file}' has an empty root node."); + 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 ea329d9..8465c2f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlParseErrorKind.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlParseErrorKind.cs @@ -39,4 +39,12 @@ public enum XmlParseErrorKind /// The XML file does not start with the XML header. /// DataBeforeHeader = 8, + /// + /// The XML file is missing an expected node. + /// + MissingNode = 9, + /// + /// The XML element contains an unsupported tag. + /// + UnknownNode = 10 } \ 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 32d7562..9f9f1cb 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 @@ -14,14 +14,19 @@ true snupkg + true + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlFileParser.cs index 2c81137..5a3875a 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlFileParser.cs @@ -1,4 +1,5 @@ using System.IO; +using PG.Commons.Collections; using PG.Commons.Hashing; namespace PG.StarWarsGame.Files.XML.Parsers; @@ -10,7 +11,7 @@ public interface IPetroglyphXmlFileParser : IPetroglyphXmlParser public interface IPetroglyphXmlFileParser : IPetroglyphXmlParser, IPetroglyphXmlFileParser { - public new T ParseFile(Stream stream); + public new T? ParseFile(Stream stream); public void ParseFile(Stream stream, IValueListDictionary parsedEntries); } \ 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 index f9b64f9..be0d855 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs @@ -4,11 +4,15 @@ using System.Text; using System.Xml; using System.Xml.Linq; -using AnakinRaW.CommonUtilities.FileSystem; using Microsoft.Extensions.DependencyInjection; using PG.Commons.Hashing; using PG.Commons.Utilities; using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.Commons.Collections; + +#if NETSTANDARD2_0 +using AnakinRaW.CommonUtilities.FileSystem; +#endif namespace PG.StarWarsGame.Files.XML.Parsers; @@ -22,22 +26,33 @@ public abstract class PetroglyphXmlFileParser(IServiceProvider serviceProvide public T ParseFile(Stream xmlStream) { - var root = GetRootElement(xmlStream); - return root is null ? default! : Parse(root); + var root = GetRootElement(xmlStream, out var fileName); + if (root is null) + OnParseError(new XmlParseErrorEventArgs(new XmlLocationInfo(fileName, 0), XmlParseErrorKind.EmptyRoot, + "Unable to get root node from XML file.")); + return root is null ? default! : Parse(root, fileName); } public void ParseFile(Stream xmlStream, IValueListDictionary parsedEntries) { - var root = GetRootElement(xmlStream); + var root = GetRootElement(xmlStream, out var fileName); if (root is not null) - Parse(root, parsedEntries); + Parse(root, parsedEntries, fileName); } - protected abstract void Parse(XElement element, IValueListDictionary parsedElements); + public sealed override T Parse(XElement element) + { + var fileName = GetStrippedFileName(XmlLocationInfo.FromElement(element).XmlFile); + return Parse(element, fileName); + } + + protected abstract T Parse(XElement element, string fileName); + + protected abstract void Parse(XElement element, IValueListDictionary parsedElements, string fileName); - private XElement? GetRootElement(Stream xmlStream) + private XElement? GetRootElement(Stream xmlStream, out string fileName) { - var fileName = GetStrippedFileName(xmlStream.GetFilePath()); + 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."); @@ -59,7 +74,7 @@ public void ParseFile(Stream xmlStream, IValueListDictionary parsedEnt return doc.Root; } - private string GetStrippedFileName(string filePath) + protected string GetStrippedFileName(string filePath) { if (!_fileSystem.Path.IsPathFullyQualified(filePath)) return filePath; @@ -87,8 +102,8 @@ private void SkipLeadingWhiteSpace(string fileName, Stream stream) } if (count != 0) - _listener?.OnXmlParseError(this, new XmlParseErrorEventArgs(fileName, null, - XmlParseErrorKind.DataBeforeHeader, $"XML header is not the first entry of the file '{fileName}'")); + _listener?.OnXmlParseError(this, new XmlParseErrorEventArgs(new XmlLocationInfo(fileName, 0), + XmlParseErrorKind.DataBeforeHeader, $"XML header is not the first entry of the XML file.")); stream.Position = count; } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlParser.cs index 21b4b43..cdebbf3 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlParser.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using PG.StarWarsGame.Files.XML.ErrorHandling; using PG.StarWarsGame.Files.XML.Parsers.Primitives; +using System.Linq; namespace PG.StarWarsGame.Files.XML.Parsers; @@ -32,8 +33,34 @@ protected virtual void OnParseError(XmlParseErrorEventArgs e) _errorListener?.OnXmlParseError(this, e); } + protected string GetTagName(XElement element) + { + return element.Name.LocalName; + } + + protected bool GetNameAttributeValue(XElement element, out string value) + { + return GetAttributeValue(element, "Name", out value!, string.Empty); + } + + protected bool GetAttributeValue(XElement element, string attribute, out string? value, string? defaultValue = null) + { + 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}'")); + return false; + } + + value = nameAttribute.Value; + return true; + } + object IPetroglyphXmlParser.Parse(XElement element) { - return Parse(element); + return Parse(element)!; } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/IPrimitiveParserProvider.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/IPrimitiveParserProvider.cs index 3be1a0f..0725d5e 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/IPrimitiveParserProvider.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/IPrimitiveParserProvider.cs @@ -18,5 +18,7 @@ public interface IPrimitiveParserProvider PetroglyphXmlBooleanParser BooleanParser { get; } + PetroglyphXmlVector2FParser Vector2FParser { get; } + CommaSeparatedStringKeyValueListParser CommaSeparatedStringKeyValueListParser { get; } } \ No newline at end of file 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 68d8ec7..541a148 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlByteParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlByteParser.cs @@ -18,9 +18,8 @@ public override byte Parse(XElement element) var asByte = (byte)intValue; if (intValue != asByte) { - var location = XmlLocationInfo.FromElement(element); - OnParseError(new XmlParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.InvalidValue, - $"Expected a byte value (0 - 255) but got value '{intValue}' at {location}")); + OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, + $"Expected a byte value (0 - 255) but got value '{intValue}'.")); } return asByte; 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 6691061..8c60c60 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlFloatParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlFloatParser.cs @@ -12,19 +12,37 @@ internal PetroglyphXmlFloatParser(IServiceProvider serviceProvider, IPrimitiveXm { } - public override float Parse(XElement element) + public float Parse(string value, XElement element) { // The engine always loads FP numbers a long double and then converts that result to float - if (!double.TryParse(element.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var doubleValue)) + if (!double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var doubleValue)) { - var location = XmlLocationInfo.FromElement(element); - OnParseError(new XmlParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.MalformedValue, - $"Expected double but got value '{element.Value}' at {location}")); + OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.MalformedValue, + $"Expected double but got value '{value}'.")); return 0.0f; } + return (float)doubleValue; } + public override float Parse(XElement element) + { + return Parse(element.Value, element); + } + + 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}'.")); + } + + return corrected; + } + protected override void OnParseError(XmlParseErrorEventArgs e) { Logger?.LogWarning(e.Message); 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 02400b8..a360211 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlIntegerParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlIntegerParser.cs @@ -2,6 +2,7 @@ using System; using System.Xml.Linq; using PG.StarWarsGame.Files.XML.ErrorHandling; +using System.Runtime.CompilerServices; namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; @@ -19,18 +20,53 @@ public override int Parse(XElement element) if (!int.TryParse(element.Value, out var i)) { - var location = XmlLocationInfo.FromElement(element); - OnParseError(new XmlParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.MalformedValue, - $"Expected integer but got '{element.Value}' at {location}")); + OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.MalformedValue, + $"Expected integer but got '{element.Value}'.")); return 0; } 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; + } + protected override void OnParseError(XmlParseErrorEventArgs e) { Logger?.LogWarning(e.Message); base.OnParseError(e); } -} \ No newline at end of file +} + +internal static class PGMath +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Clamp(int value, int min, int max) + { + if (min > max) + throw new ArgumentException("min cannot be larger than max."); + if (value < min) + return min; + return value > max ? max : value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte Clamp(byte value, byte min, byte max) + { + if (min > max) + throw new ArgumentException("min cannot be larger than max."); + if (value < min) + return min; + return value > max ? max : value; + } +} + 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 82fbb38..6948952 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlLooseStringListParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlLooseStringListParser.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; using PG.StarWarsGame.Files.XML.ErrorHandling; @@ -31,8 +32,7 @@ public override IList Parse(XElement element) if (trimmedValued.Length > 0x2000) { - var location = XmlLocationInfo.FromElement(element); - OnParseError(new XmlParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.TooLongData, + OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.TooLongData, $"Input value is too long '{trimmedValued.Length}' at {XmlLocationInfo.FromElement(element)}")); return Array.Empty(); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlMax100ByteParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlMax100ByteParser.cs index af72d8b..8c6df64 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlMax100ByteParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlMax100ByteParser.cs @@ -9,7 +9,6 @@ public sealed class PetroglyphXmlMax100ByteParser : PetroglyphXmlPrimitiveElemen { internal PetroglyphXmlMax100ByteParser(IServiceProvider serviceProvider, IPrimitiveXmlParserErrorListener listener) : base(serviceProvider, listener) { - } public override byte Parse(XElement element) @@ -22,23 +21,36 @@ public override byte Parse(XElement element) var asByte = (byte)intValue; if (intValue != asByte) { - var location = XmlLocationInfo.FromElement(element); - - OnParseError(new XmlParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.InvalidValue, - $"Expected a byte value (0 - 255) but got value '{intValue}' at {location}")); + 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) { - var location = XmlLocationInfo.FromElement(element); - OnParseError(new XmlParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.InvalidValue, - $"Expected a byte value (0 - 100) but got value '{asByte}' at {location}")); + 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) + Logger?.LogWarning("Upper bound for clamp range is above 100 for a parser that is meant to be capped to value 100."); + + 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; + } + protected override void OnParseError(XmlParseErrorEventArgs e) { Logger?.LogWarning(e.Message); 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 2206f12..c80fddf 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlUnsignedIntegerParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlUnsignedIntegerParser.cs @@ -18,10 +18,8 @@ public override uint Parse(XElement element) var asUint = (uint)intValue; if (intValue != asUint) { - var location = XmlLocationInfo.FromElement(element); - - OnParseError(new XmlParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.InvalidValue, - $"Expected unsigned integer but got '{intValue}' at {location}")); + OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, + $"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 new file mode 100644 index 0000000..a099ad1 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlVector2FParser.cs @@ -0,0 +1,34 @@ +using System; +using System.Numerics; +using System.Xml.Linq; +using PG.StarWarsGame.Files.XML.ErrorHandling; + +namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; + +public sealed class PetroglyphXmlVector2FParser : PetroglyphXmlPrimitiveElementParser +{ + internal PetroglyphXmlVector2FParser(IServiceProvider serviceProvider, IPrimitiveXmlParserErrorListener listener) : base(serviceProvider, listener) + { + } + + public override Vector2 Parse(XElement element) + { + var listOfValues = PrimitiveParserProvider.LooseStringListParser.Parse(element); + + if (listOfValues.Count == 0) + return default; + + var floatParser = PrimitiveParserProvider.FloatParser; + + if (listOfValues.Count == 1) + { + var value = floatParser.Parse(listOfValues[0], element); + return new Vector2(value, 0.0f); + } + + var value1 = floatParser.Parse(listOfValues[0], element); + var value2 = floatParser.Parse(listOfValues[1], element); + + return new Vector2(value1, value2); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PrimitiveParserProvider.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PrimitiveParserProvider.cs index 747c83a..3d17ebc 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PrimitiveParserProvider.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PrimitiveParserProvider.cs @@ -17,6 +17,8 @@ internal class PrimitiveParserProvider(IServiceProvider serviceProvider) : IPrim private PetroglyphXmlByteParser _byteParser = null!; private PetroglyphXmlMax100ByteParser _max100ByteParser = null!; private PetroglyphXmlBooleanParser _booleanParser = null!; + private PetroglyphXmlVector2FParser _vector2FParser = null!; + private CommaSeparatedStringKeyValueListParser _commaSeparatedStringKeyValueListParser = null!; public PetroglyphXmlStringParser StringParser => @@ -43,6 +45,9 @@ internal class PrimitiveParserProvider(IServiceProvider serviceProvider) : IPrim public PetroglyphXmlBooleanParser BooleanParser => LazyInitializer.EnsureInitialized(ref _booleanParser, () => new PetroglyphXmlBooleanParser(serviceProvider, _primitiveParserErrorListener)); + public PetroglyphXmlVector2FParser Vector2FParser => + LazyInitializer.EnsureInitialized(ref _vector2FParser, () => new PetroglyphXmlVector2FParser(serviceProvider, _primitiveParserErrorListener)); + public CommaSeparatedStringKeyValueListParser CommaSeparatedStringKeyValueListParser => LazyInitializer.EnsureInitialized(ref _commaSeparatedStringKeyValueListParser, () => new CommaSeparatedStringKeyValueListParser(serviceProvider, _primitiveParserErrorListener)); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/XmlFileContainerParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/XmlFileContainerParser.cs index 20aeace..3769213 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/XmlFileContainerParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/XmlFileContainerParser.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Xml.Linq; +using PG.Commons.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Files.XML.ErrorHandling; @@ -11,12 +12,12 @@ public class XmlFileContainerParser(IServiceProvider serviceProvider, IXmlParser { protected override bool LoadLineInfo => false; - protected override void Parse(XElement element, IValueListDictionary parsedElements) + protected override void Parse(XElement element, IValueListDictionary parsedElements, string fileName) { throw new NotSupportedException(); } - public override XmlFileContainer Parse(XElement element) + protected override XmlFileContainer Parse(XElement element, string fileNaem) { var files = new List(); foreach (var child in element.Elements()) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ValueListDictionary.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ValueListDictionary.cs deleted file mode 100644 index 73f8aab..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ValueListDictionary.cs +++ /dev/null @@ -1,312 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using AnakinRaW.CommonUtilities.Collections; - -namespace PG.StarWarsGame.Files.XML; - - -public interface IReadOnlyValueListDictionary : IEnumerable> where TKey : notnull -{ - ICollection Values { get; } - - ICollection Keys { get; } - - int Count { get; } - - TKey this[int index] { get; } - - TValue GetValueAtIndex(int index); - - bool ContainsKey(TKey key); - - ReadOnlyFrugalList GetValues(TKey key); - - TValue GetLastValue(TKey key); - - TValue GetFirstValue(TKey key); - - bool TryGetFirstValue(TKey key, [NotNullWhen(true)] out TValue value); - - bool TryGetLastValue(TKey key, [NotNullWhen(true)] out TValue value); - - bool TryGetValues(TKey key, out ReadOnlyFrugalList values); -} - -public interface IValueListDictionary : IReadOnlyValueListDictionary where TKey : notnull -{ - bool Add(TKey key, TValue value); -} - -// NOT THREAD-SAFE! -public class ValueListDictionary : IValueListDictionary where TKey : notnull -{ - private readonly List _insertionTrackingList = new(); - private readonly Dictionary _singleValueDictionary = new (); - private readonly Dictionary> _multiValueDictionary = new(); - - private readonly EqualityComparer _equalityComparer = EqualityComparer.Default; - - public int Count => _insertionTrackingList.Count; - - public TKey this[int index] => _insertionTrackingList[index]; - - public ICollection Keys => _singleValueDictionary.Keys.Concat(_multiValueDictionary.Keys).ToList(); - - public ICollection Values => this.Select(x => x.Value).ToList(); - - public TValue GetValueAtIndex(int index) - { - if (index < 0 || index >= Count) - throw new ArgumentOutOfRangeException(nameof(index)); - - var key = this[index]; - if (_singleValueDictionary.TryGetValue(key, out var value)) - return value; - - if (index == 0) - return _multiValueDictionary[key].First(); - - if (index == Count - 1) - return _multiValueDictionary[key].Last(); - - var keyCount = 0; - foreach (var k in _insertionTrackingList.Take(index + 1)) - { - if (_equalityComparer.Equals(key, k)) - keyCount++; - } - - return _multiValueDictionary[key][keyCount - 1]; - } - - public bool ContainsKey(TKey key) - { - return _singleValueDictionary.ContainsKey(key) || _multiValueDictionary.ContainsKey(key); - } - - public bool Add(TKey key, TValue value) - { - if (key is null) - throw new ArgumentNullException(nameof(key)); - - _insertionTrackingList.Add(key); - - if (!_singleValueDictionary.ContainsKey(key)) - { - if (!_multiValueDictionary.TryGetValue(key, out var list)) - { - _singleValueDictionary.Add(key, value); - return false; - } - - list.Add(value); - return true; - } - - Debug.Assert(_multiValueDictionary.ContainsKey(key) == false); - - var firstValue = _singleValueDictionary[key]; - _singleValueDictionary.Remove(key); - - _multiValueDictionary.Add(key, [ - firstValue, - value - ]); - - return true; - } - - public TValue GetLastValue(TKey key) - { - if (_singleValueDictionary.TryGetValue(key, out var value)) - return value; - - if (_multiValueDictionary.TryGetValue(key, out var valueList)) - return valueList.Last(); - - throw new KeyNotFoundException($"The key '{key}' was not found."); - } - - public TValue GetFirstValue(TKey key) - { - if (_singleValueDictionary.TryGetValue(key, out var value)) - return value; - - if (_multiValueDictionary.TryGetValue(key, out var valueList)) - return valueList.First(); - - throw new KeyNotFoundException($"The key '{key}' was not found."); - } - - public ReadOnlyFrugalList GetValues(TKey key) - { - if (TryGetValues(key, out var values)) - return values; - - throw new KeyNotFoundException($"The key '{key}' was not found."); - - } - - public bool TryGetFirstValue(TKey key, [NotNullWhen(true)] out TValue value) - { - if (_singleValueDictionary.TryGetValue(key, out value!)) - return true; - - if (_multiValueDictionary.TryGetValue(key, out var valueList)) - { - value = valueList.First()!; - return true; - } - - return false; - } - - public bool TryGetLastValue(TKey key, [NotNullWhen(true)] out TValue value) - { - if (_singleValueDictionary.TryGetValue(key, out value!)) - return true; - - if (_multiValueDictionary.TryGetValue(key, out var valueList)) - { - value = valueList.Last()!; - return true; - } - - return false; - } - - public bool TryGetValues(TKey key, out ReadOnlyFrugalList values) - { - if (_singleValueDictionary.TryGetValue(key, out var value)) - { - values = new ReadOnlyFrugalList(value); - return true; - } - - if (_multiValueDictionary.TryGetValue(key, out var valueList)) - { - values = new ReadOnlyFrugalList(valueList); - return true; - } - - values = ReadOnlyFrugalList.Empty; - return false; - } - - public IEnumerator> GetEnumerator() - { - return new Enumerator(this); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - public struct Enumerator : IEnumerator> - { - private Dictionary.Enumerator _singleEnumerator; - private Dictionary>.Enumerator _multiEnumerator; - private List.Enumerator _currentListEnumerator = default; - private bool _isMultiEnumeratorActive = false; - - internal Enumerator(ValueListDictionary valueListDictionary) - { - _singleEnumerator = valueListDictionary._singleValueDictionary.GetEnumerator(); - _multiEnumerator = valueListDictionary._multiValueDictionary.GetEnumerator(); - } - - public KeyValuePair Current => - _isMultiEnumeratorActive - ? new KeyValuePair(_multiEnumerator.Current.Key, _currentListEnumerator.Current) - : _singleEnumerator.Current; - - object IEnumerator.Current => Current; - - public bool MoveNext() - { - if (_singleEnumerator.MoveNext()) - return true; - - if (_isMultiEnumeratorActive) - { - if (_currentListEnumerator.MoveNext()) - return true; - _isMultiEnumeratorActive = false; - } - - if (_multiEnumerator.MoveNext()) - { - _currentListEnumerator = _multiEnumerator.Current.Value.GetEnumerator(); - _isMultiEnumeratorActive = true; - return _currentListEnumerator.MoveNext(); - } - - return false; - } - - public void Reset() - { - throw new NotSupportedException(); - } - - public void Dispose() - { - _singleEnumerator.Dispose(); - _multiEnumerator.Dispose(); - } - } -} - -public static class ValueListDictionaryExtensions -{ - public static IEnumerable AggregateValues( - this IReadOnlyValueListDictionary valueListDictionary, - ISet keys, Predicate filter, - AggregateStrategy aggregateStrategy) - where TKey : notnull - where T : TValue - { - foreach (var key in keys) - { - if (!valueListDictionary.ContainsKey(key)) - continue; - if (aggregateStrategy == AggregateStrategy.MultipleValuesPerKey) - { - foreach (var value in valueListDictionary.GetValues(key)) - { - if (value is not null) - { - var typedValue = (T)value; - if (filter(typedValue)) - yield return typedValue; - } - - } - } - else - { - var value = aggregateStrategy == AggregateStrategy.FirstValuePerKey - ? valueListDictionary.GetFirstValue(key) - : valueListDictionary.GetLastValue(key); - if (value is not null) - { - var typedValue = (T)value; - if (filter(typedValue)) - yield return typedValue; - } - } - } - } - - public enum AggregateStrategy - { - FirstValuePerKey, - LastValuePerKey, - MultipleValuesPerKey, - } -} \ No newline at end of file