diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be274ae..2d7d7f3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,7 +48,7 @@ jobs: # use publish for .NET Core run: dotnet publish ${{ env.TOOL_PROJ_PATH }} --configuration Release -f net10.0 --output ./releases/net10.0 /p:DebugType=None /p:DebugSymbols=false - name: Upload a Build Artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: Binary Releases path: ./releases @@ -68,7 +68,7 @@ jobs: with: fetch-depth: 0 submodules: recursive - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: name: Binary Releases path: ./releases @@ -103,7 +103,7 @@ jobs: # Change into the artifacts directory to avoid including the directory itself in the zip archive working-directory: ./releases/net10.0 run: zip -r ../ModVerify-Net10.zip . - - uses: dotnet/nbgv@v0.4.2 + - uses: dotnet/nbgv@v0.5.1 id: nbgv - name: Create GitHub release # Create a GitHub release on push to main only diff --git a/Directory.Build.props b/Directory.Build.props index 5110cdb..3e2f119 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -33,20 +33,17 @@ - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - + all 3.9.50 - + \ No newline at end of file diff --git a/aet.png b/aet.png new file mode 100644 index 0000000..ac47943 Binary files /dev/null and b/aet.png differ diff --git a/modules/ModdingToolBase b/modules/ModdingToolBase index 5103bad..da072f4 160000 --- a/modules/ModdingToolBase +++ b/modules/ModdingToolBase @@ -1 +1 @@ -Subproject commit 5103bad6f09ba88061ccbc36ee285ee9300744cc +Subproject commit da072f43e6b85aab35b43d11f6b36eab61bdcfa6 diff --git a/src/ModVerify.CliApp/App/CreateBaselineAction.cs b/src/ModVerify.CliApp/App/CreateBaselineAction.cs index b0eb880..b776e09 100644 --- a/src/ModVerify.CliApp/App/CreateBaselineAction.cs +++ b/src/ModVerify.CliApp/App/CreateBaselineAction.cs @@ -2,10 +2,10 @@ using AET.ModVerify.App.Settings; using AET.ModVerify.App.Utilities; using AET.ModVerify.Reporting; +using AET.ModVerify.Reporting.Baseline; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; using System.IO.Abstractions; using System.Threading.Tasks; @@ -26,16 +26,14 @@ protected override void PrintAction(VerificationTarget target) Console.WriteLine(); } - protected override async Task ProcessVerifyFindings( - VerificationTarget verificationTarget, - IReadOnlyCollection allErrors) + protected override async Task ProcessResult(VerificationResult result) { var baselineFactory = ServiceProvider.GetRequiredService(); - var baseline = baselineFactory.CreateBaseline(verificationTarget, Settings, allErrors); + var baseline = baselineFactory.CreateBaseline(result.Target, Settings, result.Errors); var fullPath = _fileSystem.Path.GetFullPath(Settings.NewBaselinePath); Logger?.LogInformation(ModVerifyConstants.ConsoleEventId, - "Writing Baseline to '{FullPath}' with {Number} findings", fullPath, allErrors.Count); + "Writing Baseline to '{FullPath}' with {Number} findings", fullPath, result.Errors.Count); await baselineFactory.WriteBaselineAsync(baseline, Settings.NewBaselinePath); @@ -43,7 +41,7 @@ protected override async Task ProcessVerifyFindings( Console.WriteLine(); Console.ForegroundColor = ConsoleColor.DarkGreen; - Console.WriteLine($"Baseline for {verificationTarget.Name} created."); + Console.WriteLine($"Baseline for {result.Target.Name} created."); Console.ResetColor(); return ModVerifyConstants.Success; diff --git a/src/ModVerify.CliApp/App/ModVerifyApplicationAction.cs b/src/ModVerify.CliApp/App/ModVerifyApplicationAction.cs index b3f12f7..113e633 100644 --- a/src/ModVerify.CliApp/App/ModVerifyApplicationAction.cs +++ b/src/ModVerify.CliApp/App/ModVerifyApplicationAction.cs @@ -1,13 +1,13 @@ using System; -using System.Collections.Generic; using System.IO.Abstractions; using System.Threading.Tasks; using AET.ModVerify.App.GameFinder; using AET.ModVerify.App.Reporting; using AET.ModVerify.App.Settings; using AET.ModVerify.App.TargetSelectors; -using AET.ModVerify.Pipeline; using AET.ModVerify.Reporting; +using AET.ModVerify.Reporting.Baseline; +using AET.ModVerify.Reporting.Suppressions; using AnakinRaW.ApplicationBase; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -71,43 +71,55 @@ public async Task ExecuteAsync() PrintAction(verificationTarget); - var allErrors = await VerifyTargetAsync(verificationTarget) + var verificationResult = await VerifyTargetAsync(verificationTarget) .ConfigureAwait(false); - return await ProcessVerifyFindings(verificationTarget, allErrors); + return await ProcessResult(verificationResult); } - protected abstract Task ProcessVerifyFindings( - VerificationTarget verificationTarget, - IReadOnlyCollection allErrors); + protected abstract Task ProcessResult(VerificationResult result); protected abstract VerificationBaseline GetBaseline(VerificationTarget verificationTarget); - private async Task> VerifyTargetAsync(VerificationTarget verificationTarget) + private async Task VerifyTargetAsync(VerificationTarget verificationTarget) { var progressReporter = new VerifyConsoleProgressReporter(verificationTarget.Name, Settings.ReportSettings); var baseline = GetBaseline(verificationTarget); var suppressions = GetSuppressions(); - using var verifyPipeline = new GameVerifyPipeline( - verificationTarget, - Settings.VerifyPipelineSettings, - progressReporter, - new EngineInitializeProgressReporter(verificationTarget.Engine), - baseline, - suppressions, - ServiceProvider); - try { + var verifierService = ServiceProvider.GetRequiredService(); + Logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Verifying '{Target}'...", verificationTarget.Name); - await verifyPipeline.RunAsync().ConfigureAwait(false); + + var verificationResult = await verifierService.VerifyAsync( + verificationTarget, + Settings.VerifierServiceSettings, + baseline, + suppressions, + progressReporter, + new EngineInitializeProgressReporter(verificationTarget.Engine)); + progressReporter.Report(string.Empty, 1.0); - } - catch (OperationCanceledException) - { - Logger?.LogWarning(ModVerifyConstants.ConsoleEventId, "Verification stopped due to enabled failFast setting."); + + switch (verificationResult.Status) + { + case VerificationCompletionStatus.CompletedFailFast: + Logger?.LogWarning(ModVerifyConstants.ConsoleEventId, "Verification stopped due to enabled failFast setting."); + break; + case VerificationCompletionStatus.Cancelled: + Logger?.LogWarning(ModVerifyConstants.ConsoleEventId, "Verification was cancelled."); + break; + case VerificationCompletionStatus.Completed: + default: + Logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Verification completed successfully."); + break; + } + + return verificationResult; + } catch (Exception e) { @@ -119,9 +131,6 @@ private async Task> VerifyTargetAsync(Ver { progressReporter.Dispose(); } - - Logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Finished verification"); - return verifyPipeline.FilteredErrors; } private SuppressionList GetSuppressions() diff --git a/src/ModVerify.CliApp/App/VerifyAction.cs b/src/ModVerify.CliApp/App/VerifyAction.cs index 0cfea0e..e17d7bc 100644 --- a/src/ModVerify.CliApp/App/VerifyAction.cs +++ b/src/ModVerify.CliApp/App/VerifyAction.cs @@ -1,12 +1,14 @@ using AET.ModVerify.App.Reporting; using AET.ModVerify.App.Settings; +using AET.ModVerify.App.Utilities; using AET.ModVerify.Reporting; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using AET.ModVerify.App.Utilities; +using AET.ModVerify.Reporting.Reporters; +using AET.ModVerify.Reporting.Baseline; namespace AET.ModVerify.App; @@ -23,20 +25,27 @@ protected override void PrintAction(VerificationTarget target) Console.WriteLine(); } - protected override async Task ProcessVerifyFindings( - VerificationTarget verificationTarget, - IReadOnlyCollection allErrors) + protected override async Task ProcessResult(VerificationResult result) { Logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "Reporting Errors..."); - var reportBroker = new VerificationReportBroker(ServiceProvider); - await reportBroker.ReportAsync(allErrors); + var reportBroker = new VerificationReportBroker(CreateReporters(), ServiceProvider); + + result = result with + { + Target = result.Target with + { + Location = result.Target.Location.MaskUsername() + } + }; + + await reportBroker.ReportAsync(result); if (Settings.AppFailsOnMinimumSeverity.HasValue && - allErrors.Any(x => x.Severity >= Settings.AppFailsOnMinimumSeverity)) + result.Errors.Any(x => x.Severity >= Settings.AppFailsOnMinimumSeverity)) { Logger?.LogInformation(ModVerifyConstants.ConsoleEventId, "The verification of {Target} completed with findings of the specified failure severity {Severity}", - verificationTarget.Name, Settings.AppFailsOnMinimumSeverity); + result.Target.Name, Settings.AppFailsOnMinimumSeverity); return ModVerifyConstants.CompletedWithFindings; } @@ -57,4 +66,35 @@ protected override VerificationBaseline GetBaseline(VerificationTarget verificat } return baseline; } + + private IReadOnlyCollection CreateReporters() + { + var reporters = new List(); + + reporters.Add(IVerificationReporter.CreateConsole(new ConsoleReporterSettings + { + Verbose = Settings.ReportSettings.Verbose, + MinimumReportSeverity = Settings.VerifierServiceSettings.FailFastSettings.IsFailFast + ? VerificationSeverity.Information + : VerificationSeverity.Error + }, ServiceProvider)); + + var outputDirectory = Settings.ReportDirectory; + reporters.Add(IVerificationReporter.CreateJson(new JsonReporterSettings + { + OutputDirectory = outputDirectory, + MinimumReportSeverity = Settings.ReportSettings.MinimumReportSeverity, + AggregateResults = true, + Verbose = Settings.ReportSettings.Verbose + }, ServiceProvider)); + + reporters.Add(IVerificationReporter.CreateText(new TextFileReporterSettings + { + Verbose = Settings.ReportSettings.Verbose, + OutputDirectory = outputDirectory!, + MinimumReportSeverity = Settings.ReportSettings.MinimumReportSeverity + }, ServiceProvider)); + + return reporters; + } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModVerify.CliApp.csproj b/src/ModVerify.CliApp/ModVerify.CliApp.csproj index ec2fd46..f7b7ff8 100644 --- a/src/ModVerify.CliApp/ModVerify.CliApp.csproj +++ b/src/ModVerify.CliApp/ModVerify.CliApp.csproj @@ -5,14 +5,14 @@ Exe AET.ModVerify.App ModVerify - $(RepoRootPath)aet.ico + Resources/aet.ico AlamoEngineTools.ModVerify.CliApp ModVerify Console Application AET.ModVerify - Console application that analyzes game modifications for Empire at War / Forces of Corruption for common errors. + An application that analyzes game modifications for Empire at War / Forces of Corruption for common errors. alamo,petroglyph,glyphx @@ -25,27 +25,23 @@ true - - - - - - - - + + + + - + - - - - - + + + + + @@ -66,15 +62,15 @@ - + compile runtime; build; native; contentfiles; analyzers; buildtransitive - + compile runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -86,10 +82,6 @@ - - - - diff --git a/src/ModVerify.CliApp/Program.cs b/src/ModVerify.CliApp/Program.cs index 2d23b3b..e5316f4 100644 --- a/src/ModVerify.CliApp/Program.cs +++ b/src/ModVerify.CliApp/Program.cs @@ -2,11 +2,6 @@ using AET.ModVerify.App.Settings.CommandLine; using AET.ModVerify.App.Updates; using AET.ModVerify.App.Utilities; -using AET.ModVerify.Reporting; -using AET.ModVerify.Reporting.Reporters; -using AET.ModVerify.Reporting.Reporters.JSON; -using AET.ModVerify.Reporting.Reporters.Text; -using AET.ModVerify.Reporting.Settings; using AET.SteamAbstraction; using AnakinRaW.ApplicationBase; using AnakinRaW.ApplicationBase.Environment; @@ -20,7 +15,6 @@ using Microsoft.Extensions.Logging; using PG.Commons; using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Engine.Xml.Parsers; using PG.StarWarsGame.Files.ALO; using PG.StarWarsGame.Files.MEG; using PG.StarWarsGame.Files.MTD; @@ -36,11 +30,11 @@ using Serilog.Sinks.SystemConsole.Themes; using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO.Abstractions; using System.Runtime.InteropServices; using System.Threading.Tasks; using AET.ModVerify.App.Reporting; +using PG.StarWarsGame.Engine.Xml; using Testably.Abstractions; using ILogger = Serilog.ILogger; @@ -57,8 +51,8 @@ private static Task Main(string[] args) internal class Program : SelfUpdateableAppLifecycle { - private static readonly string EngineParserNamespace = typeof(XmlObjectParser<>).Namespace!; - private static readonly string ParserNamespace = typeof(PetroglyphXmlFileParser<>).Namespace!; + private static readonly string EngineParserNamespace = typeof(PetroglyphStarWarsGameXmlParser).Namespace!; + private static readonly string ParserNamespace = typeof(XmlFileParser<>).Namespace!; private static readonly string ModVerifyRootNameSpace = typeof(Program).Namespace!; private static readonly CompiledExpression PrintToConsoleExpression = SerilogExpression.Compile($"EventId.Id = {ModVerifyConstants.ConsoleEventIdValue}"); @@ -157,11 +151,10 @@ protected override void CreateAppServices(IServiceCollection services, IReadOnly PetroglyphCommons.ContributeServices(services); PetroglyphEngineServiceContribution.ContributeServices(services); + services.AddModVerify(); services.RegisterVerifierCache(); services.AddSingleton(sp => new BaselineFactory(sp)); - - SetupVerifyReporting(services); if (_offlineMode) { @@ -200,37 +193,6 @@ protected override async Task RunAppAsync(string[] args, IServiceProvider a return await new ModVerifyApplication(_modVerifyAppSettings, appServiceProvider).RunAsync().ConfigureAwait(false); } - private void SetupVerifyReporting(IServiceCollection serviceCollection) - { - Debug.Assert(_modVerifyAppSettings is not null); - - var verifySettings = _modVerifyAppSettings as AppVerifySettings; - - // Console should be in minimal summary mode if we are in a different mode than verify. - serviceCollection.RegisterConsoleReporter(new ReporterSettings - { - MinimumReportSeverity = verifySettings?.VerifyPipelineSettings.FailFastSettings.IsFailFast is true - ? VerificationSeverity.Information - : VerificationSeverity.Error - }, summaryOnly: verifySettings is null); - - if (verifySettings == null) - return; - - var outputDirectory = verifySettings.ReportDirectory; - serviceCollection.RegisterJsonReporter(new JsonReporterSettings - { - OutputDirectory = outputDirectory!, - MinimumReportSeverity = _modVerifyAppSettings.ReportSettings.MinimumReportSeverity - }); - - serviceCollection.RegisterTextFileReporter(new TextFileReporterSettings - { - OutputDirectory = outputDirectory!, - MinimumReportSeverity = _modVerifyAppSettings.ReportSettings.MinimumReportSeverity - }); - } - private void ConfigureLogging(ILoggingBuilder loggingBuilder) { loggingBuilder.ClearProviders(); diff --git a/src/ModVerify.CliApp/Properties/launchSettings.json b/src/ModVerify.CliApp/Properties/launchSettings.json index 299ce46..46fa0fc 100644 --- a/src/ModVerify.CliApp/Properties/launchSettings.json +++ b/src/ModVerify.CliApp/Properties/launchSettings.json @@ -2,15 +2,15 @@ "profiles": { "Verify": { "commandName": "Project", - "commandLineArgs": "" + "commandLineArgs": "verify --offline" }, "Verify (Interactive)": { "commandName": "Project", - "commandLineArgs": "verify -o verifyResults --offline --minFailSeverity Information" + "commandLineArgs": "verify -o verifyResults --offline --minFailSeverity Information --searchBaseline" }, "Verify (Automatic Target Selection)": { "commandName": "Project", - "commandLineArgs": "verify -o verifyResults --path \"C:\\Program Files (x86)\\Steam\\steamapps\\common\\Star Wars Empire at War\\corruption\"" + "commandLineArgs": "verify --offline -o verifyResults --path \"C:/Program Files (x86)/Steam/steamapps/common/Star Wars Empire at War/corruption/Mods/Test\"" }, "Create Baseline Interactive": { "commandName": "Project", diff --git a/src/ModVerify.CliApp/Reporting/BaselineFactory.cs b/src/ModVerify.CliApp/Reporting/BaselineFactory.cs index a83cea9..b621343 100644 --- a/src/ModVerify.CliApp/Reporting/BaselineFactory.cs +++ b/src/ModVerify.CliApp/Reporting/BaselineFactory.cs @@ -7,10 +7,9 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.IO.Abstractions; -using System.Linq; using System.Threading.Tasks; -using PG.StarWarsGame.Engine; using AET.ModVerify.App.Utilities; +using AET.ModVerify.Reporting.Baseline; namespace AET.ModVerify.App.Reporting; @@ -89,21 +88,13 @@ public VerificationBaseline CreateBaseline( Engine = target.Engine, Name = target.Name, Version = target.Version, - Location = settings.WriteLocations ? MaskUsername(target.Location) : null, + Location = settings.WriteLocations ? target.Location.MaskUsername() : null, IsGame = target.IsGame, }; return new VerificationBaseline(settings.ReportSettings.MinimumReportSeverity, errors, baselineTarget); } - private static GameLocations MaskUsername(GameLocations targetLocation) - { - return new GameLocations( - targetLocation.ModPaths.Select(PathUtilities.MaskUsername).ToList(), - PathUtilities.MaskUsername(targetLocation.GamePath), - targetLocation.FallbackPaths.Select(PathUtilities.MaskUsername).ToList()); - } - public async Task WriteBaselineAsync(VerificationBaseline baseline, string filePath) { #if NET diff --git a/src/ModVerify.CliApp/Reporting/BaselineSelector.cs b/src/ModVerify.CliApp/Reporting/BaselineSelector.cs index 791eaae..efcaae5 100644 --- a/src/ModVerify.CliApp/Reporting/BaselineSelector.cs +++ b/src/ModVerify.CliApp/Reporting/BaselineSelector.cs @@ -1,6 +1,6 @@ using AET.ModVerify.App.Resources.Baselines; using AET.ModVerify.App.Settings; -using AET.ModVerify.Reporting; +using AET.ModVerify.Reporting.Baseline; using AnakinRaW.ApplicationBase; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/src/ModVerify.CliApp/Reporting/IBaselineFactory.cs b/src/ModVerify.CliApp/Reporting/IBaselineFactory.cs index 721a7ce..a534cdb 100644 --- a/src/ModVerify.CliApp/Reporting/IBaselineFactory.cs +++ b/src/ModVerify.CliApp/Reporting/IBaselineFactory.cs @@ -1,5 +1,6 @@ using AET.ModVerify.App.Settings; using AET.ModVerify.Reporting; +using AET.ModVerify.Reporting.Baseline; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; diff --git a/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs b/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs index b2ce170..66587ae 100644 --- a/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs +++ b/src/ModVerify.CliApp/Reporting/VerifyConsoleProgressReporter.cs @@ -1,7 +1,7 @@ using System; using System.Threading; using AET.ModVerify.App.Settings; -using AET.ModVerify.Pipeline.Progress; +using AET.ModVerify.Progress; using AnakinRaW.CommonUtilities; using AnakinRaW.CommonUtilities.SimplePipeline.Progress; using ShellProgressBar; diff --git a/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json b/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json index ce70f8a..39b65f9 100644 --- a/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json +++ b/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json @@ -1,5 +1,5 @@ { - "version": "2.1", + "version": "2.2", "target": { "name": "Forces of Corruption (SteamGold)", "engine": "Foc", @@ -10,3038 +10,2864 @@ "errors": [ { "id": "XML04", - "verifiers": [ - "XMLError" - ], - "message": "Expected double but got value \u002737\u0060\u0027. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #11571\u0027", "severity": "Warning", + "asset": "Size", + "message": "Expected double but got value \u002737\u0060\u0027. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #11571\u0027", "context": [ - "DATA\\XML\\COMMANDBARCOMPONENTS.XML", + "Parser: PG.StarWarsGame.Files.XML.Parsers.PetroglyphXmlFloatParser", + "File: DATA\\XML\\COMMANDBARCOMPONENTS.XML", "Size", "parentName=\u0027bm_text_steal\u0027" - ], - "asset": "Size" + ] }, { "id": "XML04", - "verifiers": [ - "XMLError" - ], - "message": "Expected integer but got \u002780, 20\u0027. File=\u0027DATA\\XML\\SFXEVENTSWEAPONS.XML #90\u0027", "severity": "Warning", + "asset": "Probability", + "message": "Expected integer but got \u002780, 20\u0027. File=\u0027DATA\\XML\\SFXEVENTSWEAPONS.XML #90\u0027", "context": [ - "DATA\\XML\\SFXEVENTSWEAPONS.XML", + "Parser: PG.StarWarsGame.Files.XML.Parsers.PetroglyphXmlIntegerParser", + "File: DATA\\XML\\SFXEVENTSWEAPONS.XML", "Probability", "parentName=\u0027Unit_TIE_Fighter_Fire\u0027" - ], - "asset": "Probability" + ] }, { - "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027CIN_Reb_CelebHall.alo\u0027", - "severity": "Error", - "context": [], - "asset": "CIN_Reb_CelebHall.alo" + "id": "XML08", + "severity": "Information", + "asset": "DATA\\XML\\UNITS_HERO_REBEL_ROGUE_SQUADRON.XML", + "message": "XML header is not the first entry of the XML file. File=\u0027DATA\\XML\\UNITS_HERO_REBEL_ROGUE_SQUADRON.XML #0\u0027", + "context": [ + "Parser: PG.StarWarsGame.Engine.Xml.Parsers.GameObjectFileParser", + "File: DATA\\XML\\UNITS_HERO_REBEL_ROGUE_SQUADRON.XML" + ] }, { - "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027p_ssd_debris\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_ECLIPSE_UC_DC.ALO\u0027", - "severity": "Error", + "id": "XML10", + "severity": "Information", + "asset": "Disabled_Darken", + "message": "The node \u0027Disabled_Darken\u0027 is not supported. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #8608\u0027", "context": [ - "DATA\\ART\\MODELS\\UV_ECLIPSE_UC_DC.ALO" - ], - "asset": "p_ssd_debris" + "Parser: PG.StarWarsGame.Engine.Xml.Parsers.CommandBarComponentParser", + "File: DATA\\XML\\COMMANDBARCOMPONENTS.XML", + "Disabled_Darken", + "parentName=\u0027b_fast_forward_t\u0027" + ] }, { - "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", - "AET.ModVerify.Verifiers.Commons.TextureVeifier" - ], - "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall.tga\u0027 for context: [W_SITH_LEFTHALL.ALO].", - "severity": "Error", + "id": "XML10", + "severity": "Information", + "asset": "Mega_Texture_Name", + "message": "The node \u0027Mega_Texture_Name\u0027 is not supported. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #8\u0027", "context": [ - "W_SITH_LEFTHALL.ALO" - ], - "asset": "Cin_Reb_CelebHall_Wall.tga" + "Parser: PG.StarWarsGame.Engine.Xml.Parsers.CommandBarComponentParser", + "File: DATA\\XML\\COMMANDBARCOMPONENTS.XML", + "Mega_Texture_Name", + "parentName=\u0027i_main_commandbar\u0027" + ] }, { - "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027Cin_ImperialCraft.alo\u0027", - "severity": "Error", - "context": [], - "asset": "Cin_ImperialCraft.alo" + "id": "XML08", + "severity": "Information", + "asset": "DATA\\XML\\UNITS_SPACE_UNDERWORLD_INTERCEPTOR4.XML", + "message": "XML header is not the first entry of the XML file. File=\u0027DATA\\XML\\UNITS_SPACE_UNDERWORLD_INTERCEPTOR4.XML #0\u0027", + "context": [ + "Parser: PG.StarWarsGame.Engine.Xml.Parsers.GameObjectFileParser", + "File: DATA\\XML\\UNITS_SPACE_UNDERWORLD_INTERCEPTOR4.XML" + ] }, { - "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027Cin_Officer.alo\u0027", - "severity": "Error", - "context": [], - "asset": "Cin_Officer.alo" + "id": "XML08", + "severity": "Information", + "asset": "DATA\\XML\\UNITS_SPACE_EMPIRE_TIE_DEFENDER.XML", + "message": "XML header is not the first entry of the XML file. File=\u0027DATA\\XML\\UNITS_SPACE_EMPIRE_TIE_DEFENDER.XML #0\u0027", + "context": [ + "Parser: PG.StarWarsGame.Engine.Xml.Parsers.GameObjectFileParser", + "File: DATA\\XML\\UNITS_SPACE_EMPIRE_TIE_DEFENDER.XML" + ] }, { - "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027Cin_DStar_protons.alo\u0027", - "severity": "Error", - "context": [], - "asset": "Cin_DStar_protons.alo" + "id": "XML10", + "severity": "Information", + "asset": "Disabled_Darken", + "message": "The node \u0027Disabled_Darken\u0027 is not supported. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #8569\u0027", + "context": [ + "Parser: PG.StarWarsGame.Engine.Xml.Parsers.CommandBarComponentParser", + "File: DATA\\XML\\COMMANDBARCOMPONENTS.XML", + "Disabled_Darken", + "parentName=\u0027b_fast_forward\u0027" + ] }, { - "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", - "AET.ModVerify.Verifiers.Commons.TextureVeifier" - ], - "message": "Could not find texture \u0027w_grenade.tga\u0027 for context: [W_GRENADE.ALO].", - "severity": "Error", + "id": "XML08", + "severity": "Information", + "asset": "DATA\\XML\\GROUNDSTRUCTURES_UNDERWORLD.XML", + "message": "XML header is not the first entry of the XML file. File=\u0027DATA\\XML\\GROUNDSTRUCTURES_UNDERWORLD.XML #0\u0027", "context": [ - "W_GRENADE.ALO" - ], - "asset": "w_grenade.tga" + "Parser: PG.StarWarsGame.Engine.Xml.Parsers.GameObjectFileParser", + "File: DATA\\XML\\GROUNDSTRUCTURES_UNDERWORLD.XML" + ] }, { - "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_03_STATION_D.ALO\u0027", - "severity": "Error", + "id": "XML08", + "severity": "Information", + "asset": "DATA\\XML\\UNITS_LAND_REBEL_GALLOFREE_HTT.XML", + "message": "XML header is not the first entry of the XML file. File=\u0027DATA\\XML\\UNITS_LAND_REBEL_GALLOFREE_HTT.XML #0\u0027", "context": [ - "DATA\\ART\\MODELS\\UB_03_STATION_D.ALO" - ], - "asset": "p_uwstation_death" + "Parser: PG.StarWarsGame.Engine.Xml.Parsers.GameObjectFileParser", + "File: DATA\\XML\\UNITS_LAND_REBEL_GALLOFREE_HTT.XML" + ] }, { - "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027Cin_EI_Vader.alo\u0027", - "severity": "Error", - "context": [], - "asset": "Cin_EI_Vader.alo" + "id": "XML10", + "severity": "Information", + "asset": "Disabled_Darken", + "message": "The node \u0027Disabled_Darken\u0027 is not supported. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #8589\u0027", + "context": [ + "Parser: PG.StarWarsGame.Engine.Xml.Parsers.CommandBarComponentParser", + "File: DATA\\XML\\COMMANDBARCOMPONENTS.XML", + "Disabled_Darken", + "parentName=\u0027b_play_pause_t\u0027" + ] }, { - "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027MODELS\u0027", - "severity": "Error", - "context": [], - "asset": "MODELS" + "id": "XML10", + "severity": "Information", + "asset": "Disabled_Darken", + "message": "The node \u0027Disabled_Darken\u0027 is not supported. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #8550\u0027", + "context": [ + "Parser: PG.StarWarsGame.Engine.Xml.Parsers.CommandBarComponentParser", + "File: DATA\\XML\\COMMANDBARCOMPONENTS.XML", + "Disabled_Darken", + "parentName=\u0027b_play_pause\u0027" + ] }, { - "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027Cin_DeathStar_Wall.alo\u0027", - "severity": "Error", - "context": [], - "asset": "Cin_DeathStar_Wall.alo" + "id": "XML08", + "severity": "Information", + "asset": "DATA\\XML\\SPACEPROPS_UNDERWORLD.XML", + "message": "XML header is not the first entry of the XML file. File=\u0027DATA\\XML\\SPACEPROPS_UNDERWORLD.XML #0\u0027", + "context": [ + "Parser: PG.StarWarsGame.Engine.Xml.Parsers.GameObjectFileParser", + "File: DATA\\XML\\SPACEPROPS_UNDERWORLD.XML" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_PRISON.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0210_ENG.WAV", + "message": "Audio file \u0027U000_LEI0210_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\NB_PRISON.ALO" - ], - "asset": "p_smoke_small_thin2" + "Unit_Group_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_01_STATION_D.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0207_ENG.WAV", + "message": "Audio file \u0027U000_LEI0207_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\UB_01_STATION_D.ALO" - ], - "asset": "p_uwstation_death" + "Unit_Group_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027CIN_Officer_Row.alo\u0027", "severity": "Error", - "context": [], - "asset": "CIN_Officer_Row.alo" + "asset": "U000_LEI0205_ENG.WAV", + "message": "Audio file \u0027U000_LEI0205_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_MEDIUM.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0207_ENG.WAV", + "message": "Audio file \u0027U000_LEI0207_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\W_STARS_MEDIUM.ALO" - ], - "asset": "Lensflare0" + "Unit_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027CIN_DeathStar_Hangar.alo\u0027", "severity": "Error", - "context": [], - "asset": "CIN_DeathStar_Hangar.alo" + "asset": "U000_DEF3006_ENG.WAV", + "message": "Audio file \u0027U000_DEF3006_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Corrupt_Sabateur" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027Cin_EV_lambdaShuttle_150.alo\u0027", "severity": "Error", - "context": [], - "asset": "Cin_EV_lambdaShuttle_150.alo" + "asset": "U000_ARC3104_ENG.WAV", + "message": "Audio file \u0027U000_ARC3104_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Produce_Troops_Arc_Hammer" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027p_smoke_small_thin4\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_PRISON.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0315_ENG.WAV", + "message": "Audio file \u0027U000_LEI0315_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\NB_PRISON.ALO" - ], - "asset": "p_smoke_small_thin4" + "Unit_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_CINE.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0201_ENG.WAV", + "message": "Audio file \u0027U000_LEI0201_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\W_STARS_CINE.ALO" - ], - "asset": "Lensflare0" + "Unit_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_SKIPRAY.ALO\u0027.", "severity": "Error", + "asset": "U000_LEI0211_ENG.WAV", + "message": "Audio file \u0027U000_LEI0211_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\UV_SKIPRAY.ALO" - ], - "asset": "Default.fx" + "Unit_Group_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\UI_IG88.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0303_ENG.WAV", + "message": "Audio file \u0027U000_LEI0303_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\UI_IG88.ALO" - ], - "asset": "p_desert_ground_dust" + "Unit_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027CIN_p_proton_torpedo.alo\u0027", "severity": "Error", - "context": [], - "asset": "CIN_p_proton_torpedo.alo" + "asset": "U000_LEI0111_ENG.WAV", + "message": "Audio file \u0027U000_LEI0111_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Select_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027CIN_Fire_Huge.alo\u0027", "severity": "Error", - "context": [], - "asset": "CIN_Fire_Huge.alo" + "asset": "AMB_URB_CLEAR_LOOP_1.WAV", + "message": "Audio file \u0027AMB_URB_CLEAR_LOOP_1.WAV\u0027 could not be found.", + "context": [ + "Weather_Ambient_Clear_Urban_Loop" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_04_STATION_D.ALO\u0027", "severity": "Error", + "asset": "FS_BEETLE_3.WAV", + "message": "Audio file \u0027FS_BEETLE_3.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\UB_04_STATION_D.ALO" - ], - "asset": "p_uwstation_death" + "SFX_Anim_Beetle_Footsteps" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027RV_nebulonb_D_death_00.ALO\u0027", "severity": "Error", - "context": [], - "asset": "RV_nebulonb_D_death_00.ALO" + "asset": "U000_LEI0205_ENG.WAV", + "message": "Audio file \u0027U000_LEI0205_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Group_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_02_STATION_D.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0215_ENG.WAV", + "message": "Audio file \u0027U000_LEI0215_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\UB_02_STATION_D.ALO" - ], - "asset": "p_uwstation_death" + "Unit_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027W_Kamino_Reflect.ALO\u0027", "severity": "Error", - "context": [], - "asset": "W_Kamino_Reflect.ALO" + "asset": "U000_LEI0504_ENG.WAV", + "message": "Audio file \u0027U000_LEI0504_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Remove_Corruption_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_MONCAL_BUILDING.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0104_ENG.WAV", + "message": "Audio file \u0027U000_LEI0104_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\NB_MONCAL_BUILDING.ALO" - ], - "asset": "p_smoke_small_thin2" + "Unit_Select_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027p_steam_small\u0027 not found for model \u0027DATA\\ART\\MODELS\\RB_HEAVYVEHICLEFACTORY.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0303_ENG.WAV", + "message": "Audio file \u0027U000_LEI0303_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\RB_HEAVYVEHICLEFACTORY.ALO" - ], - "asset": "p_steam_small" + "Unit_Group_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027Cin_EV_Stardestroyer_Warp.alo\u0027", "severity": "Error", - "context": [], - "asset": "Cin_EV_Stardestroyer_Warp.alo" + "asset": "U000_LEI0202_ENG.WAV", + "message": "Audio file \u0027U000_LEI0202_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Fleet_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027Cin_DStar_TurretLasers.alo\u0027", "severity": "Error", - "context": [], - "asset": "Cin_DStar_TurretLasers.alo" + "asset": "U000_LEI0212_ENG.WAV", + "message": "Audio file \u0027U000_LEI0212_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027CIN_Rbel_GreyGroup.alo\u0027", "severity": "Error", - "context": [], - "asset": "CIN_Rbel_GreyGroup.alo" + "asset": "U000_LEI0105_ENG.WAV", + "message": "Audio file \u0027U000_LEI0105_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Select_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027Cin_Planet_Hoth_High.alo\u0027", "severity": "Error", - "context": [], - "asset": "Cin_Planet_Hoth_High.alo" + "asset": "U000_LEI0311_ENG.WAV", + "message": "Audio file \u0027U000_LEI0311_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027CIN_Trooper_Row.alo\u0027", "severity": "Error", - "context": [], - "asset": "CIN_Trooper_Row.alo" + "asset": "U000_LEI0110_ENG.WAV", + "message": "Audio file \u0027U000_LEI0110_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Select_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\UI_SABOTEUR.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0313_ENG.WAV", + "message": "Audio file \u0027U000_LEI0313_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\UI_SABOTEUR.ALO" - ], - "asset": "p_desert_ground_dust" + "Unit_Group_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_CINE_LUA.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0103_ENG.WAV", + "message": "Audio file \u0027U000_LEI0103_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\W_STARS_CINE_LUA.ALO" - ], - "asset": "Lensflare0" + "Unit_Select_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027W_AllShaders.ALO\u0027", "severity": "Error", - "context": [], - "asset": "W_AllShaders.ALO" + "asset": "U000_LEI0308_ENG.WAV", + "message": "Audio file \u0027U000_LEI0308_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Group_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\EV_TIE_LANCET.ALO\u0027.", "severity": "Error", + "asset": "U000_TMC0212_ENG.WAV", + "message": "Audio file \u0027U000_TMC0212_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\EV_TIE_LANCET.ALO" - ], - "asset": "Default.fx" + "Unit_Assist_Move_Tie_Mauler" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027CINE_EV_StarDestroyer.ALO\u0027", "severity": "Error", - "context": [], - "asset": "CINE_EV_StarDestroyer.ALO" + "asset": "U000_LEI0304_ENG.WAV", + "message": "Audio file \u0027U000_LEI0304_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027Cin_EI_Palpatine.alo\u0027", "severity": "Error", - "context": [], - "asset": "Cin_EI_Palpatine.alo" + "asset": "U000_LEI0101_ENG.WAV", + "message": "Audio file \u0027U000_LEI0101_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Select_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", - "AET.ModVerify.Verifiers.Commons.TextureVeifier" - ], - "message": "Could not find texture \u0027Cin_DeathStar.tga\u0027 for context: [ALTTEST.ALO].", "severity": "Error", + "asset": "U000_LEI0211_ENG.WAV", + "message": "Audio file \u0027U000_LEI0211_ENG.WAV\u0027 could not be found.", "context": [ - "ALTTEST.ALO" - ], - "asset": "Cin_DeathStar.tga" + "Unit_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", - "AET.ModVerify.Verifiers.Commons.TextureVeifier" - ], - "message": "Could not find texture \u0027UB_girder_B.tga\u0027 for context: [UV_MDU_CAGE.ALO].", "severity": "Error", + "asset": "U000_LEI0305_ENG.WAV", + "message": "Audio file \u0027U000_LEI0305_ENG.WAV\u0027 could not be found.", "context": [ - "UV_MDU_CAGE.ALO" - ], - "asset": "UB_girder_B.tga" + "Unit_Group_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027P_mptl-2a_Die\u0027 not found for model \u0027DATA\\ART\\MODELS\\RV_MPTL-2A.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0208_ENG.WAV", + "message": "Audio file \u0027U000_LEI0208_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\RV_MPTL-2A.ALO" - ], - "asset": "P_mptl-2a_Die" + "Unit_Fleet_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\EV_MDU_SENSORNODE.ALO\u0027.", "severity": "Error", + "asset": "U000_LEI0311_ENG.WAV", + "message": "Audio file \u0027U000_LEI0311_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\EV_MDU_SENSORNODE.ALO" - ], - "asset": "Default.fx" + "Unit_Group_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027Cin_Planet_Alderaan_High.alo\u0027", "severity": "Error", - "context": [], - "asset": "Cin_Planet_Alderaan_High.alo" + "asset": "U000_LEI0314_ENG.WAV", + "message": "Audio file \u0027U000_LEI0314_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027p_hp_archammer-damage\u0027 not found for model \u0027DATA\\ART\\MODELS\\EV_ARCHAMMER.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0108_ENG.WAV", + "message": "Audio file \u0027U000_LEI0108_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\EV_ARCHAMMER.ALO" - ], - "asset": "p_hp_archammer-damage" + "Unit_Select_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_05_STATION_D.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0213_ENG.WAV", + "message": "Audio file \u0027U000_LEI0213_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\UB_05_STATION_D.ALO" - ], - "asset": "p_uwstation_death" + "Unit_Group_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027p_explosion_smoke_small_thin5\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_NOGHRI_HUT.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0202_ENG.WAV", + "message": "Audio file \u0027U000_LEI0202_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\NB_NOGHRI_HUT.ALO" - ], - "asset": "p_explosion_smoke_small_thin5" + "Unit_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027CIN_Probe_Droid.alo\u0027", "severity": "Error", - "context": [], - "asset": "CIN_Probe_Droid.alo" + "asset": "U000_LEI0113_ENG.WAV", + "message": "Audio file \u0027U000_LEI0113_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Select_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_HIGH.ALO\u0027", "severity": "Error", + "asset": "AMB_DES_CLEAR_LOOP_1.WAV", + "message": "Audio file \u0027AMB_DES_CLEAR_LOOP_1.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\W_STARS_HIGH.ALO" - ], - "asset": "Lensflare0" + "Weather_Ambient_Clear_Sandstorm_Loop" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027W_Volcano_Rock02.ALO\u0027", "severity": "Error", - "context": [], - "asset": "W_Volcano_Rock02.ALO" + "asset": "U000_LEI0107_ENG.WAV", + "message": "Audio file \u0027U000_LEI0107_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Select_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027lookat\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_ECLIPSE.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0305_ENG.WAV", + "message": "Audio file \u0027U000_LEI0305_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\UV_ECLIPSE.ALO" - ], - "asset": "lookat" + "Unit_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027Cin_Shuttle_Tyderium.alo\u0027", "severity": "Error", - "context": [], - "asset": "Cin_Shuttle_Tyderium.alo" + "asset": "U000_LEI0209_ENG.WAV", + "message": "Audio file \u0027U000_LEI0209_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Group_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027W_SwampGasEmit.ALO\u0027", "severity": "Error", - "context": [], - "asset": "W_SwampGasEmit.ALO" + "asset": "U000_LEI0308_ENG.WAV", + "message": "Audio file \u0027U000_LEI0308_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027P_heat_small01\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_VCH.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0207_ENG.WAV", + "message": "Audio file \u0027U000_LEI0207_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\NB_VCH.ALO" - ], - "asset": "P_heat_small01" + "Unit_Fleet_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027CIN_Rbel_NavyRow.alo\u0027", "severity": "Error", - "context": [], - "asset": "CIN_Rbel_NavyRow.alo" + "asset": "U000_MAL0503_ENG.WAV", + "message": "Audio file \u0027U000_MAL0503_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Assist_Move_Missile_Launcher" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027CIN_Fire_Medium.alo\u0027", "severity": "Error", - "context": [], - "asset": "CIN_Fire_Medium.alo" + "asset": "U000_LEI0102_ENG.WAV", + "message": "Audio file \u0027U000_LEI0102_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Select_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027p_ewok_drag_dirt\u0027 not found for model \u0027DATA\\ART\\MODELS\\UI_EWOK_HANDLER.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0306_ENG.WAV", + "message": "Audio file \u0027U000_LEI0306_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\UI_EWOK_HANDLER.ALO" - ], - "asset": "p_ewok_drag_dirt" + "Unit_Group_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027W_Bush_Swmp00.ALO\u0027", "severity": "Error", - "context": [], - "asset": "W_Bush_Swmp00.ALO" + "asset": "FS_BEETLE_2.WAV", + "message": "Audio file \u0027FS_BEETLE_2.WAV\u0027 could not be found.", + "context": [ + "SFX_Anim_Beetle_Footsteps" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027W_droid_steam.alo\u0027", "severity": "Error", - "context": [], - "asset": "W_droid_steam.alo" + "asset": "U000_LEI0312_ENG.WAV", + "message": "Audio file \u0027U000_LEI0312_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Group_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027CIN_Biker_Row.alo\u0027", "severity": "Error", - "context": [], - "asset": "CIN_Biker_Row.alo" + "asset": "U000_LEI0402_ENG.WAV", + "message": "Audio file \u0027U000_LEI0402_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Guard_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027w_planet_volcanic.alo\u0027", "severity": "Error", - "context": [], - "asset": "w_planet_volcanic.alo" + "asset": "U000_LEI0309_ENG.WAV", + "message": "Audio file \u0027U000_LEI0309_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Group_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027DATA\\ART\\MODELS\\RB_HYPERVELOCITYGUN.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0501_ENG.WAV", + "message": "Audio file \u0027U000_LEI0501_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\RB_HYPERVELOCITYGUN.ALO" - ], - "asset": "p_smoke_small_thin2" + "Unit_Remove_Corruption_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027lookat\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_ECLIPSE_UC.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0201_ENG.WAV", + "message": "Audio file \u0027U000_LEI0201_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\UV_ECLIPSE_UC.ALO" - ], - "asset": "lookat" + "Unit_Group_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027CIN_REb_CelebCharacters.alo\u0027", "severity": "Error", - "context": [], - "asset": "CIN_REb_CelebCharacters.alo" + "asset": "U000_MCF1601_ENG.WAV", + "message": "Audio file \u0027U000_MCF1601_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_StarDest_MC30_Frigate" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027Cin_DeathStar_High.alo\u0027", "severity": "Error", - "context": [], - "asset": "Cin_DeathStar_High.alo" + "asset": "TESTUNITMOVE_ENG.WAV", + "message": "Audio file \u0027TESTUNITMOVE_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Move_Gneneric_Test" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", - "AET.ModVerify.Verifiers.Commons.TextureVeifier" - ], - "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall_B.tga\u0027 for context: [W_SITH_LEFTHALL.ALO].", "severity": "Error", + "asset": "U000_LEI0114_ENG.WAV", + "message": "Audio file \u0027U000_LEI0114_ENG.WAV\u0027 could not be found.", "context": [ - "W_SITH_LEFTHALL.ALO" - ], - "asset": "Cin_Reb_CelebHall_Wall_B.tga" + "Unit_Select_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", - "AET.ModVerify.Verifiers.Commons.TextureVeifier" - ], - "message": "Could not find texture \u0027NB_YsalamiriTree_B.tga\u0027 for context: [UV_MDU_CAGE.ALO].", "severity": "Error", + "asset": "U000_ARC3105_ENG.WAV", + "message": "Audio file \u0027U000_ARC3105_ENG.WAV\u0027 could not be found.", "context": [ - "UV_MDU_CAGE.ALO" - ], - "asset": "NB_YsalamiriTree_B.tga" - }, - { - "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027Cin_Coruscant.alo\u0027", - "severity": "Error", - "context": [], - "asset": "Cin_Coruscant.alo" + "Unit_Complete_Troops_Arc_Hammer" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027p_prison_light\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_PRISON.ALO\u0027", "severity": "Error", + "asset": "EGL_STAR_VIPER_SPINNING_1.WAV", + "message": "Audio file \u0027EGL_STAR_VIPER_SPINNING_1.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\NB_PRISON.ALO" - ], - "asset": "p_prison_light" + "Unit_Star_Viper_Spinning_By" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027p_cold_tiny01\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_SCH.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0401_ENG.WAV", + "message": "Audio file \u0027U000_LEI0401_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\NB_SCH.ALO" - ], - "asset": "p_cold_tiny01" + "Unit_Guard_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027CIN_NavyTrooper_Row.alo\u0027", "severity": "Error", - "context": [], - "asset": "CIN_NavyTrooper_Row.alo" + "asset": "U000_LEI0213_ENG.WAV", + "message": "Audio file \u0027U000_LEI0213_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_CRUSADERCLASSCORVETTE.ALO\u0027.", "severity": "Error", + "asset": "U000_LEI0212_ENG.WAV", + "message": "Audio file \u0027U000_LEI0212_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\UV_CRUSADERCLASSCORVETTE.ALO" - ], - "asset": "Default.fx" + "Unit_Group_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027CIN_Rbel_Soldier.alo\u0027", "severity": "Error", - "context": [], - "asset": "CIN_Rbel_Soldier.alo" + "asset": "U000_LEI0115_ENG.WAV", + "message": "Audio file \u0027U000_LEI0115_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Select_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\RI_KYLEKATARN.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0306_ENG.WAV", + "message": "Audio file \u0027U000_LEI0306_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\RI_KYLEKATARN.ALO" - ], - "asset": "p_desert_ground_dust" + "Unit_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027CIN_Lambda_Mouth.alo\u0027", "severity": "Error", - "context": [], - "asset": "CIN_Lambda_Mouth.alo" + "asset": "U000_LEI0601_ENG.WAV", + "message": "Audio file \u0027U000_LEI0601_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Increase_Production_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", - "AET.ModVerify.Verifiers.Commons.TextureVeifier" - ], - "message": "Could not find texture \u0027p_particle_master\u0027 for context: [P_DIRT_EMITTER_TEST1.ALO].", "severity": "Error", + "asset": "U000_ARC3106_ENG.WAV", + "message": "Audio file \u0027U000_ARC3106_ENG.WAV\u0027 could not be found.", "context": [ - "P_DIRT_EMITTER_TEST1.ALO" - ], - "asset": "p_particle_master" + "Unit_Complete_Troops_Arc_Hammer" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027Cin_bridge.alo\u0027", "severity": "Error", - "context": [], - "asset": "Cin_bridge.alo" + "asset": "U000_LEI0602_ENG.WAV", + "message": "Audio file \u0027U000_LEI0602_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Increase_Production_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027W_Vol_Steam01.ALO\u0027", "severity": "Error", - "context": [], - "asset": "W_Vol_Steam01.ALO" + "asset": "U000_DEF3106_ENG.WAV", + "message": "Audio file \u0027U000_DEF3106_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Weaken_Sabateur" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027CIN_Rbel_grey.alo\u0027", "severity": "Error", - "context": [], - "asset": "CIN_Rbel_grey.alo" + "asset": "U000_LEI0403_ENG.WAV", + "message": "Audio file \u0027U000_LEI0403_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Guard_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027w_sith_arch.alo\u0027", "severity": "Error", - "context": [], - "asset": "w_sith_arch.alo" + "asset": "U000_LEI0314_ENG.WAV", + "message": "Audio file \u0027U000_LEI0314_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Group_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027Cin_rv_XWingProp.alo\u0027", "severity": "Error", - "context": [], - "asset": "Cin_rv_XWingProp.alo" + "asset": "U000_LEI0203_ENG.WAV", + "message": "Audio file \u0027U000_LEI0203_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Group_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027Cin_DStar_Dish_close.alo\u0027", "severity": "Error", - "context": [], - "asset": "Cin_DStar_Dish_close.alo" + "asset": "U000_LEI0205_ENG.WAV", + "message": "Audio file \u0027U000_LEI0205_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Fleet_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier", - "AET.ModVerify.Verifiers.Commons.TextureVeifier" - ], - "message": "Could not find texture \u0027W_TE_Rock_f_02_b.tga\u0027 for context: [EV_TIE_PHANTOM.ALO].", "severity": "Error", + "asset": "U000_TMC0212_ENG.WAV", + "message": "Audio file \u0027U000_TMC0212_ENG.WAV\u0027 could not be found.", "context": [ - "EV_TIE_PHANTOM.ALO" - ], - "asset": "W_TE_Rock_f_02_b.tga" + "Unit_Move_Tie_Mauler" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027pe_bwing_yellow\u0027 not found for model \u0027DATA\\ART\\MODELS\\RV_BWING.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0203_ENG.WAV", + "message": "Audio file \u0027U000_LEI0203_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\RV_BWING.ALO" - ], - "asset": "pe_bwing_yellow" + "Unit_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027CIN_Lambda_Head.alo\u0027", "severity": "Error", - "context": [], - "asset": "CIN_Lambda_Head.alo" + "asset": "U000_LEI0307_ENG.WAV", + "message": "Audio file \u0027U000_LEI0307_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027p_explosion_small_delay00\u0027 not found for model \u0027DATA\\ART\\MODELS\\EB_COMMANDCENTER.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0203_ENG.WAV", + "message": "Audio file \u0027U000_LEI0203_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\EB_COMMANDCENTER.ALO" - ], - "asset": "p_explosion_small_delay00" + "Unit_Fleet_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027Cin_DStar_LeverPanel.alo\u0027", "severity": "Error", - "context": [], - "asset": "Cin_DStar_LeverPanel.alo" + "asset": "U000_LEI0201_ENG.WAV", + "message": "Audio file \u0027U000_LEI0201_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Fleet_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027p_splash_wake_lava.alo\u0027", "severity": "Error", - "context": [], - "asset": "p_splash_wake_lava.alo" + "asset": "FS_BEETLE_1.WAV", + "message": "Audio file \u0027FS_BEETLE_1.WAV\u0027 could not be found.", + "context": [ + "SFX_Anim_Beetle_Footsteps" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_LOW.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0204_ENG.WAV", + "message": "Audio file \u0027U000_LEI0204_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\W_STARS_LOW.ALO" - ], - "asset": "Lensflare0" + "Unit_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\EI_MARAJADE.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0206_ENG.WAV", + "message": "Audio file \u0027U000_LEI0206_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\EI_MARAJADE.ALO" - ], - "asset": "p_desert_ground_dust" + "Unit_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Proxy particle \u0027p_bomb_spin\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_THERMAL_DETONATOR_EMPIRE.ALO\u0027", "severity": "Error", + "asset": "U000_LEI0603_ENG.WAV", + "message": "Audio file \u0027U000_LEI0603_ENG.WAV\u0027 could not be found.", "context": [ - "DATA\\ART\\MODELS\\W_THERMAL_DETONATOR_EMPIRE.ALO" - ], - "asset": "p_bomb_spin" + "Unit_Increase_Production_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027Cin_EV_TieAdvanced.alo\u0027", "severity": "Error", - "context": [], - "asset": "Cin_EV_TieAdvanced.alo" + "asset": "U000_LEI0313_ENG.WAV", + "message": "Audio file \u0027U000_LEI0313_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.ReferencedModelsVerifier", - "AET.ModVerify.Verifiers.Commons.SingleModelVerifier" - ], - "message": "Unable to find .ALO file \u0027CIN_Rbel_Soldier_Group.alo\u0027", "severity": "Error", - "context": [], - "asset": "CIN_Rbel_Soldier_Group.alo" + "asset": "U000_LEI0301_ENG.WAV", + "message": "Audio file \u0027U000_LEI0301_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0213_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0312_ENG.WAV", + "message": "Audio file \u0027U000_LEI0312_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0213_ENG.WAV" + "Unit_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0113_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0404_ENG.WAV", + "message": "Audio file \u0027U000_LEI0404_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0113_ENG.WAV" + "Unit_Guard_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0603_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0604_ENG.WAV", + "message": "Audio file \u0027U000_LEI0604_ENG.WAV\u0027 could not be found.", "context": [ "Unit_Increase_Production_Leia" - ], - "asset": "U000_LEI0603_ENG.WAV" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0309_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0210_ENG.WAV", + "message": "Audio file \u0027U000_LEI0210_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0309_ENG.WAV" + "Unit_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0212_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0307_ENG.WAV", + "message": "Audio file \u0027U000_LEI0307_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0212_ENG.WAV" + "Unit_Group_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_MAL0503_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "C000_DST0102_ENG.WAV", + "message": "Audio file \u0027C000_DST0102_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Assist_Move_Missile_Launcher" - ], - "asset": "U000_MAL0503_ENG.WAV" + "EHD_Death_Star_Activate" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_MCF1601_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0215_ENG.WAV", + "message": "Audio file \u0027U000_LEI0215_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_StarDest_MC30_Frigate" - ], - "asset": "U000_MCF1601_ENG.WAV" + "Unit_Group_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0111_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0206_ENG.WAV", + "message": "Audio file \u0027U000_LEI0206_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0111_ENG.WAV" + "Unit_Group_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_ARC3106_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0202_ENG.WAV", + "message": "Audio file \u0027U000_LEI0202_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Complete_Troops_Arc_Hammer" - ], - "asset": "U000_ARC3106_ENG.WAV" + "Unit_Group_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0303_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0304_ENG.WAV", + "message": "Audio file \u0027U000_LEI0304_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0303_ENG.WAV" + "Unit_Group_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0404_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0315_ENG.WAV", + "message": "Audio file \u0027U000_LEI0315_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Guard_Leia" - ], - "asset": "U000_LEI0404_ENG.WAV" + "Unit_Group_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027TESTUNITMOVE_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0112_ENG.WAV", + "message": "Audio file \u0027U000_LEI0112_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Move_Gneneric_Test" - ], - "asset": "TESTUNITMOVE_ENG.WAV" + "Unit_Select_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0401_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "FS_BEETLE_4.WAV", + "message": "Audio file \u0027FS_BEETLE_4.WAV\u0027 could not be found.", "context": [ - "Unit_Guard_Leia" - ], - "asset": "U000_LEI0401_ENG.WAV" + "SFX_Anim_Beetle_Footsteps" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027EGL_STAR_VIPER_SPINNING_1.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0109_ENG.WAV", + "message": "Audio file \u0027U000_LEI0109_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Star_Viper_Spinning_By" - ], - "asset": "EGL_STAR_VIPER_SPINNING_1.WAV" + "Unit_Select_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_TMC0212_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0309_ENG.WAV", + "message": "Audio file \u0027U000_LEI0309_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Move_Tie_Mauler" - ], - "asset": "U000_TMC0212_ENG.WAV" + "Unit_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0110_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0106_ENG.WAV", + "message": "Audio file \u0027U000_LEI0106_ENG.WAV\u0027 could not be found.", "context": [ "Unit_Select_Leia" - ], - "asset": "U000_LEI0110_ENG.WAV" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0314_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0301_ENG.WAV", + "message": "Audio file \u0027U000_LEI0301_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0314_ENG.WAV" + "Unit_Group_Attack_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0305_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0209_ENG.WAV", + "message": "Audio file \u0027U000_LEI0209_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0305_ENG.WAV" + "Unit_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0112_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0208_ENG.WAV", + "message": "Audio file \u0027U000_LEI0208_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0112_ENG.WAV" + "Unit_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0209_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0503_ENG.WAV", + "message": "Audio file \u0027U000_LEI0503_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0209_ENG.WAV" + "Unit_Remove_Corruption_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0211_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0208_ENG.WAV", + "message": "Audio file \u0027U000_LEI0208_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0211_ENG.WAV" + "Unit_Group_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0205_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0206_ENG.WAV", + "message": "Audio file \u0027U000_LEI0206_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0205_ENG.WAV" + "Unit_Fleet_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0115_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0502_ENG.WAV", + "message": "Audio file \u0027U000_LEI0502_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0115_ENG.WAV" + "Unit_Remove_Corruption_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0604_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "U000_LEI0204_ENG.WAV", + "message": "Audio file \u0027U000_LEI0204_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Increase_Production_Leia" - ], - "asset": "U000_LEI0604_ENG.WAV" + "Unit_Group_Move_Leia" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0602_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "underworld_logo_off.tga", + "message": "Could not find GUI texture \u0027underworld_logo_off.tga\u0027 at location \u0027MegaTexture\u0027.", "context": [ - "Unit_Increase_Production_Leia" - ], - "asset": "U000_LEI0602_ENG.WAV" + "IDC_PLAY_FACTION_A_BUTTON_BIG", + "MegaTexture" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0315_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "i_dialogue_button_large_middle_off.tga", + "message": "Could not find GUI texture \u0027i_dialogue_button_large_middle_off.tga\u0027 at location \u0027Repository\u0027.", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0315_ENG.WAV" + "IDC_PLAY_FACTION_B_BUTTON_BIG", + "Repository" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_DEF3006_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "underworld_logo_rollover.tga", + "message": "Could not find GUI texture \u0027underworld_logo_rollover.tga\u0027 at location \u0027MegaTexture\u0027.", "context": [ - "Unit_Corrupt_Sabateur" - ], - "asset": "U000_DEF3006_ENG.WAV" + "IDC_PLAY_FACTION_A_BUTTON_BIG", + "MegaTexture" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0210_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "underworld_logo_selected.tga", + "message": "Could not find GUI texture \u0027underworld_logo_selected.tga\u0027 at location \u0027MegaTexture\u0027.", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0210_ENG.WAV" + "IDC_PLAY_FACTION_A_BUTTON_BIG", + "MegaTexture" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0105_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "lookat", + "message": "Proxy particle \u0027lookat\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_ECLIPSE_UC.ALO\u0027", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0105_ENG.WAV" + "Eclipse_Super_Star_Destroyer", + "DATA\\ART\\MODELS\\UV_ECLIPSE_UC.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0208_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "w_planet_volcanic.alo", + "message": "Unable to find .ALO file \u0027w_planet_volcanic.alo\u0027", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0208_ENG.WAV" + "Volcanic_Backdrop_Large" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0106_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Default.fx", + "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_SKIPRAY.ALO\u0027.", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0106_ENG.WAV" + "Skipray_Bombing_Run", + "DATA\\ART\\MODELS\\UV_SKIPRAY.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0202_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "pe_bwing_yellow", + "message": "Proxy particle \u0027pe_bwing_yellow\u0027 not found for model \u0027DATA\\ART\\MODELS\\RV_BWING.ALO\u0027", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0202_ENG.WAV" + "B-Wing", + "DATA\\ART\\MODELS\\RV_BWING.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0306_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Cin_DeathStar_High.alo", + "message": "Unable to find .ALO file \u0027Cin_DeathStar_High.alo\u0027", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0306_ENG.WAV" + "Death_Star_Whole_small" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0101_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "CIN_Rbel_NavyRow.alo", + "message": "Unable to find .ALO file \u0027CIN_Rbel_NavyRow.alo\u0027", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0101_ENG.WAV" + "Cin_Rebel_NavyRow" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027C000_DST0102_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "W_AllShaders.ALO", + "message": "Unable to find .ALO file \u0027W_AllShaders.ALO\u0027", "context": [ - "EHD_Death_Star_Activate" - ], - "asset": "C000_DST0102_ENG.WAV" + "Prop_AllShaders" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0103_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "p_prison_light", + "message": "Proxy particle \u0027p_prison_light\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_PRISON.ALO\u0027", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0103_ENG.WAV" + "Imperial_Prison_Facility", + "DATA\\ART\\MODELS\\NB_PRISON.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0403_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Cin_DeathStar_High.alo", + "message": "Unable to find .ALO file \u0027Cin_DeathStar_High.alo\u0027", "context": [ - "Unit_Guard_Leia" - ], - "asset": "U000_LEI0403_ENG.WAV" + "Death_Star_Whole_Vsmall" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027FS_BEETLE_2.WAV\u0027 could not be found.", "severity": "Error", + "asset": "W_Vol_Steam01.ALO", + "message": "Unable to find .ALO file \u0027W_Vol_Steam01.ALO\u0027", "context": [ - "SFX_Anim_Beetle_Footsteps" - ], - "asset": "FS_BEETLE_2.WAV" + "Prop_Vol_Steam01" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0201_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Cin_Coruscant.alo", + "message": "Unable to find .ALO file \u0027Cin_Coruscant.alo\u0027", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0201_ENG.WAV" + "Corusant_Backdrop_Large 6x" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0203_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Default.fx", + "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_CRUSADERCLASSCORVETTE.ALO\u0027.", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0203_ENG.WAV" + "Crusader_Gunship", + "DATA\\ART\\MODELS\\UV_CRUSADERCLASSCORVETTE.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0114_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Lensflare0", + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_HIGH.ALO\u0027", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0114_ENG.WAV" + "Stars_High", + "DATA\\ART\\MODELS\\W_STARS_HIGH.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027FS_BEETLE_1.WAV\u0027 could not be found.", "severity": "Error", + "asset": "lookat", + "message": "Proxy particle \u0027lookat\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_ECLIPSE.ALO\u0027", "context": [ - "SFX_Anim_Beetle_Footsteps" - ], - "asset": "FS_BEETLE_1.WAV" + "Eclipse_Prop", + "DATA\\ART\\MODELS\\UV_ECLIPSE.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0304_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "p_uwstation_death", + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_05_STATION_D.ALO\u0027", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0304_ENG.WAV" + "Underworld_Star_Base_5_Death_Clone", + "DATA\\ART\\MODELS\\UB_05_STATION_D.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_DeathStar.tga", + "message": "Could not find texture \u0027Cin_DeathStar.tga\u0027 for context: [Test_Base_Hector--\u003EALTTEST.ALO].", + "context": [ + "Test_Base_Hector", + "ALTTEST.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Lensflare0", + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_CINE_LUA.ALO\u0027", + "context": [ + "Stars_Lua_Cinematic", + "DATA\\ART\\MODELS\\W_STARS_CINE_LUA.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_DStar_LeverPanel.alo", + "message": "Unable to find .ALO file \u0027Cin_DStar_LeverPanel.alo\u0027", + "context": [ + "Death_Star_LeverPanel" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Lensflare0", + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_LOW.ALO\u0027", + "context": [ + "Stars_Low", + "DATA\\ART\\MODELS\\W_STARS_LOW.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "W_TE_Rock_f_02_b.tga", + "message": "Could not find texture \u0027W_TE_Rock_f_02_b.tga\u0027 for context: [Vengeance_Frigate--\u003EUV_VENGEANCE.ALO].", + "context": [ + "Vengeance_Frigate", + "UV_VENGEANCE.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_rv_XWingProp.alo", + "message": "Unable to find .ALO file \u0027Cin_rv_XWingProp.alo\u0027", + "context": [ + "Cin_X-WingProp" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CIN_Lambda_Mouth.alo", + "message": "Unable to find .ALO file \u0027CIN_Lambda_Mouth.alo\u0027", + "context": [ + "Cin_Lambda_Mouth" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_uwstation_death", + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_04_STATION_D.ALO\u0027", + "context": [ + "Underworld_Star_Base_4_Death_Clone", + "DATA\\ART\\MODELS\\UB_04_STATION_D.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_EV_lambdaShuttle_150.alo", + "message": "Unable to find .ALO file \u0027Cin_EV_lambdaShuttle_150.alo\u0027", + "context": [ + "Lambda_Shuttle_150X6-9" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CIN_Rbel_Soldier_Group.alo", + "message": "Unable to find .ALO file \u0027CIN_Rbel_Soldier_Group.alo\u0027", + "context": [ + "Cin_Rebel_SoldierRow" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_EI_Palpatine.alo", + "message": "Unable to find .ALO file \u0027Cin_EI_Palpatine.alo\u0027", + "context": [ + "Cin_Emperor_Shot_5" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_Reb_CelebHall_Wall_B.tga", + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall_B.tga\u0027 for context: [Cin_sith_console--\u003EW_SITH_CONSOLE.ALO].", + "context": [ + "Cin_sith_console", + "W_SITH_CONSOLE.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_rv_XWingProp.alo", + "message": "Unable to find .ALO file \u0027Cin_rv_XWingProp.alo\u0027", + "context": [ + "Grounded_Xwing" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_EV_Stardestroyer_Warp.alo", + "message": "Unable to find .ALO file \u0027Cin_EV_Stardestroyer_Warp.alo\u0027", + "context": [ + "Star_Destroyer_Warp" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_particle_master", + "message": "Could not find texture \u0027p_particle_master\u0027 for context: [Test_Particle--\u003EP_DIRT_EMITTER_TEST1.ALO].", + "context": [ + "Test_Particle", + "P_DIRT_EMITTER_TEST1.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "W_Bush_Swmp00.ALO", + "message": "Unable to find .ALO file \u0027W_Bush_Swmp00.ALO\u0027", + "context": [ + "Prop_Swamp_Bush00" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "W_TE_Rock_f_02_b.tga", + "message": "Could not find texture \u0027W_TE_Rock_f_02_b.tga\u0027 for context: [The_Peacebringer--\u003EUV_KRAYTCLASSDESTROYER_TYBER.ALO].", + "context": [ + "The_Peacebringer", + "UV_KRAYTCLASSDESTROYER_TYBER.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_Reb_CelebHall_Wall_B.tga", + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall_B.tga\u0027 for context: [Cin_sith_lefthall--\u003EW_SITH_LEFTHALL.ALO].", + "context": [ + "Cin_sith_lefthall", + "W_SITH_LEFTHALL.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_Shuttle_Tyderium.alo", + "message": "Unable to find .ALO file \u0027Cin_Shuttle_Tyderium.alo\u0027", + "context": [ + "Intro2_Shuttle_Tyderium" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_smoke_small_thin2", + "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_PRISON.ALO\u0027", + "context": [ + "Imperial_Prison_Facility", + "DATA\\ART\\MODELS\\NB_PRISON.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "P_mptl-2a_Die", + "message": "Proxy particle \u0027P_mptl-2a_Die\u0027 not found for model \u0027DATA\\ART\\MODELS\\RV_MPTL-2A.ALO\u0027", + "context": [ + "MPTL", + "DATA\\ART\\MODELS\\RV_MPTL-2A.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_DeathStar_High.alo", + "message": "Unable to find .ALO file \u0027Cin_DeathStar_High.alo\u0027", + "context": [ + "UM05_PROP_DSTAR" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CIN_Rbel_GreyGroup.alo", + "message": "Unable to find .ALO file \u0027CIN_Rbel_GreyGroup.alo\u0027", + "context": [ + "Cin_Rebel_GreyGroup" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_Officer.alo", + "message": "Unable to find .ALO file \u0027Cin_Officer.alo\u0027", + "context": [ + "FIN_Officer" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CIN_Reb_CelebHall.alo", + "message": "Unable to find .ALO file \u0027CIN_Reb_CelebHall.alo\u0027", + "context": [ + "REb_CelebHall" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_ewok_drag_dirt", + "message": "Proxy particle \u0027p_ewok_drag_dirt\u0027 not found for model \u0027DATA\\ART\\MODELS\\UI_EWOK_HANDLER.ALO\u0027", + "context": [ + "Ewok_Handler", + "DATA\\ART\\MODELS\\UI_EWOK_HANDLER.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_steam_small", + "message": "Proxy particle \u0027p_steam_small\u0027 not found for model \u0027DATA\\ART\\MODELS\\RB_HEAVYVEHICLEFACTORY.ALO\u0027", + "context": [ + "R_Ground_Heavy_Vehicle_Factory", + "DATA\\ART\\MODELS\\RB_HEAVYVEHICLEFACTORY.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_DeathStar_Wall.alo", + "message": "Unable to find .ALO file \u0027Cin_DeathStar_Wall.alo\u0027", + "context": [ + "Death_Star_Hangar_Outside" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "W_Kamino_Reflect.ALO", + "message": "Unable to find .ALO file \u0027W_Kamino_Reflect.ALO\u0027", + "context": [ + "Prop_Kamino_Reflection_00" + ] + }, + { + "id": "FILE00", + "severity": "Warning", + "asset": "i_button_general_dodonna.tga", + "message": "Could not find icon \u0027i_button_general_dodonna.tga\u0027 for game object type \u0027General_Dodonna\u0027.", + "context": [ + "General_Dodonna" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_cold_tiny01", + "message": "Proxy particle \u0027p_cold_tiny01\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_SCH.ALO\u0027", + "context": [ + "Arctic_Civilian_Spawn_House", + "DATA\\ART\\MODELS\\NB_SCH.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_desert_ground_dust", + "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\EI_MARAJADE.ALO\u0027", + "context": [ + "Mara_Jade", + "DATA\\ART\\MODELS\\EI_MARAJADE.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_smoke_small_thin2", + "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027DATA\\ART\\MODELS\\RB_HYPERVELOCITYGUN.ALO\u0027", + "context": [ + "Ground_Empire_Hypervelocity_Gun", + "DATA\\ART\\MODELS\\RB_HYPERVELOCITYGUN.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_uwstation_death", + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_02_STATION_D.ALO\u0027", + "context": [ + "Underworld_Star_Base_2_Death_Clone", + "DATA\\ART\\MODELS\\UB_02_STATION_D.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_splash_wake_lava.alo", + "message": "Unable to find .ALO file \u0027p_splash_wake_lava.alo\u0027", + "context": [ + "Splash_Wake_Lava" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_Reb_CelebHall_Wall.tga", + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall.tga\u0027 for context: [Cin_sith_console--\u003EW_SITH_CONSOLE.ALO].", + "context": [ + "Cin_sith_console", + "W_SITH_CONSOLE.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_EV_TieAdvanced.alo", + "message": "Unable to find .ALO file \u0027Cin_EV_TieAdvanced.alo\u0027", + "context": [ + "Fin_Vader_TIE" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_uwstation_death", + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_03_STATION_D.ALO\u0027", + "context": [ + "Underworld_Star_Base_3_Death_Clone", + "DATA\\ART\\MODELS\\UB_03_STATION_D.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CIN_Trooper_Row.alo", + "message": "Unable to find .ALO file \u0027CIN_Trooper_Row.alo\u0027", + "context": [ + "Cin_Trooper_Row" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CIN_Officer_Row.alo", + "message": "Unable to find .ALO file \u0027CIN_Officer_Row.alo\u0027", + "context": [ + "Cin_Officer_Row" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CINE_EV_StarDestroyer.ALO", + "message": "Unable to find .ALO file \u0027CINE_EV_StarDestroyer.ALO\u0027", + "context": [ + "CIN_Star_Destroyer3X" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_EV_lambdaShuttle_150.alo", + "message": "Unable to find .ALO file \u0027Cin_EV_lambdaShuttle_150.alo\u0027", + "context": [ + "Lambda_Shuttle_150" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Lensflare0", + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_MEDIUM.ALO\u0027", + "context": [ + "Stars_Medium", + "DATA\\ART\\MODELS\\W_STARS_MEDIUM.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_DStar_TurretLasers.alo", + "message": "Unable to find .ALO file \u0027Cin_DStar_TurretLasers.alo\u0027", + "context": [ + "TurretLasers_DStar_Xplode" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CIN_Rbel_Soldier.alo", + "message": "Unable to find .ALO file \u0027CIN_Rbel_Soldier.alo\u0027", + "context": [ + "Cin_Rebel_soldier" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0301_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "p_smoke_small_thin4", + "message": "Proxy particle \u0027p_smoke_small_thin4\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_PRISON.ALO\u0027", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0301_ENG.WAV" + "Imperial_Prison_Facility", + "DATA\\ART\\MODELS\\NB_PRISON.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0503_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Cin_ImperialCraft.alo", + "message": "Unable to find .ALO file \u0027Cin_ImperialCraft.alo\u0027", "context": [ - "Unit_Remove_Corruption_Leia" - ], - "asset": "U000_LEI0503_ENG.WAV" + "Intro2_ImperialCraft" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0109_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "W_SwampGasEmit.ALO", + "message": "Unable to find .ALO file \u0027W_SwampGasEmit.ALO\u0027", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0109_ENG.WAV" + "Prop_SwampGasEmitter" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0308_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "p_desert_ground_dust", + "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\UI_IG88.ALO\u0027", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0308_ENG.WAV" + "IG-88", + "DATA\\ART\\MODELS\\UI_IG88.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0402_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Cin_Planet_Hoth_High.alo", + "message": "Unable to find .ALO file \u0027Cin_Planet_Hoth_High.alo\u0027", "context": [ - "Unit_Guard_Leia" - ], - "asset": "U000_LEI0402_ENG.WAV" + "Hoth_Backdrop_Large 6x" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0108_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "NB_YsalamiriTree_B.tga", + "message": "Could not find texture \u0027NB_YsalamiriTree_B.tga\u0027 for context: [Ysalamiri_Tree--\u003ENB_YSALAMIRI_TREE.ALO].", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0108_ENG.WAV" + "Ysalamiri_Tree", + "NB_YSALAMIRI_TREE.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0307_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "CIN_DeathStar_Hangar.alo", + "message": "Unable to find .ALO file \u0027CIN_DeathStar_Hangar.alo\u0027", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0307_ENG.WAV" + "Cin_DeathStar_Hangar" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0311_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Lensflare0", + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_STARS_CINE.ALO\u0027", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0311_ENG.WAV" + "Stars_Cinematic", + "DATA\\ART\\MODELS\\W_STARS_CINE.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0102_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "w_sith_arch.alo", + "message": "Unable to find .ALO file \u0027w_sith_arch.alo\u0027", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0102_ENG.WAV" + "Cin_sith_arch" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0104_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "W_TE_Rock_f_02_b.tga", + "message": "Could not find texture \u0027W_TE_Rock_f_02_b.tga\u0027 for context: [F9TZ_Cloaking_Transport--\u003EUV_F9TZTRANSPORT.ALO].", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0104_ENG.WAV" + "F9TZ_Cloaking_Transport", + "UV_F9TZTRANSPORT.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027FS_BEETLE_3.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Cin_Reb_CelebHall_Wall_B.tga", + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall_B.tga\u0027 for context: [Cin_w_tile--\u003EW_TILE.ALO].", "context": [ - "SFX_Anim_Beetle_Footsteps" - ], - "asset": "FS_BEETLE_3.WAV" + "Cin_w_tile", + "W_TILE.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0313_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "CIN_Probe_Droid.alo", + "message": "Unable to find .ALO file \u0027CIN_Probe_Droid.alo\u0027", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0313_ENG.WAV" + "Empire_Droid" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0206_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "CIN_Fire_Medium.alo", + "message": "Unable to find .ALO file \u0027CIN_Fire_Medium.alo\u0027", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0206_ENG.WAV" + "Fin_Fire_Medium" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_ARC3104_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Cin_Planet_Alderaan_High.alo", + "message": "Unable to find .ALO file \u0027Cin_Planet_Alderaan_High.alo\u0027", "context": [ - "Unit_Produce_Troops_Arc_Hammer" - ], - "asset": "U000_ARC3104_ENG.WAV" + "Alderaan_Backdrop_Large 6x" + ] + }, + { + "id": "FILE00", + "severity": "Warning", + "asset": "i_button_ni_nightsister_ranger.tga", + "message": "Could not find icon \u0027i_button_ni_nightsister_ranger.tga\u0027 for game object type \u0027Dathomir_Night_Sister\u0027.", + "context": [ + "Dathomir_Night_Sister" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0312_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Cin_Reb_CelebHall_Wall.tga", + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall.tga\u0027 for context: [Cin_w_tile--\u003EW_TILE.ALO].", "context": [ - "Unit_Attack_Leia" - ], - "asset": "U000_LEI0312_ENG.WAV" + "Cin_w_tile", + "W_TILE.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0215_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "W_Kamino_Reflect.ALO", + "message": "Unable to find .ALO file \u0027W_Kamino_Reflect.ALO\u0027", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0215_ENG.WAV" + "Prop_Kamino_Reflection_01" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0107_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "CIN_Lambda_Head.alo", + "message": "Unable to find .ALO file \u0027CIN_Lambda_Head.alo\u0027", "context": [ - "Unit_Select_Leia" - ], - "asset": "U000_LEI0107_ENG.WAV" + "Cin_Lambda_Head" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0501_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "p_explosion_small_delay00", + "message": "Proxy particle \u0027p_explosion_small_delay00\u0027 not found for model \u0027DATA\\ART\\MODELS\\EB_COMMANDCENTER.ALO\u0027", "context": [ - "Unit_Remove_Corruption_Leia" - ], - "asset": "U000_LEI0501_ENG.WAV" + "Imperial_Command_Center", + "DATA\\ART\\MODELS\\EB_COMMANDCENTER.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027AMB_DES_CLEAR_LOOP_1.WAV\u0027 could not be found.", "severity": "Error", + "asset": "p_smoke_small_thin2", + "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_MONCAL_BUILDING.ALO\u0027", "context": [ - "Weather_Ambient_Clear_Sandstorm_Loop" - ], - "asset": "AMB_DES_CLEAR_LOOP_1.WAV" + "MonCalamari_Spawn_House", + "DATA\\ART\\MODELS\\NB_MONCAL_BUILDING.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0504_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "W_Volcano_Rock02.ALO", + "message": "Unable to find .ALO file \u0027W_Volcano_Rock02.ALO\u0027", "context": [ - "Unit_Remove_Corruption_Leia" - ], - "asset": "U000_LEI0504_ENG.WAV" + "Prop_Volcano_RockForm03" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0502_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Cin_EI_Vader.alo", + "message": "Unable to find .ALO file \u0027Cin_EI_Vader.alo\u0027", "context": [ - "Unit_Remove_Corruption_Leia" - ], - "asset": "U000_LEI0502_ENG.WAV" + "Cin_Vader_Shot_6-9" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_DEF3106_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "p_uwstation_death", + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027DATA\\ART\\MODELS\\UB_01_STATION_D.ALO\u0027", "context": [ - "Unit_Weaken_Sabateur" - ], - "asset": "U000_DEF3106_ENG.WAV" + "Underworld_Star_Base_1_Death_Clone", + "DATA\\ART\\MODELS\\UB_01_STATION_D.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_ARC3105_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Cin_Reb_CelebHall_Wall.tga", + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall.tga\u0027 for context: [Cin_sith_lefthall--\u003EW_SITH_LEFTHALL.ALO].", "context": [ - "Unit_Complete_Troops_Arc_Hammer" - ], - "asset": "U000_ARC3105_ENG.WAV" + "Cin_sith_lefthall", + "W_SITH_LEFTHALL.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0601_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "CIN_NavyTrooper_Row.alo", + "message": "Unable to find .ALO file \u0027CIN_NavyTrooper_Row.alo\u0027", "context": [ - "Unit_Increase_Production_Leia" - ], - "asset": "U000_LEI0601_ENG.WAV" + "Cin_NavyTrooper_Row" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0204_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "p_ssd_debris", + "message": "Proxy particle \u0027p_ssd_debris\u0027 not found for model \u0027DATA\\ART\\MODELS\\UV_ECLIPSE_UC_DC.ALO\u0027", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0204_ENG.WAV" + "Eclipse_Super_Star_Destroyer_Death_Clone", + "DATA\\ART\\MODELS\\UV_ECLIPSE_UC_DC.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027U000_LEI0207_ENG.WAV\u0027 could not be found.", "severity": "Error", + "asset": "Cin_EI_Vader.alo", + "message": "Unable to find .ALO file \u0027Cin_EI_Vader.alo\u0027", "context": [ - "Unit_Move_Leia" - ], - "asset": "U000_LEI0207_ENG.WAV" + "Cin_Vader" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027FS_BEETLE_4.WAV\u0027 could not be found.", "severity": "Error", + "asset": "p_explosion_smoke_small_thin5", + "message": "Proxy particle \u0027p_explosion_smoke_small_thin5\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_NOGHRI_HUT.ALO\u0027", "context": [ - "SFX_Anim_Beetle_Footsteps" - ], - "asset": "FS_BEETLE_4.WAV" + "Noghri_Spawn_House", + "DATA\\ART\\MODELS\\NB_NOGHRI_HUT.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.AudioFilesVerifier" - ], - "message": "Audio file \u0027AMB_URB_CLEAR_LOOP_1.WAV\u0027 could not be found.", "severity": "Error", + "asset": "p_hp_archammer-damage", + "message": "Proxy particle \u0027p_hp_archammer-damage\u0027 not found for model \u0027DATA\\ART\\MODELS\\EV_ARCHAMMER.ALO\u0027", "context": [ - "Weather_Ambient_Clear_Urban_Loop" - ], - "asset": "AMB_URB_CLEAR_LOOP_1.WAV" + "Arc_Hammer", + "DATA\\ART\\MODELS\\EV_ARCHAMMER.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" - ], - "message": "Could not find GUI texture \u0027i_dialogue_button_large_middle_off.tga\u0027 at location \u0027Repository\u0027.", "severity": "Error", + "asset": "NB_YsalamiriTree_B.tga", + "message": "Could not find texture \u0027NB_YsalamiriTree_B.tga\u0027 for context: [Underworld_Ysalamiri_Cage--\u003EUV_MDU_CAGE.ALO].", "context": [ - "IDC_PLAY_FACTION_B_BUTTON_BIG", - "Repository" - ], - "asset": "i_dialogue_button_large_middle_off.tga" + "Underworld_Ysalamiri_Cage", + "UV_MDU_CAGE.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" - ], - "message": "Could not find GUI texture \u0027underworld_logo_selected.tga\u0027 at location \u0027MegaTexture\u0027.", "severity": "Error", + "asset": "Cin_bridge.alo", + "message": "Unable to find .ALO file \u0027Cin_bridge.alo\u0027", "context": [ - "IDC_PLAY_FACTION_A_BUTTON_BIG", - "MegaTexture" - ], - "asset": "underworld_logo_selected.tga" + "UM05_PROP_BRIDGE" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" - ], - "message": "Could not find GUI texture \u0027underworld_logo_off.tga\u0027 at location \u0027MegaTexture\u0027.", "severity": "Error", + "asset": "Cin_DStar_protons.alo", + "message": "Unable to find .ALO file \u0027Cin_DStar_protons.alo\u0027", "context": [ - "IDC_PLAY_FACTION_A_BUTTON_BIG", - "MegaTexture" - ], - "asset": "underworld_logo_off.tga" + "Protons_DStar_Xplode" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" - ], - "message": "Could not find GUI texture \u0027i_button_petro_sliver.tga\u0027 at location \u0027MegaTexture\u0027.", "severity": "Error", + "asset": "Cin_DeathStar_High.alo", + "message": "Unable to find .ALO file \u0027Cin_DeathStar_High.alo\u0027", "context": [ - "IDC_MENU_PETRO_LOGO", - "MegaTexture" - ], - "asset": "i_button_petro_sliver.tga" + "Death_Star_Whole" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Default.fx", + "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\EV_TIE_LANCET.ALO\u0027.", + "context": [ + "Lancet_Air_Artillery", + "DATA\\ART\\MODELS\\EV_TIE_LANCET.ALO" + ] }, { "id": "FILE00", - "verifiers": [ - "AET.ModVerify.Verifiers.GuiDialogs.GuiDialogsVerifier" - ], - "message": "Could not find GUI texture \u0027underworld_logo_rollover.tga\u0027 at location \u0027MegaTexture\u0027.", "severity": "Error", + "asset": "UB_girder_B.tga", + "message": "Could not find texture \u0027UB_girder_B.tga\u0027 for context: [Underworld_Ysalamiri_Cage--\u003EUV_MDU_CAGE.ALO].", "context": [ - "IDC_PLAY_FACTION_A_BUTTON_BIG", - "MegaTexture" - ], - "asset": "underworld_logo_rollover.tga" + "Underworld_Ysalamiri_Cage", + "UV_MDU_CAGE.ALO" + ] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_planet_land_forces\u0027 is not connected to a shell component.", + "id": "FILE00", + "severity": "Error", + "asset": "Cin_bridge.alo", + "message": "Unable to find .ALO file \u0027Cin_bridge.alo\u0027", + "context": [ + "Imperial_Bridge" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_desert_ground_dust", + "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\UI_SABOTEUR.ALO\u0027", + "context": [ + "Underworld_Saboteur", + "DATA\\ART\\MODELS\\UI_SABOTEUR.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CIN_p_proton_torpedo.alo", + "message": "Unable to find .ALO file \u0027CIN_p_proton_torpedo.alo\u0027", + "context": [ + "Cin_Proj_Ground_Proton_Torpedo" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CIN_Rbel_grey.alo", + "message": "Unable to find .ALO file \u0027CIN_Rbel_grey.alo\u0027", + "context": [ + "Cin_Rebel_Grey" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Default.fx", + "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027DATA\\ART\\MODELS\\EV_MDU_SENSORNODE.ALO\u0027.", + "context": [ + "Empire_Offensive_Sensor_Node", + "DATA\\ART\\MODELS\\EV_MDU_SENSORNODE.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "w_grenade.tga", + "message": "Could not find texture \u0027w_grenade.tga\u0027 for context: [Proj_Merc_Concussion_Grenade--\u003EW_GRENADE.ALO].", + "context": [ + "Proj_Merc_Concussion_Grenade", + "W_GRENADE.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CIN_Fire_Huge.alo", + "message": "Unable to find .ALO file \u0027CIN_Fire_Huge.alo\u0027", + "context": [ + "Fin_Fire_Huge" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_EI_Palpatine.alo", + "message": "Unable to find .ALO file \u0027Cin_EI_Palpatine.alo\u0027", + "context": [ + "Cin_Emperor_Shot_6-9" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_desert_ground_dust", + "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027DATA\\ART\\MODELS\\RI_KYLEKATARN.ALO\u0027", + "context": [ + "Kyle_Katarn", + "DATA\\ART\\MODELS\\RI_KYLEKATARN.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "P_heat_small01", + "message": "Proxy particle \u0027P_heat_small01\u0027 not found for model \u0027DATA\\ART\\MODELS\\NB_VCH.ALO\u0027", + "context": [ + "Volcanic_Civilian_Spawn_House_Independent_AI", + "DATA\\ART\\MODELS\\NB_VCH.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CIN_REb_CelebCharacters.alo", + "message": "Unable to find .ALO file \u0027CIN_REb_CelebCharacters.alo\u0027", + "context": [ + "REb_CelebCharacters" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "W_TE_Rock_f_02_b.tga", + "message": "Could not find texture \u0027W_TE_Rock_f_02_b.tga\u0027 for context: [TIE_Phantom--\u003EEV_TIE_PHANTOM.ALO].", + "context": [ + "TIE_Phantom", + "EV_TIE_PHANTOM.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "CIN_Biker_Row.alo", + "message": "Unable to find .ALO file \u0027CIN_Biker_Row.alo\u0027", + "context": [ + "Cin_Biker_Row" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "p_bomb_spin", + "message": "Proxy particle \u0027p_bomb_spin\u0027 not found for model \u0027DATA\\ART\\MODELS\\W_THERMAL_DETONATOR_EMPIRE.ALO\u0027", + "context": [ + "TIE_Bomber_Bombing_Run_Bomb", + "DATA\\ART\\MODELS\\W_THERMAL_DETONATOR_EMPIRE.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_DStar_Dish_close.alo", + "message": "Unable to find .ALO file \u0027Cin_DStar_Dish_close.alo\u0027", + "context": [ + "Death_Star_Dish_Close" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "W_droid_steam.alo", + "message": "Unable to find .ALO file \u0027W_droid_steam.alo\u0027", + "context": [ + "Prop_Droid_Steam" + ] + }, + { + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_planet_land_forces" + "asset": "b_planet_left", + "message": "The CommandBar component \u0027b_planet_left\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_ground_sell\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_ground_sell" + "asset": "encyclopedia_header_text", + "message": "The CommandBar component \u0027encyclopedia_header_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_bracket_medium\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_bracket_medium" + "asset": "st_control_group", + "message": "The CommandBar component \u0027st_control_group\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027b_planet_right\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "b_planet_right" + "asset": "tutorial_text_back", + "message": "The CommandBar component \u0027tutorial_text_back\u0027 is not connected to a shell component.", + "context": [] }, { "id": "CMDBAR04", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_credit_bar\u0027 is not supported by the game.", - "severity": "Information", - "context": [], - "asset": "g_credit_bar" + "severity": "Warning", + "asset": "encyclopedia_back", + "message": "The CommandBar component \u0027encyclopedia_back\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027zoomed_header_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "zoomed_header_text" + "asset": "g_build", + "message": "The CommandBar component \u0027g_build\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_space_level_pips\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_space_level_pips" + "asset": "generic_collision", + "message": "The CommandBar component \u0027generic_collision\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027bribed_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "bribed_icon" + "asset": "objective_back", + "message": "The CommandBar component \u0027objective_back\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027encyclopedia_header_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "encyclopedia_header_text" + "asset": "st_shields_large", + "message": "The CommandBar component \u0027st_shields_large\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027tutorial_text_back\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "tutorial_text_back" + "asset": "zoomed_cost_text", + "message": "The CommandBar component \u0027zoomed_cost_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027encyclopedia_back\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "encyclopedia_back" + "asset": "st_bracket_medium", + "message": "The CommandBar component \u0027st_bracket_medium\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027encyclopedia_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "encyclopedia_text" + "asset": "g_smuggled", + "message": "The CommandBar component \u0027g_smuggled\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027balance_pip\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "balance_pip" + "asset": "skirmish_upgrade", + "message": "The CommandBar component \u0027skirmish_upgrade\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027objective_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "objective_text" + "asset": "st_ability_icon", + "message": "The CommandBar component \u0027st_ability_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027skirmish_upgrade\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "skirmish_upgrade" + "asset": "g_bounty_hunter", + "message": "The CommandBar component \u0027g_bounty_hunter\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027surface_mod_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "surface_mod_icon" + "asset": "radar_blip", + "message": "The CommandBar component \u0027radar_blip\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_hero_health\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_hero_health" + "asset": "g_weather", + "message": "The CommandBar component \u0027g_weather\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027bribe_display\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "bribe_display" + "asset": "st_health_medium", + "message": "The CommandBar component \u0027st_health_medium\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_build\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_build" + "asset": "tutorial_text", + "message": "The CommandBar component \u0027tutorial_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027garrison_slot_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "garrison_slot_icon" + "asset": "garrison_slot_icon", + "message": "The CommandBar component \u0027garrison_slot_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_conflict\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_conflict" + "asset": "encyclopedia_right_text", + "message": "The CommandBar component \u0027encyclopedia_right_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027tooltip_name\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "tooltip_name" + "asset": "g_planet_ring", + "message": "The CommandBar component \u0027g_planet_ring\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027garrison_respawn_counter\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "garrison_respawn_counter" + "asset": "tooltip_price", + "message": "The CommandBar component \u0027tooltip_price\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_ability_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_ability_icon" + "asset": "bribe_display", + "message": "The CommandBar component \u0027bribe_display\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_shields_medium\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_shields_medium" + "asset": "tooltip_icon_land", + "message": "The CommandBar component \u0027tooltip_icon_land\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_health\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_health" + "asset": "encyclopedia_center_text", + "message": "The CommandBar component \u0027encyclopedia_center_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_weather\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_weather" + "asset": "st_bracket_small", + "message": "The CommandBar component \u0027st_bracket_small\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_health_medium\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_health_medium" + "asset": "g_ground_sell", + "message": "The CommandBar component \u0027g_ground_sell\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_power\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_power" + "asset": "st_shields_medium", + "message": "The CommandBar component \u0027st_shields_medium\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_ground_level_pips\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_ground_level_pips" + "asset": "g_corruption_text", + "message": "The CommandBar component \u0027g_corruption_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027zoomed_cost_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "zoomed_cost_text" + "asset": "st_health_bar", + "message": "The CommandBar component \u0027st_health_bar\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027bm_title_4011\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "bm_title_4011" + "asset": "b_quick_ref", + "message": "The CommandBar component \u0027b_quick_ref\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_planet_name\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_planet_name" + "asset": "g_special_ability", + "message": "The CommandBar component \u0027g_special_ability\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_shields_large\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_shields_large" + "asset": "g_hero_icon", + "message": "The CommandBar component \u0027g_hero_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_hero_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_hero_icon" + "asset": "st_hero_health", + "message": "The CommandBar component \u0027st_hero_health\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027generic_flytext\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "generic_flytext" + "asset": "bribed_icon", + "message": "The CommandBar component \u0027bribed_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027reinforcement_counter\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "reinforcement_counter" + "asset": "g_credit_bar", + "message": "The CommandBar component \u0027g_credit_bar\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_planet_value\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_planet_value" + "asset": "g_space_icon", + "message": "The CommandBar component \u0027g_space_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027radar_blip\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "radar_blip" + "asset": "bm_title_4010", + "message": "The CommandBar component \u0027bm_title_4010\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_political_control\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_political_control" + "asset": "help_back", + "message": "The CommandBar component \u0027help_back\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_planet_ring\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_planet_ring" + "asset": "g_space_level", + "message": "The CommandBar component \u0027g_space_level\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_garrison_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_garrison_icon" + "asset": "g_planet_ability", + "message": "The CommandBar component \u0027g_planet_ability\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027encyclopedia_right_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "encyclopedia_right_text" + "asset": "st_shields", + "message": "The CommandBar component \u0027st_shields\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027b_quick_ref\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "b_quick_ref" + "asset": "b_planet_right", + "message": "The CommandBar component \u0027b_planet_right\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027objective_back\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "objective_back" + "asset": "st_health_large", + "message": "The CommandBar component \u0027st_health_large\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027encyclopedia_center_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "encyclopedia_center_text" + "asset": "bm_title_4011", + "message": "The CommandBar component \u0027bm_title_4011\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_shields\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_shields" + "asset": "g_ground_level_pips", + "message": "The CommandBar component \u0027g_ground_level_pips\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_grab_bar\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_grab_bar" + "asset": "g_political_control", + "message": "The CommandBar component \u0027g_political_control\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_smuggler\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_smuggler" + "asset": "st_hero_icon", + "message": "The CommandBar component \u0027st_hero_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_enemy_hero\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_enemy_hero" + "asset": "objective_text", + "message": "The CommandBar component \u0027objective_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027cs_ability_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "cs_ability_text" + "asset": "zoomed_text", + "message": "The CommandBar component \u0027zoomed_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027encyclopedia_cost_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "encyclopedia_cost_text" + "asset": "garrison_respawn_counter", + "message": "The CommandBar component \u0027garrison_respawn_counter\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_planet_ability\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_planet_ability" + "asset": "st_grab_bar", + "message": "The CommandBar component \u0027st_grab_bar\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_control_group\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_control_group" + "asset": "zoomed_center_text", + "message": "The CommandBar component \u0027zoomed_center_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027gui_dialog_tooltip\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "gui_dialog_tooltip" + "asset": "g_space_level_pips", + "message": "The CommandBar component \u0027g_space_level_pips\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027remote_bomb_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "remote_bomb_icon" + "asset": "generic_flytext", + "message": "The CommandBar component \u0027generic_flytext\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027tutorial_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "tutorial_text" + "asset": "gui_dialog_tooltip", + "message": "The CommandBar component \u0027gui_dialog_tooltip\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_space_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_space_icon" + "asset": "st_health", + "message": "The CommandBar component \u0027st_health\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_bracket_large\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_bracket_large" + "asset": "g_planet_name", + "message": "The CommandBar component \u0027g_planet_name\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027zoomed_back\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "zoomed_back" + "asset": "zoomed_header_text", + "message": "The CommandBar component \u0027zoomed_header_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027encyclopedia_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "encyclopedia_icon" + "asset": "g_conflict", + "message": "The CommandBar component \u0027g_conflict\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027zoomed_right_text\u0027 is not connected to a shell component.", - "severity": "Warning", - "context": [], - "asset": "zoomed_right_text" + "id": "CMDBAR03", + "severity": "Information", + "asset": "g_credit_bar", + "message": "The CommandBar component \u0027g_credit_bar\u0027 is not supported by the game.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027b_beacon_t\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "b_beacon_t" + "asset": "zoomed_back", + "message": "The CommandBar component \u0027zoomed_back\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_bounty_hunter\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_bounty_hunter" + "asset": "zoomed_right_text", + "message": "The CommandBar component \u0027zoomed_right_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_credit_bar\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_credit_bar" + "asset": "st_power", + "message": "The CommandBar component \u0027st_power\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_hero\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_hero" + "asset": "st_bracket_large", + "message": "The CommandBar component \u0027st_bracket_large\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027bm_title_4010\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "bm_title_4010" + "asset": "g_ground_icon", + "message": "The CommandBar component \u0027g_ground_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_planet_fleet\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_planet_fleet" + "asset": "g_planet_land_forces", + "message": "The CommandBar component \u0027g_planet_land_forces\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_corruption_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_corruption_icon" + "asset": "tooltip_back", + "message": "The CommandBar component \u0027tooltip_back\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_smuggled\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_smuggled" + "asset": "b_beacon_t", + "message": "The CommandBar component \u0027b_beacon_t\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027help_back\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "help_back" + "asset": "balance_pip", + "message": "The CommandBar component \u0027balance_pip\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_bracket_small\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_bracket_small" + "asset": "g_corruption_icon", + "message": "The CommandBar component \u0027g_corruption_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027objective_header_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "objective_header_text" + "asset": "g_radar_blip", + "message": "The CommandBar component \u0027g_radar_blip\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_corruption_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_corruption_text" + "asset": "g_hero", + "message": "The CommandBar component \u0027g_hero\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_ground_level\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_ground_level" + "asset": "encyclopedia_cost_text", + "message": "The CommandBar component \u0027encyclopedia_cost_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027lt_weather_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "lt_weather_icon" + "asset": "remote_bomb_icon", + "message": "The CommandBar component \u0027remote_bomb_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027cs_ability_button\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "cs_ability_button" + "asset": "surface_mod_icon", + "message": "The CommandBar component \u0027surface_mod_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_radar_view\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_radar_view" + "asset": "cs_ability_button", + "message": "The CommandBar component \u0027cs_ability_button\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027objective_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "objective_icon" + "asset": "encyclopedia_text", + "message": "The CommandBar component \u0027encyclopedia_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027tooltip_back\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "tooltip_back" + "asset": "objective_icon", + "message": "The CommandBar component \u0027objective_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027zoomed_center_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "zoomed_center_text" + "asset": "st_garrison_icon", + "message": "The CommandBar component \u0027st_garrison_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_health_bar\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_health_bar" + "asset": "g_enemy_hero", + "message": "The CommandBar component \u0027g_enemy_hero\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027zoomed_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "zoomed_text" + "asset": "g_radar_view", + "message": "The CommandBar component \u0027g_radar_view\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027generic_collision\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "generic_collision" + "asset": "tactical_sell", + "message": "The CommandBar component \u0027tactical_sell\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027tooltip_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "tooltip_icon" + "asset": "cs_ability_text", + "message": "The CommandBar component \u0027cs_ability_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_radar_blip\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_radar_blip" + "asset": "g_smuggler", + "message": "The CommandBar component \u0027g_smuggler\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_hero_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_hero_icon" + "asset": "reinforcement_counter", + "message": "The CommandBar component \u0027reinforcement_counter\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_space_level\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_space_level" + "asset": "g_planet_fleet", + "message": "The CommandBar component \u0027g_planet_fleet\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027b_planet_left\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "b_planet_left" + "asset": "tooltip_left_text", + "message": "The CommandBar component \u0027tooltip_left_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027tooltip_icon_land\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "tooltip_icon_land" + "asset": "objective_header_text", + "message": "The CommandBar component \u0027objective_header_text\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_ground_icon\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_ground_icon" + "asset": "lt_weather_icon", + "message": "The CommandBar component \u0027lt_weather_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027tooltip_price\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "tooltip_price" + "asset": "g_planet_value", + "message": "The CommandBar component \u0027g_planet_value\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027tooltip_left_text\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "tooltip_left_text" + "asset": "tooltip_icon", + "message": "The CommandBar component \u0027tooltip_icon\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027st_health_large\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "st_health_large" + "asset": "g_ground_level", + "message": "The CommandBar component \u0027g_ground_level\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027tactical_sell\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "tactical_sell" + "asset": "tooltip_name", + "message": "The CommandBar component \u0027tooltip_name\u0027 is not connected to a shell component.", + "context": [] }, { - "id": "CMDBAR05", - "verifiers": [ - "AET.ModVerify.Verifiers.CommandBarVerifier" - ], - "message": "The CommandBar component \u0027g_special_ability\u0027 is not connected to a shell component.", + "id": "CMDBAR04", "severity": "Warning", - "context": [], - "asset": "g_special_ability" + "asset": "encyclopedia_icon", + "message": "The CommandBar component \u0027encyclopedia_icon\u0027 is not connected to a shell component.", + "context": [] } ] } \ No newline at end of file diff --git a/aet.ico b/src/ModVerify.CliApp/Resources/aet.ico similarity index 100% rename from aet.ico rename to src/ModVerify.CliApp/Resources/aet.ico diff --git a/src/ModVerify.CliApp/Settings/ModVerifyAppSettings.cs b/src/ModVerify.CliApp/Settings/ModVerifyAppSettings.cs index e4488a0..add39a5 100644 --- a/src/ModVerify.CliApp/Settings/ModVerifyAppSettings.cs +++ b/src/ModVerify.CliApp/Settings/ModVerifyAppSettings.cs @@ -29,7 +29,7 @@ public required VerificationTargetSettings VerificationTargetSettings init => field = value ?? throw new ArgumentNullException(nameof(value)); } - public required VerifyPipelineSettings VerifyPipelineSettings + public required VerifierServiceSettings VerifierServiceSettings { get; init => field = value ?? throw new ArgumentNullException(nameof(value)); diff --git a/src/ModVerify.CliApp/Settings/SettingsBuilder.cs b/src/ModVerify.CliApp/Settings/SettingsBuilder.cs index 23a89ff..62bca7a 100644 --- a/src/ModVerify.CliApp/Settings/SettingsBuilder.cs +++ b/src/ModVerify.CliApp/Settings/SettingsBuilder.cs @@ -1,5 +1,4 @@ using AET.ModVerify.App.Settings.CommandLine; -using AET.ModVerify.Pipeline; using AET.ModVerify.Settings; using Microsoft.Extensions.DependencyInjection; using System; @@ -31,7 +30,7 @@ private AppVerifySettings BuildFromVerifyVerb(VerifyVerbOption verifyOptions) return new AppVerifySettings(BuildReportSettings()) { ReportDirectory = GetReportDirectory(), - VerifyPipelineSettings = new VerifyPipelineSettings + VerifierServiceSettings = new VerifierServiceSettings { ParallelVerifiers = verifyOptions.Parallel ? 4 : 1, VerifiersProvider = new DefaultGameVerifiersProvider(), @@ -97,7 +96,7 @@ private AppBaselineSettings BuildFromCreateBaselineVerb(CreateBaselineVerbOption { return new AppBaselineSettings(BuildReportSettings()) { - VerifyPipelineSettings = new VerifyPipelineSettings + VerifierServiceSettings = new VerifierServiceSettings { ParallelVerifiers = baselineVerb.Parallel ? 4 : 1, VerifiersProvider = new DefaultGameVerifiersProvider(), diff --git a/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorBase.cs b/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorBase.cs index 68f0f39..0c45986 100644 --- a/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorBase.cs +++ b/src/ModVerify.CliApp/TargetSelectors/VerificationTargetSelectorBase.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO.Abstractions; using System.Linq; -using System.Runtime.InteropServices; using AET.ModVerify.App.GameFinder; using AET.ModVerify.App.Settings; using Microsoft.Extensions.DependencyInjection; diff --git a/src/ModVerify.CliApp/Utilities/ExtensionMethods.cs b/src/ModVerify.CliApp/Utilities/ExtensionMethods.cs index fc07469..5871311 100644 --- a/src/ModVerify.CliApp/Utilities/ExtensionMethods.cs +++ b/src/ModVerify.CliApp/Utilities/ExtensionMethods.cs @@ -1,8 +1,9 @@ -using System.Diagnostics.CodeAnalysis; -using AET.ModVerify.App.Settings.CommandLine; +using AET.ModVerify.App.Settings.CommandLine; using AnakinRaW.ApplicationBase.Environment; using PG.StarWarsGame.Engine; using PG.StarWarsGame.Infrastructure.Games; +using System.Diagnostics.CodeAnalysis; +using System.Linq; namespace AET.ModVerify.App.Utilities; @@ -21,10 +22,6 @@ public GameEngineType Opposite() } } - public static GameEngineType ToEngineType(this GameType type) - { - return (GameEngineType)(int)type; - } extension(ApplicationEnvironment modVerifyEnvironment) { public bool IsUpdatable() @@ -39,6 +36,19 @@ public bool IsUpdatable([NotNullWhen(true)] out UpdatableApplicationEnvironment? } } + public static GameEngineType ToEngineType(this GameType type) + { + return (GameEngineType)(int)type; + } + + public static GameLocations MaskUsername(this GameLocations targetLocation) + { + return new GameLocations( + targetLocation.ModPaths.Select(PathUtilities.MaskUsername).ToList(), + PathUtilities.MaskUsername(targetLocation.GamePath), + targetLocation.FallbackPaths.Select(PathUtilities.MaskUsername).ToList()); + } + public static bool LaunchedWithoutArguments(this BaseModVerifyOptions options) { if (options is VerifyVerbOption verifyOptions) diff --git a/src/ModVerify.CliApp/Utilities/ModVerifyConsoleUtilities.cs b/src/ModVerify.CliApp/Utilities/ModVerifyConsoleUtilities.cs index b9ebff0..9f9962b 100644 --- a/src/ModVerify.CliApp/Utilities/ModVerifyConsoleUtilities.cs +++ b/src/ModVerify.CliApp/Utilities/ModVerifyConsoleUtilities.cs @@ -2,7 +2,7 @@ using Figgle; using System; using System.Collections.Generic; -using AET.ModVerify.Reporting; +using AET.ModVerify.Reporting.Baseline; namespace AET.ModVerify.App.Utilities; diff --git a/src/ModVerify/DefaultGameVerifiersProvider.cs b/src/ModVerify/DefaultGameVerifiersProvider.cs new file mode 100644 index 0000000..d7ce226 --- /dev/null +++ b/src/ModVerify/DefaultGameVerifiersProvider.cs @@ -0,0 +1,28 @@ +using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers; +using AET.ModVerify.Verifiers.CommandBar; +using AET.ModVerify.Verifiers.Engine; +using AET.ModVerify.Verifiers.GameObjects; +using AET.ModVerify.Verifiers.GuiDialogs; +using AET.ModVerify.Verifiers.SfxEvents; +using PG.StarWarsGame.Engine; +using System; +using System.Collections.Generic; + +namespace AET.ModVerify; + +public sealed class DefaultGameVerifiersProvider : IGameVerifiersProvider +{ + public IEnumerable GetVerifiers( + IStarWarsGameEngine gameEngine, + GameVerifySettings settings, + IServiceProvider serviceProvider) + { + //yield break; + yield return new SfxEventVerifier(gameEngine, settings, serviceProvider); + yield return new HardcodedAssetsVerifier(gameEngine, settings, serviceProvider); + yield return new GuiDialogsVerifier(gameEngine, settings, serviceProvider); + yield return new GameObjectTypeVerifier(gameEngine, settings, serviceProvider); + yield return new CommandBarVerifier(gameEngine, settings, serviceProvider); + } +} \ No newline at end of file diff --git a/src/ModVerify/GameVerificationException.cs b/src/ModVerify/GameVerificationException.cs index dba312b..a6ada92 100644 --- a/src/ModVerify/GameVerificationException.cs +++ b/src/ModVerify/GameVerificationException.cs @@ -7,8 +7,11 @@ namespace AET.ModVerify; public sealed class GameVerificationException : Exception { + /// + public override string Message => ErrorMessage; + public IReadOnlyCollection Errors { get; } - + private string ErrorMessage { get @@ -23,14 +26,11 @@ private string ErrorMessage } } = null; - /// - public override string Message => ErrorMessage; - - public GameVerificationException(VerificationError error) : this([error]) + internal GameVerificationException(VerificationError error) : this([error]) { } - public GameVerificationException(IEnumerable errors) + internal GameVerificationException(IEnumerable errors) { if (errors is null) throw new ArgumentNullException(nameof(errors)); diff --git a/src/ModVerify/Pipeline/GameVerifierPipelineStep.cs b/src/ModVerify/GameVerifierPipelineStep.cs similarity index 91% rename from src/ModVerify/Pipeline/GameVerifierPipelineStep.cs rename to src/ModVerify/GameVerifierPipelineStep.cs index e83cb25..b1f45b8 100644 --- a/src/ModVerify/Pipeline/GameVerifierPipelineStep.cs +++ b/src/ModVerify/GameVerifierPipelineStep.cs @@ -1,16 +1,16 @@ -using AET.ModVerify.Verifiers; +using System; +using System.Threading; +using System.Threading.Tasks; +using AET.ModVerify.Progress; +using AET.ModVerify.Verifiers; using AnakinRaW.CommonUtilities.SimplePipeline; using AnakinRaW.CommonUtilities.SimplePipeline.Progress; using AnakinRaW.CommonUtilities.SimplePipeline.Steps; using Microsoft.Extensions.Logging; -using System; -using System.Threading; -using System.Threading.Tasks; -using AET.ModVerify.Pipeline.Progress; -namespace AET.ModVerify.Pipeline; +namespace AET.ModVerify; -public sealed class GameVerifierPipelineStep( +internal sealed class GameVerifierPipelineStep( GameVerifier verifier, IServiceProvider serviceProvider) : PipelineStep(serviceProvider), IProgressStep diff --git a/src/ModVerify/GameVerifierService.cs b/src/ModVerify/GameVerifierService.cs new file mode 100644 index 0000000..f6d8f07 --- /dev/null +++ b/src/ModVerify/GameVerifierService.cs @@ -0,0 +1,66 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using AET.ModVerify.Progress; +using AET.ModVerify.Reporting; +using AET.ModVerify.Reporting.Baseline; +using AET.ModVerify.Reporting.Suppressions; +using AET.ModVerify.Settings; +using PG.StarWarsGame.Engine; + +namespace AET.ModVerify; + +internal sealed class GameVerifierService(IServiceProvider serviceProvider) : IGameVerifierService +{ + public async Task VerifyAsync( + VerificationTarget verificationTarget, + VerifierServiceSettings settings, + VerificationBaseline baseline, + SuppressionList suppressions, + IVerifyProgressReporter? progressReporter, + IGameEngineInitializationReporter? engineInitializationReporter, + CancellationToken token = default) + { + if (verificationTarget == null) + throw new ArgumentNullException(nameof(verificationTarget)); + if (settings == null) + throw new ArgumentNullException(nameof(settings)); + + using var pipeline = new GameVerifyPipeline( + verificationTarget, + settings, + serviceProvider, + baseline, + suppressions, + progressReporter, + engineInitializationReporter); + + VerificationCompletionStatus completionStatus; + var start = DateTime.UtcNow; + + try + { + await pipeline.RunAsync(token).ConfigureAwait(false); + completionStatus = VerificationCompletionStatus.Completed; + } + catch (OperationCanceledException) + { + completionStatus = settings.FailFastSettings.IsFailFast + ? VerificationCompletionStatus.CompletedFailFast + : VerificationCompletionStatus.Cancelled; + } + + var duration = DateTime.UtcNow - start; + + return new VerificationResult + { + Duration = duration, + Errors = pipeline.Errors, + Status = completionStatus, + Target = verificationTarget, + UsedBaseline = baseline, + UsedSuppressions = suppressions, + Verifiers = pipeline.Verifiers + }; + } +} \ No newline at end of file diff --git a/src/ModVerify/Pipeline/GameVerifyPipeline.cs b/src/ModVerify/GameVerifyPipeline.cs similarity index 55% rename from src/ModVerify/Pipeline/GameVerifyPipeline.cs rename to src/ModVerify/GameVerifyPipeline.cs index 493aa8c..4a7e0fb 100644 --- a/src/ModVerify/Pipeline/GameVerifyPipeline.cs +++ b/src/ModVerify/GameVerifyPipeline.cs @@ -1,63 +1,69 @@ -using AET.ModVerify.Pipeline.Progress; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AET.ModVerify.Progress; using AET.ModVerify.Reporting; +using AET.ModVerify.Reporting.Baseline; +using AET.ModVerify.Reporting.Engine; +using AET.ModVerify.Reporting.Suppressions; using AET.ModVerify.Settings; +using AET.ModVerify.Utilities; using AET.ModVerify.Verifiers; +using AET.ModVerify.Verifiers.Engine; +using AET.ModVerify.Verifiers.Utilities; using AnakinRaW.CommonUtilities.SimplePipeline; using AnakinRaW.CommonUtilities.SimplePipeline.Runners; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using PG.StarWarsGame.Engine; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using AET.ModVerify.Utilities; -using Microsoft.Extensions.DependencyInjection; -namespace AET.ModVerify.Pipeline; +namespace AET.ModVerify; -public sealed class GameVerifyPipeline : StepRunnerPipelineBase +internal sealed class GameVerifyPipeline : StepRunnerPipelineBase { private readonly List _verifiers = []; + private readonly List _errors = []; private readonly List _verificationSteps = []; - private readonly ConcurrentGameEngineErrorReporter _engineErrorReporter = new(); - + + private readonly GameEngineErrorCollection _engineErrorReporter = new(); private readonly VerificationTarget _verificationTarget; - private readonly VerifyPipelineSettings _pipelineSettings; - private readonly IVerifyProgressReporter _progressReporter; + private readonly VerifierServiceSettings _serviceSettings; + private readonly IVerifyProgressReporter? _progressReporter; private readonly IGameEngineInitializationReporter? _engineInitializationReporter; - private readonly IPetroglyphStarWarsGameEngineService _gameEngineService; - private readonly ILogger? _logger; - private AggregatedVerifyProgressReporter? _aggregatedVerifyProgressReporter; + private readonly VerificationBaseline _baseline; + private readonly SuppressionList _suppressions; + + internal IReadOnlyCollection Errors => [.._errors]; + + internal IReadOnlyCollection Verifiers => [.. _verifiers]; - public IReadOnlyCollection FilteredErrors { get; private set; } = []; - public VerificationBaseline Baseline { get; } - public SuppressionList Suppressions { get; } + private AggregatedVerifyProgressReporter? _aggregatedVerifyProgressReporter; public GameVerifyPipeline( VerificationTarget verificationTarget, - VerifyPipelineSettings pipelineSettings, - IVerifyProgressReporter progressReporter, - IGameEngineInitializationReporter? engineInitializationReporter, + VerifierServiceSettings serviceSettings, + IServiceProvider serviceProvider, VerificationBaseline baseline, SuppressionList suppressions, - IServiceProvider serviceProvider) : base(serviceProvider) + IVerifyProgressReporter? progressReporter = null, + IGameEngineInitializationReporter? engineInitializationReporter = null) + : base(serviceProvider) { - Baseline = baseline ?? throw new ArgumentNullException(nameof(baseline)); - Suppressions = suppressions ?? throw new ArgumentNullException(nameof(suppressions)); _verificationTarget = verificationTarget ?? throw new ArgumentNullException(nameof(verificationTarget)); - _pipelineSettings = pipelineSettings ?? throw new ArgumentNullException(nameof(pipelineSettings)); - _progressReporter = progressReporter ?? throw new ArgumentNullException(nameof(progressReporter)); + _serviceSettings = serviceSettings ?? throw new ArgumentNullException(nameof(serviceSettings)); + _baseline = baseline ?? throw new ArgumentNullException(nameof(baseline)); + _suppressions = suppressions ?? throw new ArgumentNullException(nameof(suppressions)); + _progressReporter = progressReporter; _engineInitializationReporter = engineInitializationReporter; - _gameEngineService = serviceProvider.GetRequiredService(); - _logger = serviceProvider.GetService()?.CreateLogger(GetType()); - FailFast = pipelineSettings.FailFastSettings.IsFailFast; + FailFast = serviceSettings.FailFastSettings.IsFailFast; } - + protected override AsyncStepRunner CreateRunner() { - var requestedRunnerCount = _pipelineSettings.ParallelVerifiers; + var requestedRunnerCount = _serviceSettings.ParallelVerifiers; return requestedRunnerCount switch { < 0 or > 64 => throw new InvalidOperationException( @@ -70,12 +76,14 @@ protected override AsyncStepRunner CreateRunner() protected override async Task PrepareCoreAsync(CancellationToken token) { _verifiers.Clear(); + _errors.Clear(); IStarWarsGameEngine gameEngine; try { - gameEngine = await _gameEngineService.InitializeAsync( + var engineService = ServiceProvider.GetRequiredService(); + gameEngine = await engineService.InitializeAsync( _verificationTarget.Engine, _verificationTarget.Location, _engineErrorReporter, @@ -85,38 +93,43 @@ protected override async Task PrepareCoreAsync(CancellationToken token) } catch (Exception e) { - _logger?.LogError(e, "Creating game engine failed: {Message}", e.Message); + Logger?.LogError(e, "Creating game engine failed: {Message}", e.Message); throw; } - AddStep(new GameEngineErrorCollector(_engineErrorReporter, gameEngine, _pipelineSettings.GameVerifySettings, ServiceProvider)); + AddStep(new GameEngineErrorCollector(_engineErrorReporter, gameEngine, _serviceSettings.GameVerifySettings, ServiceProvider)); - foreach (var gameVerificationStep in CreateVerificationSteps(gameEngine)) + foreach (var gameVerificationStep in CreateVerifiers(gameEngine)) AddStep(gameVerificationStep); } protected override void OnExecuteStarted() { Logger?.LogInformation("Running game verifiers..."); - _aggregatedVerifyProgressReporter = new AggregatedVerifyProgressReporter(_progressReporter, _verificationSteps); - _progressReporter.Report(0.0, $"Verifying {_verificationTarget.Name}...", VerifyProgress.ProgressType, default); + if (_progressReporter is not null) + { + _aggregatedVerifyProgressReporter = new AggregatedVerifyProgressReporter(_progressReporter, _verificationSteps); + _progressReporter.Report(0.0, $"Verifying {_verificationTarget.Name}...", + VerifyProgress.ProgressType, default); + } } protected override void OnExecuteCompleted() { Logger?.LogInformation("Game verifiers finished."); - FilteredErrors = GetReportableErrors(_verifiers.SelectMany(s => s.VerifyErrors)).ToList(); - _progressReporter.Report(1.0, $"Finished Verifying {_verificationTarget.Name}", VerifyProgress.ProgressType, default); + _errors.AddRange(GetReportableErrors(_verifiers.SelectMany(s => s.VerifyErrors))); + _progressReporter?.Report(1.0, $"Finished Verifying {_verificationTarget.Name}", + VerifyProgress.ProgressType, default); } protected override void OnRunnerExecutionError(object sender, StepRunnerErrorEventArgs e) { if (FailFast && e.Exception is GameVerificationException verificationException) { - var minSeverity = _pipelineSettings.FailFastSettings.MinumumSeverity; + var minSeverity = _serviceSettings.FailFastSettings.MinumumSeverity; var ignoreError = verificationException.Errors .Where(error => error.Severity >= minSeverity) - .All(error => Baseline.Contains(error) || Suppressions.Suppresses(error)); + .All(error => _baseline.Contains(error) || _suppressions.Suppresses(error)); if (ignoreError) return; } @@ -128,6 +141,14 @@ protected override IEnumerable GetFailedSteps(IEnumerable steps) return base.GetFailedSteps(steps).Where(s => s.Error is not GameVerificationException); } + protected override void DisposeResources() + { + base.DisposeResources(); + _engineErrorReporter.Clear(); + _aggregatedVerifyProgressReporter?.Dispose(); + _aggregatedVerifyProgressReporter = null; + } + private void AddStep(GameVerifier verifier) { var verificationStep = new GameVerifierPipelineStep(verifier, ServiceProvider); @@ -141,18 +162,13 @@ private IEnumerable GetReportableErrors(IEnumerable CreateVerificationSteps(IStarWarsGameEngine engine) - { - return _pipelineSettings.VerifiersProvider - .GetVerifiers(engine, _pipelineSettings.GameVerifySettings, ServiceProvider); + return errors.ApplyBaseline(_baseline).ApplySuppressions(_suppressions); } - protected override void DisposeResources() + private IEnumerable CreateVerifiers(IStarWarsGameEngine engine) { - base.DisposeResources(); - _aggregatedVerifyProgressReporter?.Dispose(); + return _serviceSettings.VerifiersProvider + .GetVerifiers(engine, _serviceSettings.GameVerifySettings, ServiceProvider) + .Distinct(NameBasedEqualityComparer.Instance); } } \ No newline at end of file diff --git a/src/ModVerify/IGameVerifierService.cs b/src/ModVerify/IGameVerifierService.cs new file mode 100644 index 0000000..c814779 --- /dev/null +++ b/src/ModVerify/IGameVerifierService.cs @@ -0,0 +1,22 @@ +using System.Threading; +using System.Threading.Tasks; +using AET.ModVerify.Progress; +using AET.ModVerify.Reporting; +using AET.ModVerify.Reporting.Baseline; +using AET.ModVerify.Reporting.Suppressions; +using AET.ModVerify.Settings; +using PG.StarWarsGame.Engine; + +namespace AET.ModVerify; + +public interface IGameVerifierService +{ + Task VerifyAsync( + VerificationTarget verificationTarget, + VerifierServiceSettings settings, + VerificationBaseline baseline, + SuppressionList suppressions, + IVerifyProgressReporter? progressReporter, + IGameEngineInitializationReporter? engineInitializationReporter, + CancellationToken token = default); +} \ No newline at end of file diff --git a/src/ModVerify/Pipeline/IGameVerifiersProvider.cs b/src/ModVerify/IGameVerifiersProvider.cs similarity index 81% rename from src/ModVerify/Pipeline/IGameVerifiersProvider.cs rename to src/ModVerify/IGameVerifiersProvider.cs index 930afa6..c34c98d 100644 --- a/src/ModVerify/Pipeline/IGameVerifiersProvider.cs +++ b/src/ModVerify/IGameVerifiersProvider.cs @@ -4,12 +4,12 @@ using AET.ModVerify.Verifiers; using PG.StarWarsGame.Engine; -namespace AET.ModVerify.Pipeline; +namespace AET.ModVerify; public interface IGameVerifiersProvider { IEnumerable GetVerifiers( - IStarWarsGameEngine database, + IStarWarsGameEngine gameEngine, GameVerifySettings settings, IServiceProvider serviceProvider); } \ No newline at end of file diff --git a/src/ModVerify/ModVerify.csproj b/src/ModVerify/ModVerify.csproj index f1415bc..96656cc 100644 --- a/src/ModVerify/ModVerify.csproj +++ b/src/ModVerify/ModVerify.csproj @@ -1,7 +1,7 @@  - netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1;net10.0 AlamoEngineTools.ModVerify AET.ModVerify AET.ModVerify @@ -10,33 +10,25 @@ ModVerify Core AET.ModVerify - Provides interfaces and classes to verify Empire at War / Forces of Corruption game modifications. + A library that contains classes and methods to verify Empire at War / Forces of Corruption game modifications. alamo,petroglyph,glyphx true true - - - true snupkg - - - - - + - - - - + + + @@ -50,9 +42,4 @@ - - - - - diff --git a/src/ModVerify/ModVerify.csproj.DotSettings b/src/ModVerify/ModVerify.csproj.DotSettings index fcd6f14..d3b37e3 100644 --- a/src/ModVerify/ModVerify.csproj.DotSettings +++ b/src/ModVerify/ModVerify.csproj.DotSettings @@ -1,4 +1,12 @@  - True + True + True + True + True + False + False + True + True True - True \ No newline at end of file + False + False \ No newline at end of file diff --git a/src/ModVerify/ModVerifyServiceExtensions.cs b/src/ModVerify/ModVerifyServiceExtensions.cs index 8b46960..04567e7 100644 --- a/src/ModVerify/ModVerifyServiceExtensions.cs +++ b/src/ModVerify/ModVerifyServiceExtensions.cs @@ -1,12 +1,20 @@ -using AET.ModVerify.Verifiers; +using AET.ModVerify.Verifiers.Caching; using Microsoft.Extensions.DependencyInjection; namespace AET.ModVerify; public static class ModVerifyServiceExtensions { - public static IServiceCollection RegisterVerifierCache(this IServiceCollection serviceCollection) + extension(IServiceCollection serviceCollection) { - return serviceCollection.AddSingleton(sp => new AlreadyVerifiedCache(sp)); + public IServiceCollection AddModVerify() + { + return serviceCollection.AddSingleton(sp => new GameVerifierService(sp)); + } + + public IServiceCollection RegisterVerifierCache() + { + return serviceCollection.AddSingleton(new AlreadyVerifiedCache()); + } } } \ No newline at end of file diff --git a/src/ModVerify/Pipeline/DefaultGameVerifiersProvider.cs b/src/ModVerify/Pipeline/DefaultGameVerifiersProvider.cs deleted file mode 100644 index 26b3893..0000000 --- a/src/ModVerify/Pipeline/DefaultGameVerifiersProvider.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using AET.ModVerify.Settings; -using AET.ModVerify.Verifiers; -using AET.ModVerify.Verifiers.GuiDialogs; -using PG.StarWarsGame.Engine; - -namespace AET.ModVerify.Pipeline; - -public sealed class DefaultGameVerifiersProvider : IGameVerifiersProvider -{ - public IEnumerable GetVerifiers( - IStarWarsGameEngine database, - GameVerifySettings settings, - IServiceProvider serviceProvider) - { - yield return new ReferencedModelsVerifier(database, settings, serviceProvider); - yield return new DuplicateNameFinder(database, settings, serviceProvider); - yield return new AudioFilesVerifier(database, settings, serviceProvider); - yield return new GuiDialogsVerifier(database, settings, serviceProvider); - //yield return new CommandBarVerifier(database, settings, serviceProvider); - } -} \ No newline at end of file diff --git a/src/ModVerify/Pipeline/Progress/AggregatedVerifyProgressReporter.cs b/src/ModVerify/Progress/AggregatedVerifyProgressReporter.cs similarity index 95% rename from src/ModVerify/Pipeline/Progress/AggregatedVerifyProgressReporter.cs rename to src/ModVerify/Progress/AggregatedVerifyProgressReporter.cs index cfdae25..1dbf538 100644 --- a/src/ModVerify/Pipeline/Progress/AggregatedVerifyProgressReporter.cs +++ b/src/ModVerify/Progress/AggregatedVerifyProgressReporter.cs @@ -1,8 +1,8 @@ -using AnakinRaW.CommonUtilities.SimplePipeline.Progress; -using System; +using System; using System.Collections.Generic; +using AnakinRaW.CommonUtilities.SimplePipeline.Progress; -namespace AET.ModVerify.Pipeline.Progress; +namespace AET.ModVerify.Progress; internal class AggregatedVerifyProgressReporter( IVerifyProgressReporter progressReporter, diff --git a/src/ModVerify/Pipeline/Progress/IVerifyProgressReporter.cs b/src/ModVerify/Progress/IVerifyProgressReporter.cs similarity index 76% rename from src/ModVerify/Pipeline/Progress/IVerifyProgressReporter.cs rename to src/ModVerify/Progress/IVerifyProgressReporter.cs index e63e0f9..99c2141 100644 --- a/src/ModVerify/Pipeline/Progress/IVerifyProgressReporter.cs +++ b/src/ModVerify/Progress/IVerifyProgressReporter.cs @@ -1,5 +1,5 @@ using AnakinRaW.CommonUtilities.SimplePipeline.Progress; -namespace AET.ModVerify.Pipeline.Progress; +namespace AET.ModVerify.Progress; public interface IVerifyProgressReporter : IProgressReporter; \ No newline at end of file diff --git a/src/ModVerify/Pipeline/Progress/VerifyProgress.cs b/src/ModVerify/Progress/VerifyProgress.cs similarity index 84% rename from src/ModVerify/Pipeline/Progress/VerifyProgress.cs rename to src/ModVerify/Progress/VerifyProgress.cs index a18f3d3..aa5f2f2 100644 --- a/src/ModVerify/Pipeline/Progress/VerifyProgress.cs +++ b/src/ModVerify/Progress/VerifyProgress.cs @@ -1,6 +1,6 @@ using AnakinRaW.CommonUtilities.SimplePipeline.Progress; -namespace AET.ModVerify.Pipeline.Progress; +namespace AET.ModVerify.Progress; public static class VerifyProgress { diff --git a/src/ModVerify/Pipeline/Progress/VerifyProgressInfo.cs b/src/ModVerify/Progress/VerifyProgressInfo.cs similarity index 74% rename from src/ModVerify/Pipeline/Progress/VerifyProgressInfo.cs rename to src/ModVerify/Progress/VerifyProgressInfo.cs index cadeb0d..b7c8b3d 100644 --- a/src/ModVerify/Pipeline/Progress/VerifyProgressInfo.cs +++ b/src/ModVerify/Progress/VerifyProgressInfo.cs @@ -1,4 +1,4 @@ -namespace AET.ModVerify.Pipeline.Progress; +namespace AET.ModVerify.Progress; public struct VerifyProgressInfo { diff --git a/src/ModVerify/Reporting/Baseline/BaselineVerificationTarget.cs b/src/ModVerify/Reporting/Baseline/BaselineVerificationTarget.cs new file mode 100644 index 0000000..ca77fa8 --- /dev/null +++ b/src/ModVerify/Reporting/Baseline/BaselineVerificationTarget.cs @@ -0,0 +1,12 @@ +using PG.StarWarsGame.Engine; + +namespace AET.ModVerify.Reporting.Baseline; + +public sealed record BaselineVerificationTarget +{ + public required GameEngineType Engine { get; init; } + public required string Name { get; init; } + public GameLocations? Location { get; init; } // Optional compared to Verification Target + public string? Version { get; init; } + public bool IsGame { get; init; } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Baseline/InvalidBaselineException.cs b/src/ModVerify/Reporting/Baseline/InvalidBaselineException.cs new file mode 100644 index 0000000..58e180e --- /dev/null +++ b/src/ModVerify/Reporting/Baseline/InvalidBaselineException.cs @@ -0,0 +1,14 @@ +using System; + +namespace AET.ModVerify.Reporting.Baseline; + +public sealed class InvalidBaselineException : Exception +{ + internal InvalidBaselineException(string message) : base(message) + { + } + + internal InvalidBaselineException(string? message, Exception? inner) : base(message, inner) + { + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonBaselineParser.cs b/src/ModVerify/Reporting/Baseline/Json/JsonBaselineParser.cs similarity index 95% rename from src/ModVerify/Reporting/Json/JsonBaselineParser.cs rename to src/ModVerify/Reporting/Baseline/Json/JsonBaselineParser.cs index 669ef53..a8ba53c 100644 --- a/src/ModVerify/Reporting/Json/JsonBaselineParser.cs +++ b/src/ModVerify/Reporting/Baseline/Json/JsonBaselineParser.cs @@ -2,7 +2,7 @@ using System.IO; using System.Text.Json; -namespace AET.ModVerify.Reporting.Json; +namespace AET.ModVerify.Reporting.Baseline.Json; internal static class JsonBaselineParser { diff --git a/src/ModVerify/Reporting/Json/JsonBaselineSchema.cs b/src/ModVerify/Reporting/Baseline/Json/JsonBaselineSchema.cs similarity index 97% rename from src/ModVerify/Reporting/Json/JsonBaselineSchema.cs rename to src/ModVerify/Reporting/Baseline/Json/JsonBaselineSchema.cs index 12e3705..cf4513a 100644 --- a/src/ModVerify/Reporting/Json/JsonBaselineSchema.cs +++ b/src/ModVerify/Reporting/Baseline/Json/JsonBaselineSchema.cs @@ -7,9 +7,9 @@ using Json.Schema; using Json.Schema.Keywords; -namespace AET.ModVerify.Reporting.Json; +namespace AET.ModVerify.Reporting.Baseline.Json; -public static class JsonBaselineSchema +internal static class JsonBaselineSchema { private static readonly JsonSchema Schema; private static readonly EvaluationOptions EvaluationOptions; diff --git a/src/ModVerify/Reporting/Json/JsonVerificationBaseline.cs b/src/ModVerify/Reporting/Baseline/Json/JsonVerificationBaseline.cs similarity index 87% rename from src/ModVerify/Reporting/Json/JsonVerificationBaseline.cs rename to src/ModVerify/Reporting/Baseline/Json/JsonVerificationBaseline.cs index 0d9a1b7..13d9b56 100644 --- a/src/ModVerify/Reporting/Json/JsonVerificationBaseline.cs +++ b/src/ModVerify/Reporting/Baseline/Json/JsonVerificationBaseline.cs @@ -1,9 +1,10 @@ -using System; +using AET.ModVerify.Reporting.Json; +using System; using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; -namespace AET.ModVerify.Reporting.Json; +namespace AET.ModVerify.Reporting.Baseline.Json; internal class JsonVerificationBaseline { @@ -32,8 +33,8 @@ public JsonVerificationBaseline(VerificationBaseline baseline) [JsonConstructor] private JsonVerificationBaseline( JsonVerificationTarget target, - Version version, - VerificationSeverity minimumSeverity, + Version version, + VerificationSeverity minimumSeverity, IEnumerable errors) { Target = target; diff --git a/src/ModVerify/Reporting/VerificationBaseline.cs b/src/ModVerify/Reporting/Baseline/VerificationBaseline.cs similarity index 94% rename from src/ModVerify/Reporting/VerificationBaseline.cs rename to src/ModVerify/Reporting/Baseline/VerificationBaseline.cs index 3f9c274..80f95b8 100644 --- a/src/ModVerify/Reporting/VerificationBaseline.cs +++ b/src/ModVerify/Reporting/Baseline/VerificationBaseline.cs @@ -6,13 +6,14 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; +using AET.ModVerify.Reporting.Baseline.Json; using AET.ModVerify.Reporting.Json; -namespace AET.ModVerify.Reporting; +namespace AET.ModVerify.Reporting.Baseline; public sealed class VerificationBaseline : IReadOnlyCollection { - public static readonly Version LatestVersion = new(2, 1); + public static readonly Version LatestVersion = new(2, 2); public static readonly string LatestVersionString = LatestVersion.ToString(2); public static readonly VerificationBaseline Empty = new(VerificationSeverity.Information, [], null); @@ -60,7 +61,6 @@ public void ToJson(Stream stream) { JsonSerializer.Serialize(stream, new JsonVerificationBaseline(this), ModVerifyJsonSettings.JsonSettings); } - public Task ToJsonAsync(Stream stream) { return JsonSerializer.SerializeAsync(stream, new JsonVerificationBaseline(this), ModVerifyJsonSettings.JsonSettings); diff --git a/src/ModVerify/Reporting/BaselineVerificationTarget.cs b/src/ModVerify/Reporting/BaselineVerificationTarget.cs deleted file mode 100644 index 6e21ddd..0000000 --- a/src/ModVerify/Reporting/BaselineVerificationTarget.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Text; -using PG.StarWarsGame.Engine; - -namespace AET.ModVerify.Reporting; - -public sealed class BaselineVerificationTarget -{ - public required GameEngineType Engine { get; init; } - public required string Name { get; init; } - public GameLocations? Location { get; init; } // Optional compared to Verification Target - public string? Version { get; init; } - public bool IsGame { get; init; } - - public override string ToString() - { - var sb = new StringBuilder($"[Name={Name};EngineType={Engine};IsGame={IsGame};"); - if (!string.IsNullOrEmpty(Version)) sb.Append($"Version={Version};"); - if (Location is not null) - sb.Append($"Location={Location};"); - sb.Append(']'); - return sb.ToString(); - } -} \ No newline at end of file diff --git a/src/ModVerify/Reporting/ConcurrentGameEngineErrorReporter.cs b/src/ModVerify/Reporting/ConcurrentGameEngineErrorReporter.cs deleted file mode 100644 index 3130547..0000000 --- a/src/ModVerify/Reporting/ConcurrentGameEngineErrorReporter.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using PG.StarWarsGame.Engine.ErrorReporting; - -namespace AET.ModVerify.Reporting; - -public sealed class ConcurrentGameEngineErrorReporter : GameEngineErrorReporter, IGameEngineErrorCollection -{ - private readonly ConcurrentBag _xmlErrors = new(); - private readonly ConcurrentBag _initializationErrors = new(); - private readonly ConcurrentBag _asserts = new(); - - public IEnumerable XmlErrors => _xmlErrors.ToList(); - - public IEnumerable InitializationErrors => _initializationErrors.ToList(); - - public IEnumerable Asserts => _asserts.ToList(); - - public override void Report(XmlError error) - { - _xmlErrors.Add(error); - } - - public override void Report(InitializationError error) - { - _initializationErrors.Add(error); - } - - public override void Assert(EngineAssert assert) - { - _asserts.Add(assert); - } -} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/Engine/EngineErrorReporterBase.cs b/src/ModVerify/Reporting/Engine/EngineErrorReporterBase.cs similarity index 59% rename from src/ModVerify/Reporting/Reporters/Engine/EngineErrorReporterBase.cs rename to src/ModVerify/Reporting/Engine/EngineErrorReporterBase.cs index 455212d..f8c7cd2 100644 --- a/src/ModVerify/Reporting/Reporters/Engine/EngineErrorReporterBase.cs +++ b/src/ModVerify/Reporting/Engine/EngineErrorReporterBase.cs @@ -1,16 +1,30 @@ using System; using System.Collections.Generic; +using AET.ModVerify.Verifiers; using AnakinRaW.CommonUtilities; using PG.StarWarsGame.Engine.IO; -namespace AET.ModVerify.Reporting.Reporters.Engine; +namespace AET.ModVerify.Reporting.Engine; -internal abstract class EngineErrorReporterBase(IGameRepository gameRepository, IServiceProvider serviceProvider) +internal abstract class EngineErrorReporterBase : IGameVerifierInfo { - protected readonly IGameRepository GameRepository = gameRepository ?? throw new ArgumentNullException(nameof(gameRepository)); - protected readonly IServiceProvider ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + protected readonly IGameRepository GameRepository; + protected readonly IServiceProvider ServiceProvider; - public abstract string Name { get; } + public IGameVerifierInfo? Parent => null; + + public IReadOnlyList VerifierChain { get; } + + public string Name => GetType().FullName; + + public abstract string FriendlyName { get; } + + protected EngineErrorReporterBase(IGameRepository gameRepository, IServiceProvider serviceProvider) + { + GameRepository = gameRepository ?? throw new ArgumentNullException(nameof(gameRepository)); + ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + VerifierChain = [this]; + } public IEnumerable GetErrors(IEnumerable errors) { @@ -18,7 +32,7 @@ public IEnumerable GetErrors(IEnumerable errors) { var errorData = CreateError(error); yield return new VerificationError( - errorData.Identifier, errorData.Message, [Name], errorData.Context, errorData.Asset, errorData.Severity); + errorData.Identifier, errorData.Message, this, errorData.Context, errorData.Asset, errorData.Severity); } } diff --git a/src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs b/src/ModVerify/Reporting/Engine/GameAssertErrorReporter.cs similarity index 79% rename from src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs rename to src/ModVerify/Reporting/Engine/GameAssertErrorReporter.cs index 6c5fb17..0c8c633 100644 --- a/src/ModVerify/Reporting/Reporters/Engine/GameAssertErrorReporter.cs +++ b/src/ModVerify/Reporting/Engine/GameAssertErrorReporter.cs @@ -5,19 +5,24 @@ using PG.StarWarsGame.Engine.ErrorReporting; using PG.StarWarsGame.Engine.IO; -namespace AET.ModVerify.Reporting.Reporters.Engine; +namespace AET.ModVerify.Reporting.Engine; internal sealed class GameAssertErrorReporter(IGameRepository gameRepository, IServiceProvider serviceProvider) : EngineErrorReporterBase(gameRepository, serviceProvider) { - public override string Name => "GameAsserts"; + public override string FriendlyName => "Game Engine Asserts"; protected override ErrorData CreateError(EngineAssert assert) { var context = new List(); context.AddRange(assert.Context); context.Add($"location='{GetLocation(assert)}'"); - return new ErrorData(GetIdFromError(assert.Kind), assert.Message, context, assert.Value, VerificationSeverity.Warning); + return new ErrorData( + GetIdFromError(assert.Kind), + assert.Message, + context, + assert.Value, + VerificationSeverity.Warning); } private static string GetLocation(EngineAssert assert) @@ -41,6 +46,7 @@ private static string GetIdFromError(EngineAssertKind assertKind) EngineAssertKind.ValueOutOfRange => VerifierErrorCodes.AssertValueOutOfRange, EngineAssertKind.InvalidValue => VerifierErrorCodes.AssertValueInvalid, EngineAssertKind.FileNotFound => VerifierErrorCodes.FileNotFound, + EngineAssertKind.DuplicateEntry => VerifierErrorCodes.Duplicate, _ => throw new ArgumentOutOfRangeException(nameof(assertKind), assertKind, null) }; } diff --git a/src/ModVerify/Reporting/Engine/GameEngineErrorCollection.cs b/src/ModVerify/Reporting/Engine/GameEngineErrorCollection.cs new file mode 100644 index 0000000..8cb67c0 --- /dev/null +++ b/src/ModVerify/Reporting/Engine/GameEngineErrorCollection.cs @@ -0,0 +1,53 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using PG.StarWarsGame.Engine.ErrorReporting; +using PG.StarWarsGame.Files.XML.ErrorHandling; + +namespace AET.ModVerify.Reporting.Engine; + +public sealed class GameEngineErrorCollection : IGameEngineErrorCollection, IGameEngineErrorReporter +{ + private readonly ConcurrentBag _xmlErrors = new(); + private readonly ConcurrentBag _initializationErrors = new(); + private readonly ConcurrentBag _asserts = new(); + + public IEnumerable XmlErrors => _xmlErrors.ToList(); + + public IEnumerable InitializationErrors => _initializationErrors.ToList(); + + public IEnumerable Asserts => _asserts.ToList(); + + void IXmlParserErrorReporter.Report(XmlError error) + { + _xmlErrors.Add(error); + } + + void IGameEngineErrorReporter.Report(InitializationError error) + { + _initializationErrors.Add(error); + } + + void IGameEngineErrorReporter.Assert(EngineAssert assert) + { + _asserts.Add(assert); + } + + internal void Clear() + { +#if !NETFRAMEWORK && !NETSTANDARD2_0 + _xmlErrors.Clear(); + _initializationErrors.Clear(); + _asserts.Clear(); +#else + ClearUnsafe(_xmlErrors); + ClearUnsafe(_initializationErrors); + ClearUnsafe(_asserts); + + static void ClearUnsafe(ConcurrentBag bag) + { + while (bag.TryTake(out _)) ; + } +#endif + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/IGameEngineErrorCollection.cs b/src/ModVerify/Reporting/Engine/IGameEngineErrorCollection.cs similarity index 76% rename from src/ModVerify/Reporting/IGameEngineErrorCollection.cs rename to src/ModVerify/Reporting/Engine/IGameEngineErrorCollection.cs index 14d59d9..286d047 100644 --- a/src/ModVerify/Reporting/IGameEngineErrorCollection.cs +++ b/src/ModVerify/Reporting/Engine/IGameEngineErrorCollection.cs @@ -1,7 +1,8 @@ using System.Collections.Generic; using PG.StarWarsGame.Engine.ErrorReporting; +using PG.StarWarsGame.Files.XML.ErrorHandling; -namespace AET.ModVerify.Reporting; +namespace AET.ModVerify.Reporting.Engine; public interface IGameEngineErrorCollection { diff --git a/src/ModVerify/Reporting/Reporters/Engine/InitializationErrorReporter.cs b/src/ModVerify/Reporting/Engine/InitializationErrorReporter.cs similarity index 84% rename from src/ModVerify/Reporting/Reporters/Engine/InitializationErrorReporter.cs rename to src/ModVerify/Reporting/Engine/InitializationErrorReporter.cs index fc44c66..e46db1a 100644 --- a/src/ModVerify/Reporting/Reporters/Engine/InitializationErrorReporter.cs +++ b/src/ModVerify/Reporting/Engine/InitializationErrorReporter.cs @@ -3,12 +3,12 @@ using PG.StarWarsGame.Engine.ErrorReporting; using PG.StarWarsGame.Engine.IO; -namespace AET.ModVerify.Reporting.Reporters.Engine; +namespace AET.ModVerify.Reporting.Engine; internal sealed class InitializationErrorReporter(IGameRepository gameRepository, IServiceProvider serviceProvider) : EngineErrorReporterBase(gameRepository, serviceProvider) { - public override string Name => "InitializationErrors"; + public override string FriendlyName => "Initialization Errors"; protected override ErrorData CreateError(InitializationError error) { diff --git a/src/ModVerify/Reporting/Reporters/Engine/XmlParseErrorReporter.cs b/src/ModVerify/Reporting/Engine/XmlParseErrorReporter.cs similarity index 88% rename from src/ModVerify/Reporting/Reporters/Engine/XmlParseErrorReporter.cs rename to src/ModVerify/Reporting/Engine/XmlParseErrorReporter.cs index 094a701..9548ad4 100644 --- a/src/ModVerify/Reporting/Reporters/Engine/XmlParseErrorReporter.cs +++ b/src/ModVerify/Reporting/Engine/XmlParseErrorReporter.cs @@ -4,18 +4,17 @@ using AET.ModVerify.Utilities; using AET.ModVerify.Verifiers; using Microsoft.Extensions.DependencyInjection; -using PG.StarWarsGame.Engine.ErrorReporting; using PG.StarWarsGame.Engine.IO; using PG.StarWarsGame.Files.XML.ErrorHandling; -namespace AET.ModVerify.Reporting.Reporters.Engine; +namespace AET.ModVerify.Reporting.Engine; internal sealed class XmlParseErrorReporter(IGameRepository gameRepository, IServiceProvider serviceProvider) : EngineErrorReporterBase(gameRepository, serviceProvider) { private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); - public override string Name => "XMLError"; + public override string FriendlyName => "XML Errors"; protected override ErrorData CreateError(XmlError error) { @@ -29,7 +28,8 @@ protected override ErrorData CreateError(XmlError error) var context = new List { - strippedFileName + $"Parser: {error.Parser.Name}", + $"File: {strippedFileName}" }; var xmlElement = error.Element; @@ -75,6 +75,8 @@ private static VerificationSeverity GetSeverityFromError(XmlParseErrorKind xmlEr XmlParseErrorKind.DataBeforeHeader => VerificationSeverity.Information, XmlParseErrorKind.MissingNode => VerificationSeverity.Critical, XmlParseErrorKind.UnknownNode => VerificationSeverity.Information, + XmlParseErrorKind.TagHasElements => VerificationSeverity.Warning, + XmlParseErrorKind.UnexceptedElementName => VerificationSeverity.Information, _ => VerificationSeverity.Warning }; } @@ -94,6 +96,8 @@ private static string GetIdFromError(XmlParseErrorKind xmlErrorErrorKind) XmlParseErrorKind.DataBeforeHeader => VerifierErrorCodes.XmlDataBeforeHeader, XmlParseErrorKind.MissingNode => VerifierErrorCodes.XmlMissingNode, XmlParseErrorKind.UnknownNode => VerifierErrorCodes.XmlUnsupportedTag, + XmlParseErrorKind.TagHasElements => VerifierErrorCodes.XmlElementsInTag, + XmlParseErrorKind.UnexceptedElementName => VerifierErrorCodes.XmlUnexceptedElementName, _ => throw new ArgumentOutOfRangeException(nameof(xmlErrorErrorKind), xmlErrorErrorKind, null) }; } diff --git a/src/ModVerify/Reporting/IVerificationReporter.cs b/src/ModVerify/Reporting/IVerificationReporter.cs deleted file mode 100644 index 8b8664e..0000000 --- a/src/ModVerify/Reporting/IVerificationReporter.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace AET.ModVerify.Reporting; - -public interface IVerificationReporter -{ - public Task ReportAsync(IReadOnlyCollection errors); -} \ No newline at end of file diff --git a/src/ModVerify/Reporting/InvalidBaselineException.cs b/src/ModVerify/Reporting/InvalidBaselineException.cs deleted file mode 100644 index 37ab9c8..0000000 --- a/src/ModVerify/Reporting/InvalidBaselineException.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace AET.ModVerify.Reporting; - -public sealed class InvalidBaselineException : Exception -{ - public InvalidBaselineException(string message) : base(message) - { - } - - public InvalidBaselineException(string? message, Exception? inner) : base(message, inner) - { - } -} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonAggregatedVerificationError.cs b/src/ModVerify/Reporting/Json/JsonAggregatedVerificationError.cs new file mode 100644 index 0000000..a60b3de --- /dev/null +++ b/src/ModVerify/Reporting/Json/JsonAggregatedVerificationError.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AET.ModVerify.Reporting.Json; + +internal class JsonAggregatedVerificationError : JsonVerificationErrorBase +{ + [JsonPropertyName("contexts")] + [JsonPropertyOrder(99)] + public IEnumerable> Contexts { get; } + + [JsonConstructor] + public JsonAggregatedVerificationError( + string id, + IReadOnlyList? verifierChain, + string message, + VerificationSeverity severity, + IEnumerable>? contexts, + string? asset) : base(id, severity, asset, message, verifierChain) + { + Contexts = contexts ?? []; + } + + public JsonAggregatedVerificationError( + VerificationError error, + IEnumerable> contexts, + bool verbose = false) + : base(error, verbose) + { + Contexts = contexts; + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonVerificationError.cs b/src/ModVerify/Reporting/Json/JsonVerificationError.cs index 55f7b92..8094058 100644 --- a/src/ModVerify/Reporting/Json/JsonVerificationError.cs +++ b/src/ModVerify/Reporting/Json/JsonVerificationError.cs @@ -1,53 +1,30 @@ using System.Collections.Generic; +using System.Linq; using System.Text.Json.Serialization; namespace AET.ModVerify.Reporting.Json; -internal class JsonVerificationError +internal class JsonVerificationError : JsonVerificationErrorBase { - [JsonPropertyName("id")] - public string Id { get; } - - [JsonPropertyName("verifiers")] - public IReadOnlyList VerifierChain { get; } - - [JsonPropertyName("message")] - public string Message { get; } - - [JsonPropertyName("severity")] - [JsonConverter(typeof(JsonStringEnumConverter))] - public VerificationSeverity Severity { get; } - [JsonPropertyName("context")] + [JsonPropertyOrder(99)] public IEnumerable ContextEntries { get; } - [JsonPropertyName("asset")] - public string Asset { get; } - [JsonConstructor] - private JsonVerificationError( - string id, - IReadOnlyList? verifierChain, + public JsonVerificationError( + string id, + VerificationSeverity severity, + string? asset, string message, - VerificationSeverity severity, - IEnumerable? contextEntries, - string? asset) + IReadOnlyList? verifierChain, + IEnumerable contextEntries) : base(id, severity, asset, message, verifierChain) { - Id = id; - VerifierChain = verifierChain ?? []; - Message = message; - Severity = severity; - ContextEntries = contextEntries ?? []; - Asset = asset ?? string.Empty; + ContextEntries = contextEntries; } - public JsonVerificationError(VerificationError error) + public JsonVerificationError(VerificationError error, bool verbose = false) + : base(error, verbose) { - Id = error.Id; - VerifierChain = error.VerifierChain; - Message = error.Message; - Severity = error.Severity; - ContextEntries = error.ContextEntries; - Asset = error.Asset; + ContextEntries = error.ContextEntries.Any() ? error.ContextEntries : []; } } \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonVerificationErrorBase.cs b/src/ModVerify/Reporting/Json/JsonVerificationErrorBase.cs new file mode 100644 index 0000000..b7aa298 --- /dev/null +++ b/src/ModVerify/Reporting/Json/JsonVerificationErrorBase.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace AET.ModVerify.Reporting.Json; + +[JsonDerivedType(typeof(JsonVerificationError))] +[JsonDerivedType(typeof(JsonAggregatedVerificationError))] +internal abstract class JsonVerificationErrorBase +{ + [JsonPropertyName("id")] + public string Id { get; } + + [JsonPropertyName("severity")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public VerificationSeverity Severity { get; } + + [JsonPropertyName("asset")] + public string Asset { get; } + + [JsonPropertyName("message")] + public string Message { get; } + + [JsonPropertyName("verifiers")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IReadOnlyList? VerifierChain { get; } + + protected JsonVerificationErrorBase(string id, + VerificationSeverity severity, + string? asset, + string message, + IReadOnlyList? verifierChain) + { + Id = id; + VerifierChain = verifierChain; + Message = message; + Severity = severity; + Asset = asset ?? string.Empty; + } + protected JsonVerificationErrorBase(VerificationError error, bool verbose = false) + { + Id = error.Id; + VerifierChain = verbose ? error.VerifierChain.Select(x => x.Name).ToList() : null; + Message = error.Message; + Severity = error.Severity; + Asset = error.Asset; + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonVerificationReport.cs b/src/ModVerify/Reporting/Json/JsonVerificationReport.cs index 6ebb926..d10d18f 100644 --- a/src/ModVerify/Reporting/Json/JsonVerificationReport.cs +++ b/src/ModVerify/Reporting/Json/JsonVerificationReport.cs @@ -3,8 +3,11 @@ namespace AET.ModVerify.Reporting.Json; -internal class JsonVerificationReport(IEnumerable errors) +internal class JsonVerificationReport { + [JsonPropertyName("metadata")] + public required JsonVerificationReportMetadata Metadata { get; init; } + [JsonPropertyName("errors")] - public IEnumerable Errors { get; } = errors; + public required IEnumerable Errors { get; init; } } \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonVerificationReportMetadata.cs b/src/ModVerify/Reporting/Json/JsonVerificationReportMetadata.cs new file mode 100644 index 0000000..7e3b90e --- /dev/null +++ b/src/ModVerify/Reporting/Json/JsonVerificationReportMetadata.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AET.ModVerify.Reporting.Json; + +internal class JsonVerificationReportMetadata +{ + [JsonPropertyName("status")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public VerificationCompletionStatus Status { get; init; } + + [JsonPropertyName("target")] + public JsonVerificationTarget Target { get; init; } + + [JsonPropertyName("time")] + public string Date { get; } = DateTime.Now.ToString("s"); + + [JsonPropertyName("duration")] + public string Duration { get; init; } + + [JsonPropertyName("verifiers")] + public IReadOnlyCollection Verifiers { get; init; } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonVerificationTarget.cs b/src/ModVerify/Reporting/Json/JsonVerificationTarget.cs index 9495456..ccde4b7 100644 --- a/src/ModVerify/Reporting/Json/JsonVerificationTarget.cs +++ b/src/ModVerify/Reporting/Json/JsonVerificationTarget.cs @@ -1,11 +1,12 @@ using System.Text.Json.Serialization; +using AET.ModVerify.Reporting.Baseline; using PG.StarWarsGame.Engine; namespace AET.ModVerify.Reporting.Json; internal class JsonVerificationTarget { - [JsonPropertyName("name")] + [JsonPropertyName("name")] public string Name { get; } [JsonPropertyName("engine")] @@ -17,7 +18,7 @@ internal class JsonVerificationTarget [JsonPropertyName("version")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Version{ get; } + public string? Version { get; } [JsonPropertyName("location")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -25,9 +26,9 @@ internal class JsonVerificationTarget [JsonConstructor] private JsonVerificationTarget( - string name, - string? version, - JsonGameLocation? location, + string name, + string? version, + JsonGameLocation? location, GameEngineType engine, bool isGame) { @@ -38,6 +39,15 @@ private JsonVerificationTarget( IsGame = isGame; } + public JsonVerificationTarget(VerificationTarget target) + { + Name = target.Name; + Version = target.Version; + Engine = target.Engine; + Location = new JsonGameLocation(target.Location); + IsGame = target.IsGame; + } + public JsonVerificationTarget(BaselineVerificationTarget target) { Name = target.Name; diff --git a/src/ModVerify/Reporting/Reporters/ConsoleReporter.cs b/src/ModVerify/Reporting/Reporters/Console/ConsoleReporter.cs similarity index 62% rename from src/ModVerify/Reporting/Reporters/ConsoleReporter.cs rename to src/ModVerify/Reporting/Reporters/Console/ConsoleReporter.cs index dbee50d..5266550 100644 --- a/src/ModVerify/Reporting/Reporters/ConsoleReporter.cs +++ b/src/ModVerify/Reporting/Reporters/Console/ConsoleReporter.cs @@ -2,25 +2,21 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using AET.ModVerify.Reporting.Settings; namespace AET.ModVerify.Reporting.Reporters; -internal class ConsoleReporter( - ReporterSettings settings, - bool summaryOnly, - IServiceProvider serviceProvider) : - ReporterBase(settings, serviceProvider) +internal class ConsoleReporter(ConsoleReporterSettings settings, IServiceProvider serviceProvider) : + ReporterBase(settings, serviceProvider) { - public override Task ReportAsync(IReadOnlyCollection errors) + public override Task ReportAsync(VerificationResult verificationResult) { - var filteredErrors = FilteredErrors(errors).OrderByDescending(x => x.Severity).ToList(); - PrintErrorStats(errors, filteredErrors); + var filteredErrors = FilteredErrors(verificationResult.Errors).OrderByDescending(x => x.Severity).ToList(); + PrintErrorStats(verificationResult, filteredErrors); Console.WriteLine(); return Task.CompletedTask; } - private void PrintErrorStats(IReadOnlyCollection errors, List filteredErrors) + private void PrintErrorStats(VerificationResult verificationResult, List filteredErrors) { Console.WriteLine(); Console.WriteLine(); @@ -28,9 +24,9 @@ private void PrintErrorStats(IReadOnlyCollection errors, List Console.WriteLine(" Error Report "); Console.WriteLine("***********************"); Console.WriteLine(); - if (errors.Count == 0) + if (verificationResult.Errors.Count == 0) { - if (summaryOnly) + if (Settings.SummaryOnly) { Console.WriteLine("No errors found."); } @@ -44,21 +40,21 @@ private void PrintErrorStats(IReadOnlyCollection errors, List return; } - Console.WriteLine($"TOTAL Verification Errors: {errors.Count}"); + Console.WriteLine($"TOTAL Verification Errors: {verificationResult.Errors.Count}"); - var groupedBySeverity = errors.GroupBy(x => x.Severity); + var groupedBySeverity = verificationResult.Errors.GroupBy(x => x.Severity); foreach (var group in groupedBySeverity) Console.WriteLine($" Severity {group.Key}: {group.Count()}"); Console.WriteLine(); if (filteredErrors.Count == 0) { - if (errors.Count != 0) + if (verificationResult.Errors.Count != 0) Console.WriteLine("Some errors are not displayed to the console. Please check the created output files."); return; } - if (summaryOnly) + if (Settings.SummaryOnly) return; Console.WriteLine($"Below the list of errors with severity '{Settings.MinimumReportSeverity}' or higher:"); diff --git a/src/ModVerify/Reporting/Reporters/Console/ConsoleReporterSettings.cs b/src/ModVerify/Reporting/Reporters/Console/ConsoleReporterSettings.cs new file mode 100644 index 0000000..4dc2a77 --- /dev/null +++ b/src/ModVerify/Reporting/Reporters/Console/ConsoleReporterSettings.cs @@ -0,0 +1,6 @@ +namespace AET.ModVerify.Reporting.Reporters; + +public sealed record ConsoleReporterSettings : ReporterSettings +{ + public bool SummaryOnly { get; init; } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/ExtensionMethods.cs b/src/ModVerify/Reporting/Reporters/ExtensionMethods.cs new file mode 100644 index 0000000..480e956 --- /dev/null +++ b/src/ModVerify/Reporting/Reporters/ExtensionMethods.cs @@ -0,0 +1,44 @@ +using System; + +namespace AET.ModVerify.Reporting.Reporters; + +public static class ExtensionMethods +{ + extension(IVerificationReporter) + { + public static IVerificationReporter CreateJson(IServiceProvider serviceProvider) + { + return IVerificationReporter.CreateJson(new JsonReporterSettings(), serviceProvider); + } + + public static IVerificationReporter CreateJson(JsonReporterSettings settings, IServiceProvider serviceProvider) + { + return new JsonReporter(settings, serviceProvider); + } + + public static IVerificationReporter CreateText(IServiceProvider serviceProvider) + { + return IVerificationReporter.CreateText(new TextFileReporterSettings(), serviceProvider); + } + + public static IVerificationReporter CreateText(TextFileReporterSettings settings, IServiceProvider serviceProvider) + { + return new TextFileReporter(settings, serviceProvider); + } + + public static IVerificationReporter CreateConsole(IServiceProvider serviceProvider, bool summaryOnly = false) + { + var settings = new ConsoleReporterSettings + { + MinimumReportSeverity = VerificationSeverity.Error, + SummaryOnly = summaryOnly + }; + return IVerificationReporter.CreateConsole(settings, serviceProvider); + } + + public static IVerificationReporter CreateConsole(ConsoleReporterSettings settings, IServiceProvider serviceProvider) + { + return new ConsoleReporter(settings, serviceProvider); + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/FileBasedReporter.cs b/src/ModVerify/Reporting/Reporters/FileBasedReporter.cs index 1057bfa..4c60121 100644 --- a/src/ModVerify/Reporting/Reporters/FileBasedReporter.cs +++ b/src/ModVerify/Reporting/Reporters/FileBasedReporter.cs @@ -1,7 +1,6 @@ using System; using System.IO; using System.IO.Abstractions; -using AET.ModVerify.Reporting.Settings; using Microsoft.Extensions.DependencyInjection; namespace AET.ModVerify.Reporting.Reporters; diff --git a/src/ModVerify/Reporting/Settings/FileBasedReporterSettings.cs b/src/ModVerify/Reporting/Reporters/FileBasedReporterSettings.cs similarity index 85% rename from src/ModVerify/Reporting/Settings/FileBasedReporterSettings.cs rename to src/ModVerify/Reporting/Reporters/FileBasedReporterSettings.cs index 759a6ab..aa468fc 100644 --- a/src/ModVerify/Reporting/Settings/FileBasedReporterSettings.cs +++ b/src/ModVerify/Reporting/Reporters/FileBasedReporterSettings.cs @@ -1,6 +1,6 @@ using System; -namespace AET.ModVerify.Reporting.Settings; +namespace AET.ModVerify.Reporting.Reporters; public record FileBasedReporterSettings : ReporterSettings { diff --git a/src/ModVerify/Reporting/Reporters/IVerificationReporter.cs b/src/ModVerify/Reporting/Reporters/IVerificationReporter.cs new file mode 100644 index 0000000..5ce7a65 --- /dev/null +++ b/src/ModVerify/Reporting/Reporters/IVerificationReporter.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace AET.ModVerify.Reporting.Reporters; + +public interface IVerificationReporter +{ + public Task ReportAsync(VerificationResult verificationResult); +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs b/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs index 348fbb1..ac11bb6 100644 --- a/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs +++ b/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs @@ -4,21 +4,92 @@ using System.Text.Json; using System.Threading.Tasks; using AET.ModVerify.Reporting.Json; +using AET.ModVerify.Verifiers; +using AnakinRaW.CommonUtilities.FileSystem.Validation; -namespace AET.ModVerify.Reporting.Reporters.JSON; +namespace AET.ModVerify.Reporting.Reporters; internal class JsonReporter(JsonReporterSettings settings, IServiceProvider serviceProvider) : FileBasedReporter(settings, serviceProvider) { - public const string FileName = "VerificationResult.json"; - - public override async Task ReportAsync(IReadOnlyCollection errors) + public override async Task ReportAsync(VerificationResult verificationResult) { - var report = new JsonVerificationReport(errors.Select(x => new JsonVerificationError(x))); + var report = CreateJsonReport(verificationResult); + var fileName = CreateFileName(verificationResult); + #if NET || NETSTANDARD2_1 await #endif - using var fs = CreateFile(FileName); + using var fs = CreateFile(fileName); await JsonSerializer.SerializeAsync(fs, report, ModVerifyJsonSettings.JsonSettings); } + + private JsonVerificationReport CreateJsonReport(VerificationResult result) + { + IEnumerable errors; + if (Settings.AggregateResults) + { + errors = result.Errors + .OrderByDescending(x => x.Severity) + .ThenBy(x => x.Id) + .GroupBy(x => new GroupKey(x.Asset, x.Id, x.VerifierChain)) + .Select, JsonVerificationErrorBase>(g => + { + var first = g.First(); + var contexts = g.Select(x => x.ContextEntries).ToList(); + if (contexts.Count == 1) + return new JsonVerificationError(first, Settings.Verbose); + return new JsonAggregatedVerificationError(first, contexts, Settings.Verbose); + }); + } + else + { + errors = result.Errors + .OrderByDescending(x => x.Severity) + .ThenBy(x => x.Id) + .Select(x => new JsonVerificationError(x, Settings.Verbose)); + } + + return new JsonVerificationReport + { + Metadata = new JsonVerificationReportMetadata + { + Target = new JsonVerificationTarget(result.Target), + Duration = result.Duration.ToString("g"), + Status = result.Status, + Verifiers = result.Verifiers.Select(x => x.Name).ToList() + }, + Errors = errors + }; + } + + private static string CreateFileName(VerificationResult result) + { + var fileName = $"VerificationResult_{result.Target.Name}.json"; + if (CurrentSystemFileNameValidator.Instance.IsValidFileName(fileName) is FileNameValidationResult.Valid) + return fileName; + // I don't think there is a safe/secure way to re-encode the file name, if it's not valid using the plain target name. + // Thus, we simply use the current date and accept the fact that files may get overwritten for different targets. + return $"VerificationResult_{DateTime.Now:yyyy_mm_dd}.json"; + + } + + private readonly record struct GroupKey(string Asset, string Id, IReadOnlyList VerifierChain) + { + public bool Equals(GroupKey other) + { + return Asset == other.Asset + && Id == other.Id + && VerifierChainEqualityComparer.Instance.Equals(VerifierChain, other.VerifierChain); + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + hashCode.Add(Asset); + hashCode.Add(Id); + hashCode.Add(VerifierChain, VerifierChainEqualityComparer.Instance); + return hashCode.ToHashCode(); + } + } } \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/JSON/JsonReporterSettings.cs b/src/ModVerify/Reporting/Reporters/JSON/JsonReporterSettings.cs index 4207b36..a2503ed 100644 --- a/src/ModVerify/Reporting/Reporters/JSON/JsonReporterSettings.cs +++ b/src/ModVerify/Reporting/Reporters/JSON/JsonReporterSettings.cs @@ -1,5 +1,5 @@ -using AET.ModVerify.Reporting.Settings; - -namespace AET.ModVerify.Reporting.Reporters.JSON; - -public record JsonReporterSettings : FileBasedReporterSettings; \ No newline at end of file +namespace AET.ModVerify.Reporting.Reporters; +public record JsonReporterSettings : FileBasedReporterSettings +{ + public bool AggregateResults { get; init; } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/ReporterBase.cs b/src/ModVerify/Reporting/Reporters/ReporterBase.cs index df444e3..47cafc5 100644 --- a/src/ModVerify/Reporting/Reporters/ReporterBase.cs +++ b/src/ModVerify/Reporting/Reporters/ReporterBase.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using AET.ModVerify.Reporting.Settings; namespace AET.ModVerify.Reporting.Reporters; @@ -12,7 +11,7 @@ public abstract class ReporterBase(T settings, IServiceProvider serviceProvid protected T Settings { get; } = settings ?? throw new ArgumentNullException(nameof(settings)); - public abstract Task ReportAsync(IReadOnlyCollection errors); + public abstract Task ReportAsync(VerificationResult verificationResult); protected IEnumerable FilteredErrors(IReadOnlyCollection errors) { diff --git a/src/ModVerify/Reporting/Settings/ReporterSettings.cs b/src/ModVerify/Reporting/Reporters/ReporterSettings.cs similarity index 61% rename from src/ModVerify/Reporting/Settings/ReporterSettings.cs rename to src/ModVerify/Reporting/Reporters/ReporterSettings.cs index ef33857..6cd56d3 100644 --- a/src/ModVerify/Reporting/Settings/ReporterSettings.cs +++ b/src/ModVerify/Reporting/Reporters/ReporterSettings.cs @@ -1,6 +1,7 @@ -namespace AET.ModVerify.Reporting.Settings; +namespace AET.ModVerify.Reporting.Reporters; public record ReporterSettings { public VerificationSeverity MinimumReportSeverity { get; init; } = VerificationSeverity.Information; + public bool Verbose { get; init; } } \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/Text/TextFileReporter.cs b/src/ModVerify/Reporting/Reporters/Text/TextFileReporter.cs index cfc5c91..f3e8126 100644 --- a/src/ModVerify/Reporting/Reporters/Text/TextFileReporter.cs +++ b/src/ModVerify/Reporting/Reporters/Text/TextFileReporter.cs @@ -1,49 +1,56 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using System.Threading.Tasks; -namespace AET.ModVerify.Reporting.Reporters.Text; +namespace AET.ModVerify.Reporting.Reporters; -internal class TextFileReporter(TextFileReporterSettings settings, IServiceProvider serviceProvider) : FileBasedReporter(settings, serviceProvider) +internal sealed class TextFileReporter(TextFileReporterSettings settings, IServiceProvider serviceProvider) + : FileBasedReporter(settings, serviceProvider) { internal const string SingleReportFileName = "VerificationResult.txt"; - public override async Task ReportAsync(IReadOnlyCollection errors) + public override async Task ReportAsync(VerificationResult verificationResult) { if (Settings.SplitIntoFiles) - await ReportByVerifier(errors); + await ReportByVerifier(verificationResult); else - await ReportWhole(errors); + await ReportWhole(verificationResult); } - private async Task ReportWhole(IReadOnlyCollection errors) + private async Task ReportWhole(VerificationResult result) { #if NET || NETSTANDARD2_1 await #endif using var streamWriter = new StreamWriter(CreateFile(SingleReportFileName)); - foreach (var error in errors.OrderBy(x => x.Id)) + await WriteHeader(result.Target, DateTime.Now, null, streamWriter); + + foreach (var error in result.Errors.OrderBy(x => x.Id)) await WriteError(error, streamWriter); } - private async Task ReportByVerifier(IReadOnlyCollection errors) + private async Task ReportByVerifier(VerificationResult result) { - var grouped = errors.GroupBy(x => x.VerifierChain.Last()); + var time = DateTime.Now; + var grouped = result.Errors.GroupBy(x => x.VerifierChain.Last().Name); foreach (var group in grouped) - await ReportToSingleFile(group); + await ReportToSingleFile(group, result.Target, time); } - private async Task ReportToSingleFile(IGrouping group) + private async Task ReportToSingleFile(IGrouping group, VerificationTarget target, DateTime time) { - var fileName = $"{GetVerifierName(group.Key)}Results.txt"; + var fileName = $"{GetVerifierName(group.Key)}_Results.txt"; #if NET || NETSTANDARD2_1 await #endif using var streamWriter = new StreamWriter(CreateFile(fileName)); + + await WriteHeader(target, time, group.Key, streamWriter); + foreach (var error in group.OrderBy(x => x.Id)) await WriteError(error, streamWriter); } @@ -52,23 +59,41 @@ private static string GetVerifierName(string verifierTypeName) { var typeNameSpan = verifierTypeName.AsSpan(); var nameIndex = typeNameSpan.LastIndexOf('.'); + var isSupType = typeNameSpan.IndexOf('/') > -1; - if (nameIndex == -1) + if (nameIndex == -1 && !isSupType) return verifierTypeName; - // They type name must not be empty - if (typeNameSpan.Length == nameIndex) - throw new InvalidOperationException(); - var name = typeNameSpan.Slice(nameIndex + 1); // Normalize subtypes (such as C/M) to avoid creating directories - if (name.IndexOf('/') != -1) + if (isSupType) return name.ToString().Replace('/', '.'); return name.ToString(); } + private static async Task WriteHeader( + VerificationTarget target, + DateTime time, + string? verifier, + StreamWriter writer) + { + var header = CreateHeader(target, time, verifier); + await writer.WriteLineAsync(header); + await writer.WriteLineAsync(); + } + + private static string CreateHeader(VerificationTarget target, DateTime time, string? verifier) + { + var sb = new StringBuilder(); + sb.Append($"# Target '{target.Name}'"); + if (!string.IsNullOrEmpty(verifier)) + sb.Append($"; Verifier: {verifier}"); + sb.Append($"; Time: {time:s}"); + return sb.ToString(); + } + private static async Task WriteError(VerificationError error, StreamWriter writer) { await writer.WriteLineAsync($"[{error.Id}] {error.Message}"); diff --git a/src/ModVerify/Reporting/Reporters/Text/TextFileReporterSettings.cs b/src/ModVerify/Reporting/Reporters/Text/TextFileReporterSettings.cs index 8fb833b..2d0e348 100644 --- a/src/ModVerify/Reporting/Reporters/Text/TextFileReporterSettings.cs +++ b/src/ModVerify/Reporting/Reporters/Text/TextFileReporterSettings.cs @@ -1,8 +1,6 @@ -using AET.ModVerify.Reporting.Settings; +namespace AET.ModVerify.Reporting.Reporters; -namespace AET.ModVerify.Reporting.Reporters.Text; - -public record TextFileReporterSettings : FileBasedReporterSettings +public sealed record TextFileReporterSettings : FileBasedReporterSettings { public bool SplitIntoFiles { get; init; } = true; } \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/VerificationReportBroker.cs b/src/ModVerify/Reporting/Reporters/VerificationReportBroker.cs new file mode 100644 index 0000000..0ea6630 --- /dev/null +++ b/src/ModVerify/Reporting/Reporters/VerificationReportBroker.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace AET.ModVerify.Reporting.Reporters; + +public sealed class VerificationReportBroker : IVerificationReporter +{ + private readonly ILogger? _logger; + private readonly IReadOnlyCollection _reporters; + + public VerificationReportBroker( + IReadOnlyCollection reporters, + IServiceProvider serviceProvider) + { + _reporters = reporters ?? throw new ArgumentNullException(nameof(reporters)); + _logger = serviceProvider.GetService()?.CreateLogger(typeof(VerificationReportBroker)); + } + + public async Task ReportAsync(VerificationResult result) + { + foreach (var reporter in _reporters) + { + try + { + await reporter.ReportAsync(result); + } + catch (Exception e) + { + _logger?.LogError(e, "Exception while reporting verification error"); + } + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs b/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs deleted file mode 100644 index b8906ac..0000000 --- a/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs +++ /dev/null @@ -1,52 +0,0 @@ -using AET.ModVerify.Reporting.Reporters.JSON; -using AET.ModVerify.Reporting.Reporters.Text; -using AET.ModVerify.Reporting.Settings; -using Microsoft.Extensions.DependencyInjection; - -namespace AET.ModVerify.Reporting.Reporters; - -public static class VerificationReportersExtensions -{ - extension(IServiceCollection serviceCollection) - { - public IServiceCollection RegisterJsonReporter() - { - return serviceCollection.RegisterJsonReporter(new JsonReporterSettings - { - OutputDirectory = "." - }); - } - - public IServiceCollection RegisterTextFileReporter() - { - return serviceCollection.RegisterTextFileReporter(new TextFileReporterSettings - { - OutputDirectory = "." - }); - } - - public IServiceCollection RegisterConsoleReporter(bool summaryOnly = false) - { - return serviceCollection.RegisterConsoleReporter(new ReporterSettings - { - MinimumReportSeverity = VerificationSeverity.Error - }, summaryOnly); - } - - public IServiceCollection RegisterJsonReporter(JsonReporterSettings settings) - { - return serviceCollection.AddSingleton(sp => new JsonReporter(settings, sp)); - } - - public IServiceCollection RegisterTextFileReporter(TextFileReporterSettings settings) - { - return serviceCollection.AddSingleton(sp => new TextFileReporter(settings, sp)); - } - - public IServiceCollection RegisterConsoleReporter(ReporterSettings settings, - bool summaryOnly = false) - { - return serviceCollection.AddSingleton(sp => new ConsoleReporter(settings, summaryOnly, sp)); - } - } -} \ No newline at end of file diff --git a/src/ModVerify/Reporting/RestoredVerifierInfo.cs b/src/ModVerify/Reporting/RestoredVerifierInfo.cs new file mode 100644 index 0000000..0f0a87b --- /dev/null +++ b/src/ModVerify/Reporting/RestoredVerifierInfo.cs @@ -0,0 +1,15 @@ +using AET.ModVerify.Verifiers; +using AET.ModVerify.Verifiers.Utilities; +using System.Collections.Generic; + +namespace AET.ModVerify.Reporting; + +internal sealed class RestoredVerifierInfo : IGameVerifierInfo +{ + public IGameVerifierInfo? Parent { get; init; } + + public IReadOnlyList VerifierChain => field ??= this.GetVerifierChain(); + + public required string Name { get; init; } + public string FriendlyName => Name; +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Settings/GlobalVerifyReportSettings.cs b/src/ModVerify/Reporting/Settings/GlobalVerifyReportSettings.cs deleted file mode 100644 index 5a51436..0000000 --- a/src/ModVerify/Reporting/Settings/GlobalVerifyReportSettings.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace AET.ModVerify.Reporting.Settings; - -//public record GlobalVerifyReportSettings -//{ -// //public VerificationSeverity MinimumReportSeverity { get; init; } = VerificationSeverity.Information; - -// public VerificationBaseline Baseline { get; init; } = VerificationBaseline.Empty; - -// public SuppressionList Suppressions { get; init; } = SuppressionList.Empty; -//} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonSuppressionFilter.cs b/src/ModVerify/Reporting/Suppressions/Json/JsonSuppressionFilter.cs similarity index 91% rename from src/ModVerify/Reporting/Json/JsonSuppressionFilter.cs rename to src/ModVerify/Reporting/Suppressions/Json/JsonSuppressionFilter.cs index 6d232c7..13f8335 100644 --- a/src/ModVerify/Reporting/Json/JsonSuppressionFilter.cs +++ b/src/ModVerify/Reporting/Suppressions/Json/JsonSuppressionFilter.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace AET.ModVerify.Reporting.Json; +namespace AET.ModVerify.Reporting.Suppressions.Json; internal class JsonSuppressionFilter(SuppressionFilter filter) { diff --git a/src/ModVerify/Reporting/Json/JsonSuppressionList.cs b/src/ModVerify/Reporting/Suppressions/Json/JsonSuppressionList.cs similarity index 88% rename from src/ModVerify/Reporting/Json/JsonSuppressionList.cs rename to src/ModVerify/Reporting/Suppressions/Json/JsonSuppressionList.cs index 1a8cb4a..54569eb 100644 --- a/src/ModVerify/Reporting/Json/JsonSuppressionList.cs +++ b/src/ModVerify/Reporting/Suppressions/Json/JsonSuppressionList.cs @@ -2,7 +2,7 @@ using System.Linq; using System.Text.Json.Serialization; -namespace AET.ModVerify.Reporting.Json; +namespace AET.ModVerify.Reporting.Suppressions.Json; internal class JsonSuppressionList { diff --git a/src/ModVerify/Reporting/SuppressionFilter.cs b/src/ModVerify/Reporting/Suppressions/SuppressionFilter.cs similarity index 93% rename from src/ModVerify/Reporting/SuppressionFilter.cs rename to src/ModVerify/Reporting/Suppressions/SuppressionFilter.cs index efac588..35a21d7 100644 --- a/src/ModVerify/Reporting/SuppressionFilter.cs +++ b/src/ModVerify/Reporting/Suppressions/SuppressionFilter.cs @@ -1,8 +1,8 @@ using System; using System.Linq; -using AET.ModVerify.Reporting.Json; +using AET.ModVerify.Reporting.Suppressions.Json; -namespace AET.ModVerify.Reporting; +namespace AET.ModVerify.Reporting.Suppressions; public sealed class SuppressionFilter : IEquatable { @@ -42,7 +42,7 @@ public bool Suppresses(VerificationError error) if (Verifier is not null) { - if (error.VerifierChain.Contains(Verifier)) + if (error.VerifierChain.Any(x => x.Name.Equals(Verifier))) suppresses = true; else return false; diff --git a/src/ModVerify/Reporting/SuppressionList.cs b/src/ModVerify/Reporting/Suppressions/SuppressionList.cs similarity index 96% rename from src/ModVerify/Reporting/SuppressionList.cs rename to src/ModVerify/Reporting/Suppressions/SuppressionList.cs index 12ebab4..a5fc8f9 100644 --- a/src/ModVerify/Reporting/SuppressionList.cs +++ b/src/ModVerify/Reporting/Suppressions/SuppressionList.cs @@ -5,8 +5,9 @@ using System.IO; using System.Linq; using System.Text.Json; +using AET.ModVerify.Reporting.Suppressions.Json; -namespace AET.ModVerify.Reporting; +namespace AET.ModVerify.Reporting.Suppressions; public sealed class SuppressionList : IReadOnlyCollection { diff --git a/src/ModVerify/Reporting/VerificationCompletionStatus.cs b/src/ModVerify/Reporting/VerificationCompletionStatus.cs new file mode 100644 index 0000000..94fde18 --- /dev/null +++ b/src/ModVerify/Reporting/VerificationCompletionStatus.cs @@ -0,0 +1,8 @@ +namespace AET.ModVerify.Reporting; + +public enum VerificationCompletionStatus +{ + Completed, + CompletedFailFast, + Cancelled, +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/VerificationError.cs b/src/ModVerify/Reporting/VerificationError.cs index 538a8fd..2e95327 100644 --- a/src/ModVerify/Reporting/VerificationError.cs +++ b/src/ModVerify/Reporting/VerificationError.cs @@ -17,7 +17,7 @@ public sealed class VerificationError : IEquatable public string Message { get; } - public IReadOnlyList VerifierChain { get; } + public IReadOnlyList VerifierChain { get; } public IReadOnlyCollection ContextEntries { get; } @@ -28,11 +28,13 @@ public sealed class VerificationError : IEquatable public VerificationError( string id, string message, - IReadOnlyList verifiers, + IGameVerifierInfo verifier, IEnumerable contextEntries, string asset, VerificationSeverity severity) { + if (verifier == null) + throw new ArgumentNullException(nameof(verifier)); if (contextEntries == null) throw new ArgumentNullException(nameof(contextEntries)); if (asset is null) @@ -41,10 +43,9 @@ public VerificationError( Id = id; Message = message ?? throw new ArgumentNullException(nameof(message)); - VerifierChain = verifiers; + VerifierChain = verifier.VerifierChain; Severity = severity; - _contextEntries = [.. contextEntries]; - ContextEntries = _contextEntries.ToList(); + ContextEntries = _contextEntries = [.. contextEntries]; Asset = asset; } @@ -52,25 +53,25 @@ internal VerificationError(JsonVerificationError error) { Id = error.Id; Message = error.Message; - VerifierChain = error.VerifierChain; + VerifierChain = RestoreVerifierChain(error.VerifierChain); _contextEntries = [..error.ContextEntries]; ContextEntries = _contextEntries.ToList(); Asset = error.Asset; } public static VerificationError Create( - IReadOnlyList verifiers, + IGameVerifierInfo verifier, string id, string message, VerificationSeverity severity, IEnumerable context, string asset) { - return new VerificationError(id, message, verifiers.Select(x => x.Name).ToList(), context, asset, severity); + return new VerificationError(id, message, verifier, context, asset, severity); } public static VerificationError Create( - IReadOnlyList verifiers, + IGameVerifierInfo verifier, string id, string message, VerificationSeverity severity, @@ -79,7 +80,7 @@ public static VerificationError Create( return new VerificationError( id, message, - verifiers.Select(x => x.Name).ToList(), + verifier, [], asset, severity); @@ -120,4 +121,26 @@ public override string ToString() return $"[{Severity}] [{string.Join(" --> ", VerifierChain)}] " + $"{Id}: Message={Message}; Asset='{Asset}'; Context=[{string.Join(",", ContextEntries)}];"; } + + private static IReadOnlyList RestoreVerifierChain(IReadOnlyList? errorVerifierChain) + { + if (errorVerifierChain is null) + return []; + + var verifierChain = new List(); + IGameVerifierInfo? previousVerifier = null; + + foreach (var name in errorVerifierChain) + { + var verifier = new RestoredVerifierInfo + { + Name = name, + Parent = previousVerifier + }; + verifierChain.Add(verifier); + previousVerifier = verifier; + } + + return verifierChain; + } } \ No newline at end of file diff --git a/src/ModVerify/Reporting/VerificationReportBroker.cs b/src/ModVerify/Reporting/VerificationReportBroker.cs deleted file mode 100644 index 5c1e8a3..0000000 --- a/src/ModVerify/Reporting/VerificationReportBroker.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace AET.ModVerify.Reporting; - -public sealed class VerificationReportBroker(IServiceProvider serviceProvider) -{ - private readonly ILogger? _logger = serviceProvider.GetService()?.CreateLogger(typeof(VerificationReportBroker)); - - public async Task ReportAsync(IReadOnlyCollection errors) - { - var reporters = serviceProvider.GetServices(); - - foreach (var reporter in reporters) - { - try - { - await reporter.ReportAsync(errors); - } - catch (Exception e) - { - _logger?.LogError(e, "Exception while reporting verification error"); - } - } - } -} \ No newline at end of file diff --git a/src/ModVerify/Reporting/VerificationResult.cs b/src/ModVerify/Reporting/VerificationResult.cs new file mode 100644 index 0000000..a4dbe1c --- /dev/null +++ b/src/ModVerify/Reporting/VerificationResult.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using AET.ModVerify.Reporting.Baseline; +using AET.ModVerify.Reporting.Suppressions; +using AET.ModVerify.Verifiers; + +namespace AET.ModVerify.Reporting; + +public sealed record VerificationResult +{ + public required VerificationCompletionStatus Status { get; init; } + + public required IReadOnlyCollection Errors + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } + + public required VerificationBaseline UsedBaseline + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } + + public required SuppressionList UsedSuppressions + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } + + public required IReadOnlyCollection Verifiers + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } + + public required VerificationTarget Target + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } + + public required TimeSpan Duration { get; init; } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/VerifierChainEqualityComparer.cs b/src/ModVerify/Reporting/VerifierChainEqualityComparer.cs new file mode 100644 index 0000000..27897cb --- /dev/null +++ b/src/ModVerify/Reporting/VerifierChainEqualityComparer.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using AET.ModVerify.Verifiers; +using AET.ModVerify.Verifiers.Utilities; + +namespace AET.ModVerify.Reporting; + +internal sealed class VerifierChainEqualityComparer : IEqualityComparer> +{ + public static readonly VerifierChainEqualityComparer Instance = new(); + + private VerifierChainEqualityComparer() + { + } + + public bool Equals(IReadOnlyList? x, IReadOnlyList? y) + { + if (ReferenceEquals(x, y)) + return true; + if (x is null || y is null) + return false; + if (x.Count != y.Count) + return false; + for (var i = 0; i < x.Count; i++) + { + if (!NameBasedEqualityComparer.Instance.Equals(x[i], y[i])) + return false; + } + return true; + } + + public int GetHashCode(IReadOnlyList? obj) + { + if (obj == null) + return 0; + var hashCode = new HashCode(); + foreach (var verifier in obj) + hashCode.Add(verifier.Name); + return hashCode.ToHashCode(); + } +} diff --git a/src/ModVerify/Resources/Schemas/2.1/baseline.json b/src/ModVerify/Resources/Schemas/2.2/baseline.json similarity index 92% rename from src/ModVerify/Resources/Schemas/2.1/baseline.json rename to src/ModVerify/Resources/Schemas/2.2/baseline.json index da37c4d..d893b4d 100644 --- a/src/ModVerify/Resources/Schemas/2.1/baseline.json +++ b/src/ModVerify/Resources/Schemas/2.2/baseline.json @@ -1,5 +1,5 @@ { - "$id": "https://AlamoEngine-Tools.github.io/schemas/mod-verify/2.1/baseline", + "$id": "https://AlamoEngine-Tools.github.io/schemas/mod-verify/2.2/baseline", "$schema": "https://json-schema.org/draft/2020-12/schema", "description": "Represents a baseline for AET ModVerify", "type": "object", @@ -73,12 +73,6 @@ "severity": { "$ref": "#/$defs/severity" }, - "verifiers": { - "type": "array", - "items": { - "type": "string" - } - }, "context": { "type": "array", "items": { @@ -91,7 +85,6 @@ "message", "asset", "severity", - "verifiers", "context" ], "additionalProperties": false @@ -99,7 +92,7 @@ }, "properties": { "version": { - "const": "2.1" + "const": "2.2" }, "minSeverity": { "$ref": "#/$defs/severity" diff --git a/src/ModVerify/Settings/VerifyPipelineSettings.cs b/src/ModVerify/Settings/VerifierServiceSettings.cs similarity index 72% rename from src/ModVerify/Settings/VerifyPipelineSettings.cs rename to src/ModVerify/Settings/VerifierServiceSettings.cs index 46fc997..762806d 100644 --- a/src/ModVerify/Settings/VerifyPipelineSettings.cs +++ b/src/ModVerify/Settings/VerifierServiceSettings.cs @@ -1,8 +1,6 @@ -using AET.ModVerify.Pipeline; +namespace AET.ModVerify.Settings; -namespace AET.ModVerify.Settings; - -public sealed class VerifyPipelineSettings +public sealed class VerifierServiceSettings { public required GameVerifySettings GameVerifySettings { get; init; } diff --git a/src/ModVerify/Utilities/VerificationErrorExtensions.cs b/src/ModVerify/Utilities/VerificationErrorExtensions.cs index e5aa9fe..f7eefbc 100644 --- a/src/ModVerify/Utilities/VerificationErrorExtensions.cs +++ b/src/ModVerify/Utilities/VerificationErrorExtensions.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using AET.ModVerify.Reporting; +using AET.ModVerify.Reporting.Baseline; +using AET.ModVerify.Reporting.Suppressions; namespace AET.ModVerify.Utilities; diff --git a/src/ModVerify/VerificationTarget.cs b/src/ModVerify/VerificationTarget.cs index c0d5b97..f12bc4d 100644 --- a/src/ModVerify/VerificationTarget.cs +++ b/src/ModVerify/VerificationTarget.cs @@ -1,10 +1,9 @@ using System; -using System.Text; using PG.StarWarsGame.Engine; namespace AET.ModVerify; -public sealed class VerificationTarget +public sealed record VerificationTarget { public required GameEngineType Engine { get; init; } @@ -28,14 +27,4 @@ public required GameLocations Location public string? Version { get; init; } public bool IsGame => Location.ModPaths.Count == 0; - - public override string ToString() - { - var sb = new StringBuilder($"[Name={Name};EngineType={Engine};"); - if (!string.IsNullOrEmpty(Version)) - sb.Append($"Version={Version};"); - sb.Append($"Location={Location};"); - sb.Append(']'); - return sb.ToString(); - } } \ No newline at end of file diff --git a/src/ModVerify/Verifiers/AlreadyVerifiedCache.cs b/src/ModVerify/Verifiers/AlreadyVerifiedCache.cs deleted file mode 100644 index 530ca4e..0000000 --- a/src/ModVerify/Verifiers/AlreadyVerifiedCache.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Concurrent; -using Microsoft.Extensions.DependencyInjection; -using PG.Commons.Hashing; -using PG.StarWarsGame.Engine; - -namespace AET.ModVerify.Verifiers; - -internal sealed class AlreadyVerifiedCache(IServiceProvider serviceProvider) : IAlreadyVerifiedCache -{ - private readonly ICrc32HashingService _crc32Hashing = serviceProvider.GetRequiredService(); - private readonly ConcurrentDictionary _cachedChecksums = new(); - - public bool TryAddEntry(string entry) - { - return TryAddEntry(entry.AsSpan()); - } - - public bool TryAddEntry(ReadOnlySpan entry) - { - return TryAddEntry(_crc32Hashing.GetCrc32Upper(entry, PGConstants.DefaultPGEncoding)); - } - - public bool TryAddEntry(Crc32 checksum) - { - return _cachedChecksums.TryAdd(checksum, 0); - } -} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/AudioFilesVerifier.cs b/src/ModVerify/Verifiers/AudioFilesVerifier.cs deleted file mode 100644 index ddd9252..0000000 --- a/src/ModVerify/Verifiers/AudioFilesVerifier.cs +++ /dev/null @@ -1,249 +0,0 @@ -using System; -using System.Buffers; -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions; -using System.Linq; -using System.Text; -using System.Threading; -using AET.ModVerify.Reporting; -using AET.ModVerify.Settings; -using AnakinRaW.CommonUtilities.FileSystem.Normalization; -using Microsoft.Extensions.DependencyInjection; -using PG.Commons.Hashing; -using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Engine.Audio.Sfx; -using PG.StarWarsGame.Engine.Localization; -#if NETSTANDARD2_0 -using AnakinRaW.CommonUtilities.FileSystem; -#endif - -namespace AET.ModVerify.Verifiers; - -public class AudioFilesVerifier : GameVerifier -{ - private static readonly PathNormalizeOptions SampleNormalizerOptions = new() - { - UnifyCase = UnifyCasingKind.UpperCaseForce, - UnifySeparatorKind = DirectorySeparatorKind.Windows, - UnifyDirectorySeparators = true - }; - - private readonly ICrc32HashingService _hashingService; - private readonly IFileSystem _fileSystem; - private readonly IGameLanguageManager _languageManager; - - public AudioFilesVerifier(IStarWarsGameEngine gameEngine, GameVerifySettings settings, IServiceProvider serviceProvider) - : base(null, gameEngine, settings, serviceProvider) - { - _hashingService = serviceProvider.GetRequiredService(); - _fileSystem = serviceProvider.GetRequiredService(); - _languageManager = serviceProvider.GetRequiredService() - .GetLanguageManager(Repository.EngineType); - } - - public override string FriendlyName => "Audio Files"; - - public override void Verify(CancellationToken token) - { - var visitedSamples = new HashSet(); - var languagesToVerify = GetLanguagesToVerify().ToList(); - - - var numSamples = GameEngine.SfxGameManager.Entries.Sum(x => x.AllSamples.Count()); - double counter = 0; - - foreach (var sfxEvent in GameEngine.SfxGameManager.Entries) - { - foreach (var codedSample in sfxEvent.AllSamples) - { - OnProgress(++counter / numSamples, $"Audio File - '{codedSample}'"); - VerifySample(codedSample.AsSpan(), sfxEvent, languagesToVerify, visitedSamples); - } - } - } - - private void VerifySample(ReadOnlySpan sample, SfxEvent sfxEvent, IEnumerable languagesToVerify, HashSet visitedSamples) - { - char[]? pooledBuffer = null; - - var buffer = sample.Length < PGConstants.MaxMegEntryPathLength - ? stackalloc char[PGConstants.MaxMegEntryPathLength] - : pooledBuffer = ArrayPool.Shared.Rent(sample.Length); - - try - { - var length = PathNormalizer.Normalize(sample, buffer, SampleNormalizerOptions); - var sampleNameBuffer = buffer.Slice(0, length); - - var crc = _hashingService.GetCrc32(sampleNameBuffer, Encoding.ASCII); - if (!visitedSamples.Add(crc)) - return; - - if (sfxEvent.IsLocalized) - { - foreach (var language in languagesToVerify) - { - VerifySampleLocalized(sfxEvent, sampleNameBuffer, language, out var localized); - if (!localized) - return; - } - } - else - { - VerifySample(sampleNameBuffer, sfxEvent); - } - } - finally - { - if (pooledBuffer is not null) - ArrayPool.Shared.Return(pooledBuffer); - } - - } - - private void VerifySampleLocalized(SfxEvent sfxEvent, ReadOnlySpan sample, LanguageType language, out bool localized) - { - char[]? pooledBuffer = null; - - var buffer = sample.Length < PGConstants.MaxMegEntryPathLength - ? stackalloc char[PGConstants.MaxMegEntryPathLength] - : pooledBuffer = ArrayPool.Shared.Rent(sample.Length); - try - { - var l = _languageManager.LocalizeFileName(sample, language, buffer, out localized); - var localizedName = buffer.Slice(0, l); - VerifySample(localizedName, sfxEvent); - } - finally - { - if (pooledBuffer is not null) - ArrayPool.Shared.Return(pooledBuffer); - } - } - - private void VerifySample(ReadOnlySpan sample, SfxEvent sfxEvent) - { - using var sampleStream = Repository.TryOpenFile(sample); - if (sampleStream is null) - { - var sampleString = sample.ToString(); - AddError(VerificationError.Create( - VerifierChain, - VerifierErrorCodes.FileNotFound, - $"Audio file '{sampleString}' could not be found.", - VerificationSeverity.Error, - [sfxEvent.Name], - sampleString)); - return; - } - using var binaryReader = new BinaryReader(sampleStream); - - // Skip Header + "fmt " - binaryReader.BaseStream.Seek(16, SeekOrigin.Begin); - - var fmtSize = binaryReader.ReadInt32(); - var format = (WaveFormats)binaryReader.ReadInt16(); - var channels = binaryReader.ReadInt16(); - - var sampleRate = binaryReader.ReadInt32(); - var bytesPerSecond = binaryReader.ReadInt32(); - - var frameSize = binaryReader.ReadInt16(); - var bitPerSecondPerChannel = binaryReader.ReadInt16(); - - if (format != WaveFormats.PCM) - { - var sampleString = sample.ToString(); - AddError(VerificationError.Create( - VerifierChain, - VerifierErrorCodes.SampleNotPCM, - $"Audio file '{sampleString}' has an invalid format '{format}'. Supported is {WaveFormats.PCM}", - VerificationSeverity.Error, - [sfxEvent.Name], - sampleString)); - } - - if (channels > 1 && !IsAmbient2D(sfxEvent)) - { - var sampleString = sample.ToString(); - AddError(VerificationError.Create( - VerifierChain, - VerifierErrorCodes.SampleNotMono, - $"Audio file '{sampleString}' is not mono audio.", - VerificationSeverity.Information, - sampleString)); - } - - if (sampleRate > 48_000) - { - var sampleString = sample.ToString(); - AddError(VerificationError.Create( - VerifierChain, - VerifierErrorCodes. InvalidSampleRate, - $"Audio file '{sampleString}' has a too high sample rate of {sampleRate}. Maximum is 48.000Hz.", - VerificationSeverity.Error, - [sfxEvent.Name], - sampleString)); - } - - if (bitPerSecondPerChannel > 16) - { - var sampleString = sample.ToString(); - AddError(VerificationError.Create( - VerifierChain, - VerifierErrorCodes.InvalidBitsPerSeconds, - $"Audio file '{sampleString}' has an invalid bit size of {bitPerSecondPerChannel}. Supported are 16bit.", - VerificationSeverity.Error, - [sfxEvent.Name], - sampleString)); - } - } - - // Some heuristics whether a SFXEvent is most likely to be an ambient sound. - private bool IsAmbient2D(SfxEvent sfxEvent) - { - if (!sfxEvent.Is2D) - return false; - - if (sfxEvent.IsPreset) - return false; - - // If the event is located in SFXEventsAmbient.xml we simply assume it's an ambient sound. - var fileName = _fileSystem.Path.GetFileName(sfxEvent.Location.XmlFile.AsSpan()); - if (fileName.Equals("SFXEventsAmbient.xml".AsSpan(), StringComparison.OrdinalIgnoreCase)) - return true; - - if (string.IsNullOrEmpty(sfxEvent.UsePresetName)) - return false; - - if (sfxEvent.UsePresetName!.StartsWith("Preset_AMB_2D")) - return true; - - return true; - } - - private IEnumerable GetLanguagesToVerify() - { - switch (Settings.LocalizationOption) - { - case VerifyLocalizationOption.English: - return new List { LanguageType.English }; - case VerifyLocalizationOption.CurrentSystem: - return new List { _languageManager.GetLanguagesFromUser() }; - case VerifyLocalizationOption.AllInstalled: - return GameEngine.InstalledLanguages; - case VerifyLocalizationOption.All: - return _languageManager.SupportedLanguages; - default: - throw new NotSupportedException($"{Settings.LocalizationOption} is not supported"); - } - } - - private enum WaveFormats - { - PCM = 1, - MSADPCM = 2, - IEEE_Float = 3, - } -} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/Caching/AlreadyVerifiedCache.cs b/src/ModVerify/Verifiers/Caching/AlreadyVerifiedCache.cs new file mode 100644 index 0000000..c6dda7c --- /dev/null +++ b/src/ModVerify/Verifiers/Caching/AlreadyVerifiedCache.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace AET.ModVerify.Verifiers.Caching; + +internal sealed class AlreadyVerifiedCache : IAlreadyVerifiedCache +{ + private readonly Dictionary _cachedEntries = new(); + + public bool TryAddEntry(string entry, bool assetExists) + { + var upper = entry.ToUpperInvariant(); + +#if NETSTANDARD2_1 || NET + return _cachedEntries.TryAdd(upper, assetExists); +#else + var alreadyVerified = _cachedEntries.ContainsKey(upper); + if (alreadyVerified) + return false; + + _cachedEntries[upper] = assetExists; + return true; +#endif + } + + public VerifiedCacheEntry GetEntry(string entry) + { + var upper = entry.ToUpperInvariant(); + var alreadyVerified = _cachedEntries.TryGetValue(upper, out var exists); + return alreadyVerified ? new VerifiedCacheEntry(true, exists) : default; + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/Caching/IAlreadyVerifiedCache.cs b/src/ModVerify/Verifiers/Caching/IAlreadyVerifiedCache.cs new file mode 100644 index 0000000..68f39b4 --- /dev/null +++ b/src/ModVerify/Verifiers/Caching/IAlreadyVerifiedCache.cs @@ -0,0 +1,8 @@ +namespace AET.ModVerify.Verifiers.Caching; + +public interface IAlreadyVerifiedCache +{ + bool TryAddEntry(string entry, bool assetExists); + + VerifiedCacheEntry GetEntry(string entry); +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/Caching/VerifiedCacheEntry.cs b/src/ModVerify/Verifiers/Caching/VerifiedCacheEntry.cs new file mode 100644 index 0000000..252a7c0 --- /dev/null +++ b/src/ModVerify/Verifiers/Caching/VerifiedCacheEntry.cs @@ -0,0 +1,14 @@ +namespace AET.ModVerify.Verifiers.Caching; + +public readonly struct VerifiedCacheEntry +{ + public bool AlreadyVerified { get; } + + public bool AssetExists { get; } + + public VerifiedCacheEntry(bool alreadyVerified, bool assetExists) + { + AlreadyVerified = alreadyVerified; + AssetExists = assetExists; + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.Base.cs b/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.Base.cs deleted file mode 100644 index 3373f8e..0000000 --- a/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.Base.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using AET.ModVerify.Reporting; -using AET.ModVerify.Settings; -using AnakinRaW.CommonUtilities.Collections; -using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Engine.CommandBar; -using PG.StarWarsGame.Engine.CommandBar.Components; - -namespace AET.ModVerify.Verifiers; - -public partial class CommandBarVerifier(IStarWarsGameEngine gameEngine, GameVerifySettings settings, IServiceProvider serviceProvider) - : GameVerifier(null, gameEngine, settings, serviceProvider) -{ - public const string CommandBarNoShellsGroup = "CMDBAR00"; - public const string CommandBarManyShellsGroup = "CMDBAR01"; - public const string CommandBarNoShellsComponentInShellGroup = "CMDBAR02"; - public const string CommandBarDuplicateComponent = "CMDBAR03"; - public const string CommandBarUnsupportedComponent = "CMDBAR04"; - public const string CommandBarShellNoModel = "CMDBAR05"; - - public override string FriendlyName => "CommandBar"; - - public override void Verify(CancellationToken token) - { - VerifyCommandBarShellsGroups(); - VerifyCommandBarComponents(); - } - - private void VerifySingleComponent(CommandBarBaseComponent component) - { - VerifyCommandBarModel(component); - VerifyComponentBone(component); - } -} - -partial class CommandBarVerifier -{ - private void VerifyCommandBarModel(CommandBarBaseComponent component) - { - if (component is not CommandBarShellComponent shellComponent) - return; - - if (shellComponent.ModelPath is null) - { - AddError(VerificationError.Create(VerifierChain, - CommandBarShellNoModel, $"The CommandBarShellComponent '{component.Name}' has no model specified.", - VerificationSeverity.Error, shellComponent.Name)); - return; - } - - var model = GameEngine.PGRender.LoadModelAndAnimations(shellComponent.ModelPath.AsSpan(), null); - if (model is null) - { - AddError(VerificationError.Create(VerifierChain, - CommandBarShellNoModel, $"Could not find model '{shellComponent.ModelPath}' for CommandBarShellComponent '{component.Name}'.", - VerificationSeverity.Error, [shellComponent.Name], shellComponent.ModelPath)); - return; - } - } - - private void VerifyComponentBone(CommandBarBaseComponent component) - { - if (component is CommandBarShellComponent) - return; - - if (component.Bone == -1) - { - AddError(VerificationError.Create(VerifierChain, - CommandBarShellNoModel, $"The CommandBar component '{component.Name}' is not connected to a shell component.", - VerificationSeverity.Warning, component.Name)); - } - } -} - -partial class CommandBarVerifier -{ - private void VerifyCommandBarComponents() - { - var occupiedComponentIds = SupportedCommandBarComponentData.GetComponentIdsForEngine(Repository.EngineType).Keys - .ToDictionary(value => value, _ => false); - - foreach (var component in GameEngine.CommandBar.Components) - { - if (!occupiedComponentIds.TryGetValue(component.Id, out var alreadyOccupied)) - { - AddError(VerificationError.Create( - VerifierChain, - CommandBarUnsupportedComponent, - $"The CommandBar component '{component.Name}' is not supported by the game.", - VerificationSeverity.Information, - component.Name)); - } - else - { - occupiedComponentIds[component.Id] = true; - } - - if (alreadyOccupied) - { - AddError(VerificationError.Create(VerifierChain, - CommandBarDuplicateComponent, - $"The CommandBar component '{component.Name}' with ID '{component.Id}' already exists.", - VerificationSeverity.Warning, - component.Name)); - } - - VerifySingleComponent(component); - } - } -} - -partial class CommandBarVerifier -{ - private void VerifyCommandBarShellsGroups() - { - var shellGroups = new FrugalList(); - foreach (var groupPair in GameEngine.CommandBar.Groups) - { - if (groupPair.Key == CommandBarConstants.ShellGroupName) - { - shellGroups.Add(groupPair.Key); - VerifyShellGroup(groupPair.Value); - } - else if (groupPair.Key.Equals(CommandBarConstants.ShellGroupName, StringComparison.OrdinalIgnoreCase)) - { - shellGroups.Add(groupPair.Key); - } - } - - if (shellGroups.Count == 0) - AddError(VerificationError.Create(VerifierChain, - CommandBarNoShellsGroup, - $"No CommandBarGroup '{CommandBarConstants.ShellGroupName}' found.", - VerificationSeverity.Error, - "GameCommandBar")); - - if (shellGroups.Count > 1) - AddError(VerificationError.Create(VerifierChain, - CommandBarManyShellsGroup, - $"Found more than one Shells CommandBarGroup. Mind that group names are case-sensitive. Correct name is '{CommandBarConstants.ShellGroupName}'", - VerificationSeverity.Warning, - shellGroups, "GameCommandBar")); - } - - private void VerifyShellGroup(CommandBarComponentGroup shellGroup) - { - foreach (var component in shellGroup.Components) - { - var shellComponent = component as CommandBarShellComponent; - if (shellComponent?.Type is not CommandBarComponentType.Shell) - { - AddError(VerificationError.Create(VerifierChain, - CommandBarNoShellsComponentInShellGroup, - $"The CommandBar component '{component.Name}' is not a shell component, but part of the '{CommandBarConstants.ShellGroupName}' group.", - VerificationSeverity.Warning, component.Name)); - } - } - } -} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.Components.cs b/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.Components.cs new file mode 100644 index 0000000..3b43093 --- /dev/null +++ b/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.Components.cs @@ -0,0 +1,51 @@ +using AET.ModVerify.Reporting; +using PG.StarWarsGame.Engine.CommandBar; +using System.Linq; +using System.Threading; + +namespace AET.ModVerify.Verifiers.CommandBar; + +partial class CommandBarVerifier +{ + private void VerifyCommandBarComponents(CancellationToken token, double startProgress) + { + var occupiedComponentIds = SupportedCommandBarComponentData + .GetComponentIdsForEngine(Repository.EngineType).Keys + .ToDictionary(value => value, _ => false); + + var counter = 0; + var numEntities = GameEngine.CommandBar.Components.Count; + var num = 1 - startProgress; + + foreach (var component in GameEngine.CommandBar.Components) + { + var progress = num + (++counter / (double)numEntities) * startProgress; + OnProgress(progress, $"CommandBarComponent - '{component.Name}'"); + + if (!occupiedComponentIds.TryGetValue(component.Id, out var alreadyOccupied)) + { + AddError(VerificationError.Create( + this, + CommandBarUnsupportedComponent, + $"The CommandBar component '{component.Name}' is not supported by the game.", + VerificationSeverity.Information, + component.Name)); + } + else + { + occupiedComponentIds[component.Id] = true; + } + + if (alreadyOccupied) + { + AddError(VerificationError.Create(this, + VerifierErrorCodes.Duplicate, + $"The CommandBar component '{component.Name}' with ID '{component.Id}' already exists.", + VerificationSeverity.Warning, + component.Name)); + } + + VerifySingleComponent(component, token); + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.Groups.cs b/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.Groups.cs new file mode 100644 index 0000000..e3ed107 --- /dev/null +++ b/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.Groups.cs @@ -0,0 +1,56 @@ +using System; +using AET.ModVerify.Reporting; +using AnakinRaW.CommonUtilities.Collections; +using PG.StarWarsGame.Engine.CommandBar; +using PG.StarWarsGame.Engine.CommandBar.Components; + +namespace AET.ModVerify.Verifiers.CommandBar; + +partial class CommandBarVerifier +{ + private void VerifyCommandBarShellsGroups() + { + var shellGroups = new FrugalList(); + foreach (var groupPair in GameEngine.CommandBar.Groups) + { + if (groupPair.Key == CommandBarConstants.ShellGroupName) + { + shellGroups.Add(groupPair.Key); + VerifyShellGroup(groupPair.Value); + } + else if (groupPair.Key.Equals(CommandBarConstants.ShellGroupName, StringComparison.OrdinalIgnoreCase)) + { + shellGroups.Add(groupPair.Key); + } + } + + if (shellGroups.Count == 0) + AddError(VerificationError.Create(this, + CommandBarNoShellsGroup, + $"No CommandBarGroup '{CommandBarConstants.ShellGroupName}' found.", + VerificationSeverity.Error, + "GameCommandBar")); + + if (shellGroups.Count > 1) + AddError(VerificationError.Create(this, + CommandBarManyShellsGroup, + $"Found more than one Shells CommandBarGroup. Mind that group names are case-sensitive. Correct name is '{CommandBarConstants.ShellGroupName}'", + VerificationSeverity.Warning, + shellGroups, "GameCommandBar")); + } + + private void VerifyShellGroup(CommandBarComponentGroup shellGroup) + { + foreach (var component in shellGroup.Components) + { + var shellComponent = component as CommandBarShellComponent; + if (shellComponent?.Type is not CommandBarComponentType.Shell) + { + AddError(VerificationError.Create(this, + CommandBarNoShellsComponentInShellGroup, + $"The CommandBar component '{component.Name}' is not a shell component, but part of the '{CommandBarConstants.ShellGroupName}' group.", + VerificationSeverity.Warning, component.Name)); + } + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.MegaTexture.cs b/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.MegaTexture.cs new file mode 100644 index 0000000..8d86bad --- /dev/null +++ b/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.MegaTexture.cs @@ -0,0 +1,41 @@ +using System.Threading; +using AET.ModVerify.Reporting; +using AET.ModVerify.Verifiers.Commons; +using AET.ModVerify.Verifiers.Utilities; +using PG.StarWarsGame.Engine.CommandBar; + +namespace AET.ModVerify.Verifiers.CommandBar; + +partial class CommandBarVerifier +{ + private void VerifyMegaTexture(CancellationToken token) + { + if (CommandBar.MtdFile is null) + { + AddError(VerificationError.Create(this, VerifierErrorCodes.FileNotFound, + $"Cannot find CommandBar MegaTextureDirectory '{CommandBarConstants.MegaTextureBaseName}.mtd'", + VerificationSeverity.Critical, $"{CommandBarConstants.MegaTextureBaseName}.mtd")); + } + else + { + var dupVerifier = new DuplicateVerifier(this); + dupVerifier.Verify(IDuplicateVerificationContext.CreateForMtd(CommandBar.MtdFile), [], token); + + foreach (var duplicateError in dupVerifier.VerifyErrors) + AddError(duplicateError); + } + + if (CommandBar.MegaTextureFileName is null) + { + AddError(VerificationError.Create(this, VerifierErrorCodes.FileNotFound, + $"Cannot find CommandBar MegaTexture '{CommandBarConstants.MegaTextureBaseName}.tga'", + VerificationSeverity.Critical, $"{CommandBarConstants.MegaTextureBaseName}.tga")); + } + else if (!GameEngine.GameRepository.TextureRepository.FileExists(CommandBar.MegaTextureFileName)) + { + AddError(VerificationError.Create(this, VerifierErrorCodes.FileNotFound, + $"Cannot find CommandBar MegaTexture '{CommandBarConstants.MegaTextureBaseName}.tga'", + VerificationSeverity.Critical, $"{CommandBarConstants.MegaTextureBaseName}.tga")); + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.SingleComponent.cs b/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.SingleComponent.cs new file mode 100644 index 0000000..4a0d951 --- /dev/null +++ b/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.SingleComponent.cs @@ -0,0 +1,74 @@ +using AET.ModVerify.Reporting; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Engine.CommandBar.Components; +using System; +using System.Threading; + +namespace AET.ModVerify.Verifiers.CommandBar; + +partial class CommandBarVerifier +{ + private void VerifySingleComponent(CommandBarBaseComponent component, CancellationToken token) + { + VerifyName(component); + VerifyCommandBarModel(component, token); + VerifyComponentBone(component); + + // TODO: Textures + } + + private void VerifyName(CommandBarBaseComponent component) + { + if (component.Name.Length > PGConstants.MaxCommandBarComponentNameBuffer) + { + AddError(VerificationError.Create(this, VerifierErrorCodes.NameTooLong, + // Deliberately not reporting the buffer length as max, as it's considered to be internal data + $"The CommandBarShellComponent name '{component.Name}' is too long. Maximum length is {PGConstants.MaxCommandBarComponentName}.", + VerificationSeverity.Critical, [], component.Name)); + } + } + + private void VerifyCommandBarModel(CommandBarBaseComponent component, CancellationToken token) + { + if (component is not CommandBarShellComponent shellComponent) + return; + + if (shellComponent.ModelPath is null) + { + AddError(VerificationError.Create(this, + CommandBarShellNoModel, $"The CommandBarShellComponent '{component.Name}' has no model specified.", + VerificationSeverity.Error, [shellComponent.Name], shellComponent.Name)); + return; + } + + using var model = GameEngine.PGRender.LoadModelAndAnimations(shellComponent.ModelPath.AsSpan(), null); + if (model is null) + { + AddError(VerificationError.Create(this, + CommandBarShellNoModel, $"Could not find model '{shellComponent.ModelPath}' for CommandBarShellComponent '{component.Name}'.", + VerificationSeverity.Error, [shellComponent.Name], shellComponent.ModelPath)); + return; + } + + _modelVerifier.VerifyModelOrParticle(model.File, [shellComponent.Name], token); + + if (model.Animations.Cout == 0) + return; + + // TODO: Verify Animations + + } + + private void VerifyComponentBone(CommandBarBaseComponent component) + { + if (component is CommandBarShellComponent) + return; + + if (component.Bone == -1) + { + AddError(VerificationError.Create(this, + CommandBarShellNoModel, $"The CommandBar component '{component.Name}' is not connected to a shell component.", + VerificationSeverity.Warning, component.Name)); + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.cs b/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.cs new file mode 100644 index 0000000..4894673 --- /dev/null +++ b/src/ModVerify/Verifiers/CommandBar/CommandBarVerifier.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq; +using System.Threading; +using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers.Commons; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Engine.CommandBar; + +namespace AET.ModVerify.Verifiers.CommandBar; + +public partial class CommandBarVerifier : GameVerifier +{ + public const string CommandBarNoShellsGroup = "CMDBAR00"; + public const string CommandBarManyShellsGroup = "CMDBAR01"; + public const string CommandBarNoShellsComponentInShellGroup = "CMDBAR02"; + public const string CommandBarUnsupportedComponent = "CMDBAR03"; + public const string CommandBarShellNoModel = "CMDBAR04"; + + private readonly SingleModelVerifier _modelVerifier; + private readonly TextureVerifier _textureVerifier; + + public override string FriendlyName => "CommandBar"; + + public ICommandBarGameManager CommandBar { get; } + + public CommandBarVerifier(IStarWarsGameEngine gameEngine, GameVerifySettings settings, IServiceProvider serviceProvider) + : base(gameEngine, settings, serviceProvider) + { + CommandBar = gameEngine.CommandBar; + _modelVerifier = new SingleModelVerifier(this); + _textureVerifier = new TextureVerifier(this); + } + + public override void Verify(CancellationToken token) + { + var progress = 0.0d; + OnProgress(progress, "Verifying MegaTexture"); + VerifyMegaTexture(token); + progress = 1 / 3.0; + OnProgress(progress, "Verifying CommandBar Shell"); + VerifyCommandBarShellsGroups(); + progress = 2 / 3.0; + OnProgress(progress, "Verifying CommandBar components"); + VerifyCommandBarComponents(token, progress); + + foreach (var subError in _modelVerifier.VerifyErrors.Concat(_textureVerifier.VerifyErrors)) + AddError(subError); + + progress = 1.0; + OnProgress(progress, null); + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/Commons/Audio/AudioFileInfo.cs b/src/ModVerify/Verifiers/Commons/Audio/AudioFileInfo.cs new file mode 100644 index 0000000..ee428cb --- /dev/null +++ b/src/ModVerify/Verifiers/Commons/Audio/AudioFileInfo.cs @@ -0,0 +1,15 @@ +namespace AET.ModVerify.Verifiers.Commons; + +public class AudioFileInfo +{ + public string SampleName { get; } + public AudioFileType ExpectedType { get; } + public bool IsAmbient { get; } + + public AudioFileInfo(string sampleName, AudioFileType expectedType, bool isAmbient) + { + SampleName = sampleName; + ExpectedType = expectedType; + IsAmbient = isAmbient; + } +} diff --git a/src/ModVerify/Verifiers/Commons/Audio/AudioFileType.cs b/src/ModVerify/Verifiers/Commons/Audio/AudioFileType.cs new file mode 100644 index 0000000..05ea0bc --- /dev/null +++ b/src/ModVerify/Verifiers/Commons/Audio/AudioFileType.cs @@ -0,0 +1,7 @@ +namespace AET.ModVerify.Verifiers.Commons; + +public enum AudioFileType +{ + Wav, + Mp3 +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/Commons/AudioFileVerifier.cs b/src/ModVerify/Verifiers/Commons/AudioFileVerifier.cs new file mode 100644 index 0000000..4537a64 --- /dev/null +++ b/src/ModVerify/Verifiers/Commons/AudioFileVerifier.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.IO; +using AET.ModVerify.Reporting; +using AET.ModVerify.Settings; +using PG.StarWarsGame.Engine; +using System.Threading; +using Microsoft.Extensions.DependencyInjection; +using AET.ModVerify.Verifiers.Caching; + +namespace AET.ModVerify.Verifiers.Commons; + +public class AudioFileVerifier : GameVerifier +{ + private readonly IAlreadyVerifiedCache? _alreadyVerifiedCache; + + public AudioFileVerifier(GameVerifierBase parent) : base(parent) + { + _alreadyVerifiedCache = Services.GetService(); + } + + public AudioFileVerifier(IGameVerifierInfo? parent, + IStarWarsGameEngine gameEngine, + GameVerifySettings settings, + IServiceProvider serviceProvider) : base(parent, gameEngine, settings, serviceProvider) + { + _alreadyVerifiedCache = serviceProvider.GetService(); + } + + public override string FriendlyName => "Audio File format"; + + public override void Verify(AudioFileInfo sampleInfo, IReadOnlyCollection contextInfo, CancellationToken token) + { + var cached = _alreadyVerifiedCache?.GetEntry(sampleInfo.SampleName); + + if (cached?.AlreadyVerified is true) + { + if (!cached.Value.AssetExists) + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.FileNotFound, + $"Audio file '{sampleInfo.SampleName}' could not be found.", + VerificationSeverity.Error, + [.. contextInfo], + sampleInfo.SampleName)); + } + return; + } + + + var sampleString = sampleInfo.SampleName; + + using var sampleStream = Repository.TryOpenFile(sampleString.AsSpan()); + + _alreadyVerifiedCache?.TryAddEntry(sampleInfo.SampleName, sampleStream is not null); + + if (sampleStream is null) + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.FileNotFound, + $"Audio file '{sampleString}' could not be found.", + VerificationSeverity.Error, + [..contextInfo], + sampleString)); + return; + } + + if (sampleInfo.ExpectedType == AudioFileType.Mp3) + { + // TODO: MP3 support to be implemented + return; + } + + using var binaryReader = new BinaryReader(sampleStream); + + // Skip Header + "fmt " + binaryReader.BaseStream.Seek(16, SeekOrigin.Begin); + + var fmtSize = binaryReader.ReadInt32(); + var format = (WaveFormats)binaryReader.ReadInt16(); + var channels = binaryReader.ReadInt16(); + + var sampleRate = binaryReader.ReadInt32(); + var bytesPerSecond = binaryReader.ReadInt32(); + + var frameSize = binaryReader.ReadInt16(); + var bitPerSecondPerChannel = binaryReader.ReadInt16(); + + if (format != WaveFormats.PCM) + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.SampleNotPCM, + $"Audio file '{sampleString}' has an invalid format '{format}'. Supported is {WaveFormats.PCM}", + VerificationSeverity.Error, + [..contextInfo], + sampleString)); + } + + if (channels > 1 && !sampleInfo.IsAmbient) + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.SampleNotMono, + $"Audio file '{sampleString}' is not mono audio.", + VerificationSeverity.Information, + sampleString)); + } + + if (sampleRate > 48_000) + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.InvalidSampleRate, + $"Audio file '{sampleString}' has a too high sample rate of {sampleRate}. Maximum is 48.000Hz.", + VerificationSeverity.Error, + [..contextInfo], + sampleString)); + } + + if (bitPerSecondPerChannel > 16) + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.InvalidBitsPerSeconds, + $"Audio file '{sampleString}' has an invalid bit size of {bitPerSecondPerChannel}. Supported are 16bit.", + VerificationSeverity.Error, + [..contextInfo], + sampleString)); + } + } + + private enum WaveFormats + { + PCM = 1, + MSADPCM = 2, + IEEE_Float = 3, + } +} diff --git a/src/ModVerify/Verifiers/Commons/DuplicateVerifier.cs b/src/ModVerify/Verifiers/Commons/DuplicateVerifier.cs new file mode 100644 index 0000000..7aa19e6 --- /dev/null +++ b/src/ModVerify/Verifiers/Commons/DuplicateVerifier.cs @@ -0,0 +1,43 @@ +using AET.ModVerify.Reporting; +using AET.ModVerify.Settings; +using PG.StarWarsGame.Engine; +using System; +using System.Collections.Generic; +using System.Threading; + +namespace AET.ModVerify.Verifiers.Commons; + +public sealed class DuplicateVerifier : GameVerifier +{ + public override string FriendlyName => "Duplicate Verifier"; + + public DuplicateVerifier(GameVerifierBase parent) : base(parent) + { + } + + public DuplicateVerifier( + IGameVerifierInfo? parent, + IStarWarsGameEngine gameEngine, + GameVerifySettings settings, + IServiceProvider serviceProvider) + : base(parent, gameEngine, settings, serviceProvider) + { + } + + public override void Verify(IDuplicateVerificationContext toVerify, IReadOnlyCollection contextInfo, CancellationToken token) + { + foreach (var crc32 in toVerify.GetCrcs()) + { + if (toVerify.HasDuplicates(crc32, out var entryNames, out var context, out var errorMessage)) + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.Duplicate, + errorMessage, + VerificationSeverity.Error, + context, + entryNames)); + } + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/Commons/Duplicates/IDuplicateVerificationContext.cs b/src/ModVerify/Verifiers/Commons/Duplicates/IDuplicateVerificationContext.cs new file mode 100644 index 0000000..4301e54 --- /dev/null +++ b/src/ModVerify/Verifiers/Commons/Duplicates/IDuplicateVerificationContext.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using PG.Commons.Hashing; + +namespace AET.ModVerify.Verifiers.Commons; + +public interface IDuplicateVerificationContext +{ + string SourceName { get; } + IEnumerable GetCrcs(); + bool HasDuplicates(Crc32 crc, out string entryNames, out IEnumerable duplicateContext, out string errorMessage); +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/Commons/Duplicates/MtdDuplicateVerificationContext.cs b/src/ModVerify/Verifiers/Commons/Duplicates/MtdDuplicateVerificationContext.cs new file mode 100644 index 0000000..624a0a6 --- /dev/null +++ b/src/ModVerify/Verifiers/Commons/Duplicates/MtdDuplicateVerificationContext.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq; +using PG.Commons.Hashing; +using PG.StarWarsGame.Files.MTD.Files; + +namespace AET.ModVerify.Verifiers.Commons; + +internal sealed class MtdDuplicateVerificationContext(IMtdFile mtdFile) : IDuplicateVerificationContext +{ + public string SourceName => mtdFile.FileName; + + public IEnumerable GetCrcs() => mtdFile.Content.Select(x => x.Crc32); + + public bool HasDuplicates(Crc32 crc, out string entryNames, out IEnumerable duplicateContext, out string errorMessage) + { + var entries = mtdFile.Content.EntriesWithCrc(crc); + if (entries.Count > 1) + { + var firstEntry = entries.First(); + entryNames = firstEntry.FileName; + duplicateContext = entries.Select(x => $"'{x.FileName}' (CRC: {x.Crc32})"); + errorMessage = $"MTD File '{SourceName}' has duplicate definitions for CRC ({firstEntry}): " + + $"{string.Join(",", entries.Select(x => x.FileName))}"; + return true; + } + + entryNames = string.Empty; + duplicateContext = []; + errorMessage = string.Empty; + return false; + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/Commons/Duplicates/NamedXmlObjectDuplicateVerificationContext.cs b/src/ModVerify/Verifiers/Commons/Duplicates/NamedXmlObjectDuplicateVerificationContext.cs new file mode 100644 index 0000000..d50057d --- /dev/null +++ b/src/ModVerify/Verifiers/Commons/Duplicates/NamedXmlObjectDuplicateVerificationContext.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using PG.Commons.Hashing; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Files.XML.Data; + +namespace AET.ModVerify.Verifiers.Commons; + +internal sealed class NamedXmlObjectDuplicateVerificationContext(string databaseName, IGameManager gameManager) + : IDuplicateVerificationContext + where T : NamedXmlObject +{ + public string SourceName => databaseName; + + public IEnumerable GetCrcs() => gameManager.EntryKeys; + + public bool HasDuplicates(Crc32 crc, out string entryNames, out IEnumerable duplicateContext, out string errorMessage) + { + var entries = gameManager.GetEntries(crc); + if (entries.Count > 1) + { + var firstEntry = entries.First(); + entryNames = firstEntry.Name; + duplicateContext = entries.Select(x => $"'{x.Name}' - {x.Location}"); + var message = $"{SourceName} '{firstEntry.Name}' ({firstEntry.Crc32}) has duplicate definitions: "; + message = entries.Aggregate(message, (current, entry) => current + $"['{entry.Name}' in {entry.Location.XmlFile}:{entry.Location.Line}] "); + errorMessage = message.TrimEnd(); + return true; + } + + entryNames = string.Empty; + duplicateContext = Array.Empty(); + errorMessage = string.Empty; + return false; + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/Commons/ModelVerifier.cs b/src/ModVerify/Verifiers/Commons/SingleModelVerifier.cs similarity index 80% rename from src/ModVerify/Verifiers/Commons/ModelVerifier.cs rename to src/ModVerify/Verifiers/Commons/SingleModelVerifier.cs index 25704d1..fa621e1 100644 --- a/src/ModVerify/Verifiers/Commons/ModelVerifier.cs +++ b/src/ModVerify/Verifiers/Commons/SingleModelVerifier.cs @@ -4,6 +4,7 @@ using AET.ModVerify.Reporting; using AET.ModVerify.Settings; using AET.ModVerify.Utilities; +using AET.ModVerify.Verifiers.Caching; using Microsoft.Extensions.DependencyInjection; using PG.StarWarsGame.Engine; using PG.StarWarsGame.Files; @@ -22,47 +23,93 @@ public sealed class SingleModelVerifier : GameVerifier { private const string ProxyAltIdentifier = "_ALT"; - private readonly TextureVeifier _textureVerifier; + private readonly TextureVerifier _textureVerifier; private readonly IAlreadyVerifiedCache? _cache; - public SingleModelVerifier(IGameVerifierInfo? parent, + public SingleModelVerifier(GameVerifierBase parent) : base(parent) + { + _textureVerifier = new TextureVerifier(this); + _cache = Services.GetService(); + } + + public SingleModelVerifier( + IGameVerifierInfo? parent, IStarWarsGameEngine engine, GameVerifySettings settings, IServiceProvider serviceProvider) : base(parent, engine, settings, serviceProvider) { - _textureVerifier = new TextureVeifier(this, engine, settings, serviceProvider); + _textureVerifier = new TextureVerifier(this); _cache = serviceProvider.GetService(); } public override void Verify(string modelName, IReadOnlyCollection contextInfo, CancellationToken token) { - try + var cacheEntry = _cache?.GetEntry(modelName); + if (cacheEntry?.AlreadyVerified is true) { - _textureVerifier.Error += OnTextureError; - - var modelPath = BuildModelPath(modelName); - VerifyAlamoFile(modelPath, contextInfo, token); + if (!cacheEntry.Value.AssetExists) + { + var error = VerificationError.Create( + this, + VerifierErrorCodes.FileNotFound, + $"Unable to find .ALO file '{modelName}'", + VerificationSeverity.Error, + contextInfo, + modelName); + AddError(error); + } + return; } - finally + + var modelPath = BuildModelPath(modelName); + VerifyAlamoFile(modelPath, contextInfo, token, out var modelExists); + + if (!modelExists) { - _textureVerifier.Error -= OnTextureError; + var error = VerificationError.Create( + this, + VerifierErrorCodes.FileNotFound, + $"Unable to find .ALO file '{modelName}'", + VerificationSeverity.Error, + contextInfo, + modelName); + AddError(error); } + + _cache?.TryAddEntry(modelName, modelExists); + + foreach (var textureError in _textureVerifier.VerifyErrors) + AddError(textureError); } - private void OnTextureError(object sender, VerificationErrorEventArgs e) + public void VerifyModelOrParticle( + IAloFile aloFile, + IReadOnlyCollection contextInfo, + CancellationToken token) { - AddError(e.Error); + switch (aloFile) + { + case IAloModelFile model: + VerifyModel(model, contextInfo, token); + break; + case IAloParticleFile particle: + VerifyParticle(particle, contextInfo); + break; + default: + throw new InvalidOperationException("The data stream is neither a model nor particle."); + } } - private void VerifyAlamoFile(string modelPath, IReadOnlyCollection contextInfo, CancellationToken token) + private void VerifyAlamoFile( + string modelPath, + IReadOnlyCollection contextInfo, + CancellationToken token, + out bool modelExists) { token.ThrowIfCancellationRequested(); - var modelName = FileSystem.Path.GetFileName(modelPath.AsSpan()); + modelExists = true; - if (_cache?.TryAddEntry(modelName) == false) - return; - IAloFile? aloFile = null; try { @@ -74,22 +121,16 @@ private void VerifyAlamoFile(string modelPath, IReadOnlyCollection conte { var aloFilePath = FileSystem.Path.GetGameStrippedPath(Repository.Path.AsSpan(), modelPath.AsSpan()).ToString(); var message = $"'{aloFilePath}' is corrupted: {e.Message}"; - AddError(VerificationError.Create(VerifierChain, VerifierErrorCodes.FileCorrupt, message, + AddError(VerificationError.Create(this, VerifierErrorCodes.BinaryFileCorrupt, message, VerificationSeverity.Critical, contextInfo, aloFilePath)); return; } + // Because throwsException is true, we know that if aloFile is null, + // the file does not exist if (aloFile is null) { - var modelNameString = modelName.ToString(); - var error = VerificationError.Create( - VerifierChain, - VerifierErrorCodes.FileNotFound, - $"Unable to find .ALO file '{modelNameString}'", - VerificationSeverity.Error, - contextInfo, - modelNameString); - AddError(error); + modelExists = false; return; } @@ -101,24 +142,6 @@ private void VerifyAlamoFile(string modelPath, IReadOnlyCollection conte } } - private void VerifyModelOrParticle( - IAloFile aloFile, - IReadOnlyCollection contextInfo, - CancellationToken token) - { - switch (aloFile) - { - case IAloModelFile model: - VerifyModel(model, contextInfo, token); - break; - case IAloParticleFile particle: - VerifyParticle(particle, contextInfo); - break; - default: - throw new InvalidOperationException("The data stream is neither a model nor particle."); - } - } - private void VerifyParticle(IAloParticleFile file, IReadOnlyCollection contextInfo) { foreach (var texture in file.Content.Textures) @@ -129,7 +152,7 @@ private void VerifyParticle(IAloParticleFile file, IReadOnlyCollection c { var particlePath = FileSystem.Path.GetGameStrippedPath(Repository.Path.AsSpan(), file.FilePath.AsSpan()).ToString(); AddError(VerificationError.Create( - VerifierChain, + this, VerifierErrorCodes.InvalidFilePath, $"Invalid texture file name '{texture}' in particle '{particlePath}'", VerificationSeverity.Error, @@ -144,7 +167,7 @@ private void VerifyParticle(IAloParticleFile file, IReadOnlyCollection c { var particlePath = FileSystem.Path.GetGameStrippedPath(Repository.Path.AsSpan(), file.FilePath.AsSpan()).ToString(); AddError(VerificationError.Create( - VerifierChain, + this, VerifierErrorCodes.InvalidParticleName, $"The particle name '{file.Content.Name}' does not match file name '{particlePath}'", VerificationSeverity.Error, @@ -163,7 +186,7 @@ private void VerifyModel(IAloModelFile file, IReadOnlyCollection context { var modelFilePath = FileSystem.Path.GetGameStrippedPath(Repository.Path.AsSpan(), file.FilePath.AsSpan()).ToString(); AddError(VerificationError.Create( - VerifierChain, + this, VerifierErrorCodes.InvalidFilePath, $"Invalid texture file name '{texture}' in model '{modelFilePath}'", VerificationSeverity.Error, @@ -181,7 +204,7 @@ private void VerifyModel(IAloModelFile file, IReadOnlyCollection context var modelFilePath = FileSystem.Path.GetGameStrippedPath(Repository.Path.AsSpan(), file.FilePath.AsSpan()).ToString(); AddError(VerificationError.Create( - VerifierChain, + this, VerifierErrorCodes.InvalidFilePath, $"Invalid shader file name '{shader}' in model '{modelFilePath}'", VerificationSeverity.Error, @@ -200,7 +223,7 @@ private void VerifyModel(IAloModelFile file, IReadOnlyCollection context var modelFilePath = FileSystem.Path .GetGameStrippedPath(Repository.Path.AsSpan(), file.FilePath.AsSpan()).ToString(); AddError(VerificationError.Create( - VerifierChain, + this, VerifierErrorCodes.InvalidFilePath, $"Invalid proxy file name '{proxy}' for model '{modelFilePath}'", VerificationSeverity.Error, @@ -223,22 +246,21 @@ private void VerifyProxyExists(IPetroglyphFileHolder model, string proxy, IReadO var proxyPath = BuildModelPath(proxyName); var modelFilePath = FileSystem.Path.GetGameStrippedPath(Repository.Path.AsSpan(), model.FilePath.AsSpan()).ToString(); + + VerifyAlamoFile(proxyPath, [..contextInfo, modelFilePath], token, out var proxyExists); - if (!Repository.ModelRepository.FileExists(proxyPath)) + if (!proxyExists) { var message = $"Proxy particle '{proxyName}' not found for model '{modelFilePath}'"; var error = VerificationError.Create( - VerifierChain, + this, VerifierErrorCodes.FileNotFound, - message, - VerificationSeverity.Error, - [..contextInfo, modelFilePath], + message, + VerificationSeverity.Error, + [.. contextInfo, modelFilePath], proxyName); AddError(error); - return; } - - VerifyAlamoFile(proxyPath, [..contextInfo, modelFilePath], token); } private void VerifyShaderExists(IPetroglyphFileHolder model, string shader, IReadOnlyCollection contextInfo) @@ -251,7 +273,7 @@ private void VerifyShaderExists(IPetroglyphFileHolder model, string shader, IRea var modelFilePath = FileSystem.Path.GetGameStrippedPath(Repository.Path.AsSpan(), model.FilePath.AsSpan()).ToString(); var message = $"Shader effect '{shader}' not found for model '{modelFilePath}'."; var error = VerificationError.Create( - VerifierChain, + this, VerifierErrorCodes.FileNotFound, message, VerificationSeverity.Error, diff --git a/src/ModVerify/Verifiers/Commons/TextureVeifier.cs b/src/ModVerify/Verifiers/Commons/TextureVerifier.cs similarity index 68% rename from src/ModVerify/Verifiers/Commons/TextureVeifier.cs rename to src/ModVerify/Verifiers/Commons/TextureVerifier.cs index 1bd3a08..e7709e3 100644 --- a/src/ModVerify/Verifiers/Commons/TextureVeifier.cs +++ b/src/ModVerify/Verifiers/Commons/TextureVerifier.cs @@ -4,19 +4,24 @@ using System.Threading; using AET.ModVerify.Reporting; using AET.ModVerify.Settings; -using Microsoft.Extensions.DependencyInjection; using PG.StarWarsGame.Engine; namespace AET.ModVerify.Verifiers.Commons; -public sealed class TextureVeifier( - IGameVerifierInfo? parent, - IStarWarsGameEngine gameEngine, - GameVerifySettings settings, - IServiceProvider serviceProvider) - : GameVerifier(parent, gameEngine, settings, serviceProvider) +public sealed class TextureVerifier : GameVerifier { - private readonly IAlreadyVerifiedCache? _cache = serviceProvider.GetService(); + public TextureVerifier(GameVerifierBase parent) : base(parent) + { + } + + public TextureVerifier( + IGameVerifierInfo? parent, + IStarWarsGameEngine gameEngine, + GameVerifySettings settings, + IServiceProvider serviceProvider) : + base(parent, gameEngine, settings, serviceProvider) + { + } public override void Verify(string texturePath, IReadOnlyCollection contextInfo, CancellationToken token) { @@ -27,10 +32,6 @@ public void Verify(ReadOnlySpan textureName, IReadOnlyCollection c { token.ThrowIfCancellationRequested(); - - if (_cache?.TryAddEntry(textureName) == false) - return; - if (Repository.TextureRepository.FileExists(textureName, false, out var tooLongPath)) return; @@ -38,7 +39,7 @@ public void Verify(ReadOnlySpan textureName, IReadOnlyCollection c if (tooLongPath) { - AddError(VerificationError.Create(VerifierChain, VerifierErrorCodes.FilePathTooLong, + AddError(VerificationError.Create(this, VerifierErrorCodes.FilePathTooLong, $"Could not find texture '{pathString}' because the engine resolved a path that is too long.", VerificationSeverity.Error, contextInfo, pathString)); return; @@ -55,7 +56,7 @@ public void Verify(ReadOnlySpan textureName, IReadOnlyCollection c messageBuilder.Append('.'); - AddError(VerificationError.Create(VerifierChain, VerifierErrorCodes.FileNotFound, + AddError(VerificationError.Create(this, VerifierErrorCodes.FileNotFound, messageBuilder.ToString(), VerificationSeverity.Error, contextInfo, pathString)); } diff --git a/src/ModVerify/Verifiers/DuplicateNameFinder.cs b/src/ModVerify/Verifiers/DuplicateNameFinder.cs deleted file mode 100644 index 0b3b585..0000000 --- a/src/ModVerify/Verifiers/DuplicateNameFinder.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using AET.ModVerify.Reporting; -using AET.ModVerify.Settings; -using AnakinRaW.CommonUtilities.Collections; -using PG.Commons.Hashing; -using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Engine.Xml; -using PG.StarWarsGame.Files.MTD.Data; -using PG.StarWarsGame.Files.MTD.Files; - -namespace AET.ModVerify.Verifiers; - -public sealed class DuplicateNameFinder( - IStarWarsGameEngine gameEngine, - GameVerifySettings settings, - IServiceProvider serviceProvider) - : GameVerifier(null, gameEngine, settings, serviceProvider) -{ - public override string FriendlyName => "Duplicates"; - - public override void Verify(CancellationToken token) - { - CheckXmlObjectsForDuplicates("GameObject", GameEngine.GameObjectTypeManager); - CheckXmlObjectsForDuplicates("SFXEvent", GameEngine.SfxGameManager); - - if (GameEngine.GuiDialogManager.MtdFile is not null) - CheckMtdForDuplicates(GameEngine.GuiDialogManager.MtdFile); - - if (GameEngine.CommandBar.MegaTextureFile is not null) - { - if (!GameEngine.CommandBar.MegaTextureFile.FilePath.Equals(GameEngine.GuiDialogManager.MtdFile?.FileName)) - CheckMtdForDuplicates(GameEngine.CommandBar.MegaTextureFile); - } - } - - private void CheckForDuplicateCrcEntries( - string sourceName, - TSource source, - Func> crcSelector, - Func> entrySelector, - Func entryToStringSelector, - Func, IEnumerable> contextSelector, - Func, string, string> errorMessageCreator) - { - foreach (var crc32 in crcSelector(source)) - { - var entries = entrySelector(source, crc32); - if (entries.Count > 1) - { - var entryNames = entryToStringSelector(entries.First()); - var context = contextSelector(entries); - AddError(VerificationError.Create( - VerifierChain, - VerifierErrorCodes.DuplicateFound, - errorMessageCreator(entries, sourceName), - VerificationSeverity.Error, - context, - entryNames)); - } - } - } - - private void CheckMtdForDuplicates(IMtdFile mtdFile) - { - CheckForDuplicateCrcEntries( - mtdFile.FileName, - mtdFile, - mtd => mtd.Content.Select(x => x.Crc32), - (mtd, crc32) => mtd.Content.EntriesWithCrc(crc32), - entry => entry.FileName, - entries => entries.Select(x => $"'{x.FileName}' (CRC: {x.Crc32})"), - CreateDuplicateMtdErrorMessage); - } - - private void CheckXmlObjectsForDuplicates(string databaseName, IGameManager gameManager) where T : NamedXmlObject - { - CheckForDuplicateCrcEntries( - databaseName, - gameManager, - manager => manager.EntryKeys, - (manager, crc32) => manager.GetEntries(crc32), - entry => entry.Name, - entries => entries.Select(x => $"'{x.Name}' - {x.Location}"), - CreateDuplicateXmlErrorMessage); - } - - private static string CreateDuplicateMtdErrorMessage(ImmutableFrugalList entries, string fileName) - { - var firstEntry = entries.First(); - return $"MTD File '{fileName}' has duplicate definitions for CRC ({firstEntry}): {string.Join(",", entries.Select(x => x.FileName))}"; - } - - private static string CreateDuplicateXmlErrorMessage(ImmutableFrugalList entries, string databaseName) where T : NamedXmlObject - { - var firstEntry = entries.First(); - var message = $"{databaseName} '{firstEntry.Name}' ({firstEntry.Crc32}) has duplicate definitions: "; - foreach (var entry in entries) - message += $"['{entry.Name}' in {entry.Location.XmlFile}:{entry.Location.Line}] "; - return message.TrimEnd(); - } -} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/GameEngineErrorCollector.cs b/src/ModVerify/Verifiers/Engine/GameEngineErrorCollector.cs similarity index 84% rename from src/ModVerify/Verifiers/GameEngineErrorCollector.cs rename to src/ModVerify/Verifiers/Engine/GameEngineErrorCollector.cs index 6475ea7..70c1cb3 100644 --- a/src/ModVerify/Verifiers/GameEngineErrorCollector.cs +++ b/src/ModVerify/Verifiers/Engine/GameEngineErrorCollector.cs @@ -2,17 +2,18 @@ using System.Collections.Generic; using System.Threading; using AET.ModVerify.Reporting; -using AET.ModVerify.Reporting.Reporters.Engine; +using AET.ModVerify.Reporting.Engine; using AET.ModVerify.Settings; using PG.StarWarsGame.Engine; -namespace AET.ModVerify.Verifiers; +namespace AET.ModVerify.Verifiers.Engine; public sealed class GameEngineErrorCollector( IGameEngineErrorCollection errorCollection, IStarWarsGameEngine gameEngine, GameVerifySettings settings, - IServiceProvider serviceProvider) : GameVerifier(null, gameEngine, settings, serviceProvider) + IServiceProvider serviceProvider) + : GameVerifier(gameEngine, settings, serviceProvider) { public override string FriendlyName => "Game Engine Initialization"; diff --git a/src/ModVerify/Verifiers/Engine/HardcodedAssetsVerifier.cs b/src/ModVerify/Verifiers/Engine/HardcodedAssetsVerifier.cs new file mode 100644 index 0000000..69a6800 --- /dev/null +++ b/src/ModVerify/Verifiers/Engine/HardcodedAssetsVerifier.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading; +using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers.Commons; +using PG.StarWarsGame.Engine; + +namespace AET.ModVerify.Verifiers.Engine; + +public sealed class HardcodedAssetsVerifier : GameVerifier +{ + private readonly SingleModelVerifier _modelVerifier; + + public HardcodedAssetsVerifier(IStarWarsGameEngine gameEngine, GameVerifySettings settings, IServiceProvider serviceProvider) + : base(gameEngine, settings, serviceProvider) + { + _modelVerifier = new SingleModelVerifier(this); + } + + public override void Verify(CancellationToken token) + { + OnProgress(0.0d, "Verifying Hardcoded Models"); + VerifyModels(token); + OnProgress(1.0, null); + } + + private void VerifyModels(CancellationToken token) + { + var models = HardcodedEngineAssets.GetHardcodedModels(GameEngine.EngineType); + + foreach (var model in models) + _modelVerifier.Verify(model, [], token); + + foreach (var error in _modelVerifier.VerifyErrors) + AddError(error); + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.Icons.cs b/src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.Icons.cs new file mode 100644 index 0000000..36541bb --- /dev/null +++ b/src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.Icons.cs @@ -0,0 +1,41 @@ +using AET.ModVerify.Reporting; +using PG.StarWarsGame.Engine.GameObjects; + +namespace AET.ModVerify.Verifiers.GameObjects; + +public sealed partial class GameObjectTypeVerifier +{ + private void VerifyIcons(GameObject gameObject, string[] context) + { + VerifyObjectIcon(gameObject, context); + } + + private void VerifyObjectIcon(GameObject gameObject, string[] context) + { + if (string.IsNullOrEmpty(gameObject.IconName)) + return; + + /* + * The engine loads game object icons with different strategies, depending on where the icon is displayed: + * 1. the game loads the texture from MTD and supports the faction prefixes e.g, r_ or e_ + * Faction prefixes have higher priority than the non-prefix versions. The player's faction (not the object owner) is used. + * This applies to all command bar components (such as build buttons) + * 2. the game loads the texture form MTD and does NOT support faction prefix. + * If the texture is not found, the game searches the texture with forced .dds name in the Textures folder. + * This applies to the GUI dialogs (such as battle summary) + * 3. the game only loads the texture from MTD NOT supporting faction prefix nor textures folder fallback. + * This applies (only) to the neutralize hero dialog + * + * We only verify whether the icon exists in the MTD data, as this is the primary case to all strategies + * (and it's what really should only be used for mods) + * Faction-specific icons are not verified as they are statically not decidable. + */ + + if (!GameEngine.CommandBar.IconExists(gameObject)) + { + AddError(VerificationError.Create(this, VerifierErrorCodes.FileNotFound, + $"Could not find icon '{gameObject.IconName}' for game object type '{gameObject.Name}'.", + VerificationSeverity.Warning, context, gameObject.IconName!)); + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.Models.cs b/src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.Models.cs new file mode 100644 index 0000000..39699a7 --- /dev/null +++ b/src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.Models.cs @@ -0,0 +1,13 @@ +using PG.StarWarsGame.Engine.GameObjects; +using System.Threading; + +namespace AET.ModVerify.Verifiers.GameObjects; + +public sealed partial class GameObjectTypeVerifier +{ + private void VerifyModels(GameObject gameObject, string[] context, CancellationToken token) + { + foreach (var model in GameEngine.GameObjectTypeManager.GetModels(gameObject)) + _singleModelVerifier.Verify(model, context, token); + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.XRef.cs b/src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.XRef.cs new file mode 100644 index 0000000..1bca0e9 --- /dev/null +++ b/src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.XRef.cs @@ -0,0 +1,44 @@ +using System.Linq; +using AET.ModVerify.Reporting; +using PG.StarWarsGame.Engine.GameObjects; + +namespace AET.ModVerify.Verifiers.GameObjects; + +public sealed partial class GameObjectTypeVerifier +{ + private void VerifyXRefs(GameObject gameObject, string[] context) + { + if (!string.IsNullOrEmpty(gameObject.VariantOfExistingTypeName) && gameObject.VariantOfExistingType is null) + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.MissingXRef, + $"Missing base type '{gameObject.VariantOfExistingTypeName}' for GameObject '{gameObject.Name}'", + VerificationSeverity.Critical, + [..context, "VariantOfExistingType"], + gameObject.VariantOfExistingTypeName)); + } + + VerifyCompanyUnits(gameObject, context); + } + + private void VerifyCompanyUnits(GameObject gameObject, string[] context) + { + if (gameObject.GroundCompanyUnits.Count == 0) + return; + + var uniqueCompanyUnits = gameObject.GroundCompanyUnits + .Select(x => x.ToUpperInvariant()) + .Distinct(); + + foreach (var companyUnit in uniqueCompanyUnits) + { + if (GameEngine.GameObjectTypeManager.FindObjectType(companyUnit) is null) + { + AddError(VerificationError.Create(this, VerifierErrorCodes.MissingXRef, + $"Missing company unit '{companyUnit}' for GameObject '{gameObject.Name}'", + VerificationSeverity.Critical, [..context, "CompanyUnits"], companyUnit)); + } + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.cs b/src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.cs new file mode 100644 index 0000000..66d4672 --- /dev/null +++ b/src/ModVerify/Verifiers/GameObjects/GameObjectTypeVerifier.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading; +using AET.ModVerify.Reporting; +using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers.Commons; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Engine.GameObjects; + +namespace AET.ModVerify.Verifiers.GameObjects; + +// TODO: Add GameObjectTypeVerifier and check that LandModelTerrainOverride is correct (all keys correct, no dups) +public sealed partial class GameObjectTypeVerifier : NamedGameEntityVerifier +{ + private readonly SingleModelVerifier _singleModelVerifier; + + public override string FriendlyName => "GameObjectType Verifier"; + + public override IGameManager GameManager => GameEngine.GameObjectTypeManager; + + public override string EntityTypeName => "GameObjectType"; + + public GameObjectTypeVerifier( + IStarWarsGameEngine gameEngine, + GameVerifySettings settings, + IServiceProvider serviceProvider) + : base(gameEngine, settings, serviceProvider) + { + _singleModelVerifier = new SingleModelVerifier(this); + } + + protected override void VerifyEntity(GameObject entity, string[] context, double progress, CancellationToken token) + { + if (entity.Name.Length >= PGConstants.MaxGameObjectTypeName) + { + AddError(VerificationError.Create(this, VerifierErrorCodes.NameTooLong, + $"The GameObjectType name '{entity.Name}' is too long. Maximum length is {PGConstants.MaxGameObjectTypeName}.", + VerificationSeverity.Critical, entity.Name)); + } + VerifyXRefs(entity, context); + VerifyModels(entity, context, token); + VerifyIcons(entity, context); + } + + protected override void PostEntityVerify(CancellationToken token) + { + foreach (var modelError in _singleModelVerifier.VerifyErrors) + AddError(modelError); + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/GameVerifier.cs b/src/ModVerify/Verifiers/GameVerifier.cs index aebfe94..83871d9 100644 --- a/src/ModVerify/Verifiers/GameVerifier.cs +++ b/src/ModVerify/Verifiers/GameVerifier.cs @@ -6,22 +6,50 @@ namespace AET.ModVerify.Verifiers; -public abstract class GameVerifier( - IGameVerifierInfo? parent, - IStarWarsGameEngine gameEngine, - GameVerifySettings settings, - IServiceProvider serviceProvider) - : GameVerifierBase(parent, gameEngine, settings, serviceProvider) where T : notnull +public abstract class GameVerifier : GameVerifierBase where T : notnull { + protected GameVerifier( + IGameVerifierInfo? parent, + IStarWarsGameEngine gameEngine, + GameVerifySettings settings, + IServiceProvider serviceProvider) : base(parent, gameEngine, settings, serviceProvider) + { + } + protected GameVerifier(GameVerifierBase parent) : base(parent) + { + } + + protected GameVerifier( + IStarWarsGameEngine gameEngine, + GameVerifySettings settings, + IServiceProvider serviceProvider) : base(gameEngine, settings, serviceProvider) + { + } + public abstract void Verify(T toVerify, IReadOnlyCollection contextInfo, CancellationToken token); } -public abstract class GameVerifier( - IGameVerifierInfo? parent, - IStarWarsGameEngine gameEngine, - GameVerifySettings settings, - IServiceProvider serviceProvider) - : GameVerifierBase(parent, gameEngine, settings, serviceProvider) +public abstract class GameVerifier : GameVerifierBase { + protected GameVerifier( + IGameVerifierInfo? parent, + IStarWarsGameEngine gameEngine, + GameVerifySettings settings, + IServiceProvider serviceProvider) + : base(parent, gameEngine, settings, serviceProvider) + { + } + protected GameVerifier(GameVerifierBase parent) : base(parent) + { + } + + protected GameVerifier( + IStarWarsGameEngine gameEngine, + GameVerifySettings settings, + IServiceProvider serviceProvider) + : base(gameEngine, settings, serviceProvider) + { + } + public abstract void Verify(CancellationToken token); } \ No newline at end of file diff --git a/src/ModVerify/Verifiers/GameVerifierBase.cs b/src/ModVerify/Verifiers/GameVerifierBase.cs index 02bacc4..2da47bc 100644 --- a/src/ModVerify/Verifiers/GameVerifierBase.cs +++ b/src/ModVerify/Verifiers/GameVerifierBase.cs @@ -7,8 +7,11 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO.Abstractions; -using AET.ModVerify.Pipeline.Progress; +using AET.ModVerify.Progress; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using PG.StarWarsGame.Engine; +using AET.ModVerify.Verifiers.Utilities; namespace AET.ModVerify.Verifiers; @@ -22,6 +25,7 @@ public abstract class GameVerifierBase : IGameVerifierInfo protected readonly IFileSystem FileSystem; protected readonly IServiceProvider Services; protected readonly GameVerifySettings Settings; + protected readonly ILogger Logger; public IReadOnlyCollection VerifyErrors => [.. _verifyErrors.Keys]; @@ -35,7 +39,23 @@ public abstract class GameVerifierBase : IGameVerifierInfo protected IGameRepository Repository => GameEngine.GameRepository; - protected IReadOnlyList VerifierChain { get; } + public IReadOnlyList VerifierChain { get; } + + + protected GameVerifierBase(GameVerifierBase parent) + : this(parent, parent.GameEngine, parent.Settings, parent.Services) + { + if (parent == null) + throw new ArgumentNullException(nameof(parent)); + } + + protected GameVerifierBase( + IStarWarsGameEngine gameEngine, + GameVerifySettings settings, + IServiceProvider serviceProvider) + : this (null, gameEngine, settings, serviceProvider) + { + } protected GameVerifierBase( IGameVerifierInfo? parent, @@ -45,12 +65,13 @@ protected GameVerifierBase( { if (serviceProvider == null) throw new ArgumentNullException(nameof(serviceProvider)); + Logger = serviceProvider.GetService()?.CreateLogger(GetType()) ?? NullLogger.Instance; FileSystem = serviceProvider.GetRequiredService(); Services = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); Parent = parent; Settings = settings ?? throw new ArgumentNullException(nameof(settings)); GameEngine = gameEngine ?? throw new ArgumentNullException(nameof(gameEngine)); - VerifierChain = CreateVerifierChain(); + VerifierChain = this.GetVerifierChain(); } protected void AddError(VerificationError error) @@ -80,18 +101,4 @@ protected void OnProgress(double progress, string? message) { Progress?.Invoke(this, new(progress, message)); } - - private IReadOnlyList CreateVerifierChain() - { - var verifierChain = new List { this }; - - var parent = Parent; - while (parent != null) - { - verifierChain.Insert(0, parent); - parent = parent.Parent; - } - - return verifierChain; - } } \ No newline at end of file diff --git a/src/ModVerify/Verifiers/GuiDialogs/GuiDialogsVerifier.cs b/src/ModVerify/Verifiers/GuiDialogs/GuiDialogsVerifier.cs index 435c5de..a33f900 100644 --- a/src/ModVerify/Verifiers/GuiDialogs/GuiDialogsVerifier.cs +++ b/src/ModVerify/Verifiers/GuiDialogs/GuiDialogsVerifier.cs @@ -1,5 +1,6 @@ using AET.ModVerify.Reporting; using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers.Caching; using AET.ModVerify.Verifiers.Commons; using Microsoft.Extensions.DependencyInjection; using PG.StarWarsGame.Engine; @@ -8,6 +9,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Threading; namespace AET.ModVerify.Verifiers.GuiDialogs; @@ -16,32 +18,29 @@ sealed class GuiDialogsVerifier : GameVerifier { internal const string DefaultComponentIdentifier = "<>"; - private static readonly GuiComponentType[] GuiComponentTypes = + private static readonly IReadOnlyList GuiComponentTypes = Enum.GetValues(typeof(GuiComponentType)).OfType().ToArray(); private readonly IAlreadyVerifiedCache? _cache; - private readonly TextureVeifier _textureVerifier; + private readonly TextureVerifier _textureVerifier; - public GuiDialogsVerifier(IStarWarsGameEngine gameEngine, + public GuiDialogsVerifier( + IStarWarsGameEngine gameEngine, GameVerifySettings settings, - IServiceProvider serviceProvider) : base(null, gameEngine, settings, serviceProvider) + IServiceProvider serviceProvider) + : base(gameEngine, settings, serviceProvider) { _cache = serviceProvider.GetService(); - _textureVerifier = new TextureVeifier(this, gameEngine, settings, serviceProvider); + _textureVerifier = new TextureVerifier(this); } public override void Verify(CancellationToken token) { - try - { - _textureVerifier.Error += OnTextureError; - VerifyMegaTexturesExist(token); - VerifyGuiTextures(); - } - finally - { - _textureVerifier.Error -= OnTextureError; - } + VerifyMegaTexturesExist(token); + VerifyGuiTextures(); + + foreach (var textureError in _textureVerifier.VerifyErrors) + AddError(textureError); } private void VerifyGuiTextures() @@ -52,6 +51,8 @@ private void VerifyGuiTextures() }; components.AddRange(GameEngine.GuiDialogManager.Components); + // TODO: Verify no double definitions for textures and components exit + foreach (var component in components) VerifyGuiComponentTexturesExist(component); @@ -63,8 +64,8 @@ private void VerifyMegaTexturesExist(CancellationToken token) if (GameEngine.GuiDialogManager.MtdFile is null) { var mtdFileName = megaTextureName ?? "<>"; - VerificationError.Create(VerifierChain, VerifierErrorCodes.FileNotFound, $"MtdFile '{mtdFileName}.mtd' could not be found", - VerificationSeverity.Critical, mtdFileName); + AddError(VerificationError.Create(this, VerifierErrorCodes.FileNotFound, $"MtdFile '{mtdFileName}.mtd' could not be found", + VerificationSeverity.Critical, mtdFileName)); } if (megaTextureName is not null) @@ -84,13 +85,19 @@ private void VerifyMegaTexturesExist(CancellationToken token) private void VerifyGuiComponentTexturesExist(string component) { - var middleButtonInRepoMode = false; - - + var buttonSpecialMode = false; + var entriesForComponent = GetTextureEntriesForComponents(component, out var defined); if (!defined) return; + if (entriesForComponent.TryGetValue(GuiComponentType.ButtonMiddle, out var middleTexture)) + { + GameEngine.GuiDialogManager.TextureExists(middleTexture, out var origin, out _); + if (origin == GuiTextureOrigin.Repository) + buttonSpecialMode = true; + } + foreach (var componentType in GuiComponentTypes) { try @@ -98,55 +105,68 @@ private void VerifyGuiComponentTexturesExist(string component) if (!entriesForComponent.TryGetValue(componentType, out var texture)) continue; - if (_cache?.TryAddEntry(texture.Texture) == false) + if (buttonSpecialMode && componentType.IsButton() && !componentType.SupportsSpecialTextureMode()) + { + // If we are in special button mode, non-supported button textures won't be loaded anyway. + continue; + } + + var cached = _cache?.GetEntry(texture.Texture); + if (cached?.AlreadyVerified is true) { // If we are in a special case we don't want to skip - if (!middleButtonInRepoMode && + if (!buttonSpecialMode && componentType is not GuiComponentType.ButtonMiddle && componentType is not GuiComponentType.Scanlines && componentType is not GuiComponentType.FrameBackground) continue; } - if (!GameEngine.GuiDialogManager.TextureExists( - texture, - out var origin, - out var isNone, - middleButtonInRepoMode) - && !isNone) + var exists = GameEngine.GuiDialogManager.TextureExists( + texture, + out var origin, + out var isNone, + buttonSpecialMode); + + if (!exists && !isNone) { - if (origin == GuiTextureOrigin.MegaTexture && texture.Texture.Length > MtdFileConstants.MaxFileNameSize) { - AddError(VerificationError.Create(VerifierChain, VerifierErrorCodes.FilePathTooLong, + AddError(VerificationError.Create(this, VerifierErrorCodes.FilePathTooLong, $"The filename is too long. Max length is {MtdFileConstants.MaxFileNameSize} characters.", VerificationSeverity.Error, texture.Texture)); } else { - var message = $"Could not find GUI texture '{texture.Texture}' at location '{origin}'."; - - if (texture.Texture.Length > PGConstants.MaxMegEntryPathLength) - message += " The file name is too long."; - - AddError(VerificationError.Create(VerifierChain, VerifierErrorCodes.FileNotFound, - message, VerificationSeverity.Error, - [component, origin.ToString()], texture.Texture)); + AddNotFoundError(texture, component, origin); } } - - if (componentType is GuiComponentType.ButtonMiddle && origin is GuiTextureOrigin.Repository) - middleButtonInRepoMode = true; + + _cache?.TryAddEntry(texture.Texture, exists); } finally { - - if (componentType >= GuiComponentType.ButtonRightDisabled) - middleButtonInRepoMode = false; + if (!componentType.IsButton()) + buttonSpecialMode = false; } } } + private void AddNotFoundError(ComponentTextureEntry texture, string component, GuiTextureOrigin? origin) + { + var sb = new StringBuilder($"Could not find GUI texture '{texture.Texture}'"); + if (origin is not null) + sb.Append($" at location '{origin}'"); + sb.Append('.'); + + if (texture.Texture.Length > PGConstants.MaxMegEntryPathLength) + sb.Append(" The file name is too long."); + + AddError(VerificationError.Create(this, VerifierErrorCodes.FileNotFound, + sb.ToString(), VerificationSeverity.Error, + [component, origin.ToString()], texture.Texture)); + } + private IReadOnlyDictionary GetTextureEntriesForComponents(string component, out bool defined) { if (component == DefaultComponentIdentifier) diff --git a/src/ModVerify/Verifiers/IAlreadyVerifiedCache.cs b/src/ModVerify/Verifiers/IAlreadyVerifiedCache.cs deleted file mode 100644 index 1fe8ad5..0000000 --- a/src/ModVerify/Verifiers/IAlreadyVerifiedCache.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using PG.Commons.Hashing; - -namespace AET.ModVerify.Verifiers; - -public interface IAlreadyVerifiedCache -{ - public bool TryAddEntry(string entry); - public bool TryAddEntry(ReadOnlySpan entry); - public bool TryAddEntry(Crc32 checksum); -} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/IGameVerifierInfo.cs b/src/ModVerify/Verifiers/IGameVerifierInfo.cs index cba43d1..731d605 100644 --- a/src/ModVerify/Verifiers/IGameVerifierInfo.cs +++ b/src/ModVerify/Verifiers/IGameVerifierInfo.cs @@ -1,9 +1,13 @@ -namespace AET.ModVerify.Verifiers; +using System.Collections.Generic; + +namespace AET.ModVerify.Verifiers; public interface IGameVerifierInfo { IGameVerifierInfo? Parent { get; } + IReadOnlyList VerifierChain { get; } + string Name { get; } string FriendlyName { get; } diff --git a/src/ModVerify/Verifiers/NamedGameEntityVerifier.cs b/src/ModVerify/Verifiers/NamedGameEntityVerifier.cs new file mode 100644 index 0000000..18ec9c3 --- /dev/null +++ b/src/ModVerify/Verifiers/NamedGameEntityVerifier.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading; +using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers.Commons; +using AET.ModVerify.Verifiers.Utilities; +using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Files.XML.Data; + +namespace AET.ModVerify.Verifiers; + +public abstract partial class NamedGameEntityVerifier( + IStarWarsGameEngine gameEngine, + GameVerifySettings settings, + IServiceProvider serviceProvider) + : GameVerifier(null, gameEngine, settings, serviceProvider) + where T : NamedXmlObject +{ + public abstract IGameManager GameManager { get; } + + public abstract string EntityTypeName { get; } + + public sealed override void Verify(CancellationToken token) + { + OnProgress(0.0, $"Verifying GameManager for '{EntityTypeName}'"); + PreEntityVerify(token); + OnProgress(0.5, null); + + var numEntities = GameEngine.GameObjectTypeManager.Entries.Count; + double counter = 0; + var context = new string[1]; + foreach (var gameEntity in GameManager.Entries) + { + LogVerifyingEntityTypeName(Logger, EntityTypeName, gameEntity.Name); + var progress = 0.5 + ++counter / numEntities * 0.5; + OnProgress(progress, $"{EntityTypeName} - '{gameEntity.Name}'"); + context[0] = gameEntity.Name; + VerifyEntity(gameEntity, context, progress, token); + } + + PostEntityVerify(token); + } + + protected virtual void PostEntityVerify(CancellationToken token) + { + } + + protected abstract void VerifyEntity(T entity, string[] context, double progress, CancellationToken token); + + protected virtual void PreEntityVerify(CancellationToken token) + { + VerifyDuplicates(token); + } + + private void VerifyDuplicates(CancellationToken token) + { + LogCheckingEntityTypeForDuplicateEntries(Logger, EntityTypeName); + var context = IDuplicateVerificationContext.CreateForNamedXmlObjects(GameManager, EntityTypeName); + var verifier = new DuplicateVerifier(this, GameEngine, Settings, Services); + verifier.Verify(context, [], token); + foreach (var error in verifier.VerifyErrors) + AddError(error); + } + + [LoggerMessage(LogLevel.Trace, "Verifying {entityType} - '{name}'")] + static partial void LogVerifyingEntityTypeName(ILogger? logger, string entityType, string name); + + [LoggerMessage(LogLevel.Debug, "Checking {entityType} for duplicate entries")] + static partial void LogCheckingEntityTypeForDuplicateEntries(ILogger logger, string entityType); +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/ReferencedModelsVerifier.cs b/src/ModVerify/Verifiers/ReferencedModelsVerifier.cs deleted file mode 100644 index a36648b..0000000 --- a/src/ModVerify/Verifiers/ReferencedModelsVerifier.cs +++ /dev/null @@ -1,50 +0,0 @@ -using AET.ModVerify.Settings; -using AET.ModVerify.Verifiers.Commons; -using PG.StarWarsGame.Engine; -using System; -using System.Linq; -using System.Threading; - -namespace AET.ModVerify.Verifiers; - -public sealed class ReferencedModelsVerifier( - IStarWarsGameEngine engine, - GameVerifySettings settings, - IServiceProvider serviceProvider) - : GameVerifier(null, engine, settings, serviceProvider) -{ - public override string FriendlyName => "Referenced Models"; - - public override void Verify(CancellationToken token) - { - var models = GameEngine.GameObjectTypeManager.Entries - .SelectMany(x => x.Models) - .Concat(FocHardcodedConstants.HardcodedModels).ToList(); - - if (models.Count == 0) - return; - - var numModels = models.Count; - var counter = 0; - - var inner = new SingleModelVerifier(this, GameEngine, Settings, Services); - try - { - inner.Error += OnModelError; - foreach (var model in models) - { - OnProgress((double)++counter / numModels, $"Model - '{model}'"); - inner.Verify(model, [], token); - } - } - finally - { - inner.Error -= OnModelError; - } - } - - private void OnModelError(object sender, VerificationErrorEventArgs e) - { - AddError(e.Error); - } -} diff --git a/src/ModVerify/Verifiers/SfxEvents/SfxEventVerifier.Samples.cs b/src/ModVerify/Verifiers/SfxEvents/SfxEventVerifier.Samples.cs new file mode 100644 index 0000000..68aee32 --- /dev/null +++ b/src/ModVerify/Verifiers/SfxEvents/SfxEventVerifier.Samples.cs @@ -0,0 +1,114 @@ +using System; +using System.Buffers; +using System.Threading; +using AET.ModVerify.Verifiers.Commons; +using AnakinRaW.CommonUtilities.FileSystem.Normalization; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Engine.Audio.Sfx; +using PG.StarWarsGame.Engine.Localization; + +namespace AET.ModVerify.Verifiers.SfxEvents; + +public partial class SfxEventVerifier +{ + private void VerifySamples(SfxEvent sfxEvent, string[] context, CancellationToken token) + { + var isAmbient = IsAmbient2D(sfxEvent); + foreach (var codedSample in sfxEvent.AllSamples) + VerifySample(codedSample.AsSpan(), sfxEvent, context, isAmbient, token); + } + + private void VerifySample( + ReadOnlySpan sample, + SfxEvent sfxEvent, + string[] context, + bool isAmbient, + CancellationToken token) + { + char[]? pooledBuffer = null; + + var buffer = sample.Length < PGConstants.MaxMegEntryPathLength + ? stackalloc char[PGConstants.MaxMegEntryPathLength] + : pooledBuffer = ArrayPool.Shared.Rent(sample.Length); + + try + { + var length = PathNormalizer.Normalize(sample, buffer, SampleNormalizerOptions); + var sampleNameBuffer = buffer.Slice(0, length); + + if (sfxEvent.IsLocalized) + { + foreach (var language in _languagesToVerify) + { + VerifySampleLocalized(context, sampleNameBuffer, isAmbient, language, out var localized, token); + if (!localized) + { + // There is no reason to continue if we failed to localize the sample name, because the verification will fail anyway + // and we want to avoid multiple errors for the same sample. + return; + } + } + } + else + { + var audioInfo = new AudioFileInfo(sampleNameBuffer.ToString(), AudioFileType.Wav, isAmbient); + _audioFileVerifier.Verify(audioInfo, context, token); + } + } + finally + { + if (pooledBuffer is not null) + ArrayPool.Shared.Return(pooledBuffer); + } + } + + private void VerifySampleLocalized(string[] context, + ReadOnlySpan sample, + bool isAmbient, + LanguageType language, + out bool localized, + CancellationToken token) + { + char[]? pooledBuffer = null; + + var buffer = sample.Length < PGConstants.MaxMegEntryPathLength + ? stackalloc char[PGConstants.MaxMegEntryPathLength] + : pooledBuffer = ArrayPool.Shared.Rent(sample.Length); + try + { + var l = _languageManager.LocalizeFileName(sample, language, buffer, out localized); + var localizedName = buffer.Slice(0, l); + + var audioInfo = new AudioFileInfo(localizedName.ToString(), AudioFileType.Wav, isAmbient); + _audioFileVerifier.Verify(audioInfo, context, token); + } + finally + { + if (pooledBuffer is not null) + ArrayPool.Shared.Return(pooledBuffer); + } + } + + // Some heuristics whether a SFXEvent is most likely to be an ambient sound. + private bool IsAmbient2D(SfxEvent sfxEvent) + { + if (!sfxEvent.Is2D) + return false; + + if (sfxEvent.IsPreset) + return false; + + // If the event is located in SFXEventsAmbient.xml we simply assume it's an ambient sound. + var fileName = _fileSystem.Path.GetFileName(sfxEvent.Location.XmlFile).AsSpan(); + if (fileName.Equals("SFXEventsAmbient.xml".AsSpan(), StringComparison.OrdinalIgnoreCase)) + return true; + + if (string.IsNullOrEmpty(sfxEvent.UsePresetName)) + return false; + + if (sfxEvent.UsePresetName!.StartsWith("Preset_AMB_2D", StringComparison.OrdinalIgnoreCase)) + return true; + + return false; + } +} diff --git a/src/ModVerify/Verifiers/SfxEvents/SfxEventVerifier.XRef.cs b/src/ModVerify/Verifiers/SfxEvents/SfxEventVerifier.XRef.cs new file mode 100644 index 0000000..ffdb772 --- /dev/null +++ b/src/ModVerify/Verifiers/SfxEvents/SfxEventVerifier.XRef.cs @@ -0,0 +1,21 @@ +using AET.ModVerify.Reporting; +using PG.StarWarsGame.Engine.Audio.Sfx; + +namespace AET.ModVerify.Verifiers.SfxEvents; + +public partial class SfxEventVerifier +{ + private void VerifyPresetRef(SfxEvent sfxEvent, string[] context) + { + if (!string.IsNullOrEmpty(sfxEvent.UsePresetName) && sfxEvent.Preset is null) + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.MissingXRef, + $"Missing preset '{sfxEvent.UsePresetName}' for SFXEvent '{sfxEvent.Name}'.", + VerificationSeverity.Error, + [..context, "Preset"], + sfxEvent.UsePresetName)); + } + } +} diff --git a/src/ModVerify/Verifiers/SfxEvents/SfxEventVerifier.cs b/src/ModVerify/Verifiers/SfxEvents/SfxEventVerifier.cs new file mode 100644 index 0000000..c688634 --- /dev/null +++ b/src/ModVerify/Verifiers/SfxEvents/SfxEventVerifier.cs @@ -0,0 +1,81 @@ +using AET.ModVerify.Reporting; +using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers.Commons; +using AnakinRaW.CommonUtilities.FileSystem.Normalization; +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Engine.Audio.Sfx; +using PG.StarWarsGame.Engine.Localization; +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Threading; + +namespace AET.ModVerify.Verifiers.SfxEvents; + +public sealed partial class SfxEventVerifier : NamedGameEntityVerifier +{ + private static readonly PathNormalizeOptions SampleNormalizerOptions = new() + { + UnifyCase = UnifyCasingKind.UpperCaseForce, + UnifySeparatorKind = DirectorySeparatorKind.Windows, + UnifyDirectorySeparators = true + }; + + private readonly IGameLanguageManager _languageManager; + private readonly IFileSystem _fileSystem; + private readonly AudioFileVerifier _audioFileVerifier; + private readonly IReadOnlyCollection _languagesToVerify; + + public override IGameManager GameManager => GameEngine.SfxGameManager; + public override string FriendlyName => "SFX Events"; + public override string EntityTypeName => "SFXEvent"; + + public SfxEventVerifier( + IStarWarsGameEngine gameEngine, + GameVerifySettings settings, + IServiceProvider serviceProvider) + : base(gameEngine, settings, serviceProvider) + { + _languageManager = serviceProvider.GetRequiredService() + .GetLanguageManager(Repository.EngineType); + _fileSystem = serviceProvider.GetRequiredService(); + _audioFileVerifier = new AudioFileVerifier(this); + _languagesToVerify = GetLanguagesToVerify(); + } + + protected override void VerifyEntity(SfxEvent entity, string[] context, double progress, CancellationToken token) + { + if (entity.Name.Length >= PGConstants.MaxSFXEventName) + { + AddError(VerificationError.Create(this, VerifierErrorCodes.NameTooLong, + $"The SFXEvent name '{entity.Name}' is too long. Maximum length is {PGConstants.MaxSFXEventName}.", + VerificationSeverity.Critical, entity.Name)); + } + VerifyPresetRef(entity, context); + VerifySamples(entity, context, token); + } + + protected override void PostEntityVerify(CancellationToken token) + { + foreach (var sampleError in _audioFileVerifier.VerifyErrors) + AddError(sampleError); + } + + private IReadOnlyCollection GetLanguagesToVerify() + { + switch (Settings.LocalizationOption) + { + case VerifyLocalizationOption.English: + return [LanguageType.English]; + case VerifyLocalizationOption.CurrentSystem: + return [_languageManager.GetLanguagesFromUser()]; + case VerifyLocalizationOption.AllInstalled: + return [..GameEngine.InstalledLanguages]; + case VerifyLocalizationOption.All: + return [.._languageManager.SupportedLanguages]; + default: + throw new NotSupportedException($"{Settings.LocalizationOption} is not supported"); + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/Utilities/DuplicateVerificationContextExtensions.cs b/src/ModVerify/Verifiers/Utilities/DuplicateVerificationContextExtensions.cs new file mode 100644 index 0000000..cd81de6 --- /dev/null +++ b/src/ModVerify/Verifiers/Utilities/DuplicateVerificationContextExtensions.cs @@ -0,0 +1,22 @@ +using AET.ModVerify.Verifiers.Commons; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Files.MTD.Files; +using PG.StarWarsGame.Files.XML.Data; + +namespace AET.ModVerify.Verifiers.Utilities; + +public static class DuplicateVerificationContextExtensions +{ + extension(IDuplicateVerificationContext) + { + public static IDuplicateVerificationContext CreateForMtd(IMtdFile mtdFile) + { + return new MtdDuplicateVerificationContext(mtdFile); + } + + public static IDuplicateVerificationContext CreateForNamedXmlObjects(IGameManager gameManager, string databaseName) where T : NamedXmlObject + { + return new NamedXmlObjectDuplicateVerificationContext(databaseName, gameManager); + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/Utilities/GameVerifierInfoExtensions.cs b/src/ModVerify/Verifiers/Utilities/GameVerifierInfoExtensions.cs new file mode 100644 index 0000000..05787a8 --- /dev/null +++ b/src/ModVerify/Verifiers/Utilities/GameVerifierInfoExtensions.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace AET.ModVerify.Verifiers.Utilities; + +internal static class GameVerifierInfoExtensions +{ + public static IReadOnlyList GetVerifierChain(this IGameVerifierInfo verifier) + { + if (verifier.Parent is null) + return [verifier]; + + var parentChain = verifier.Parent.VerifierChain; + var result = new List(parentChain.Count + 1); + result.AddRange(parentChain); + result.Add(verifier); + return result; + } +} diff --git a/src/ModVerify/Verifiers/Utilities/NameBasedEqualityComparer.cs b/src/ModVerify/Verifiers/Utilities/NameBasedEqualityComparer.cs new file mode 100644 index 0000000..1a44506 --- /dev/null +++ b/src/ModVerify/Verifiers/Utilities/NameBasedEqualityComparer.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace AET.ModVerify.Verifiers.Utilities; + +internal sealed class NameBasedEqualityComparer : IEqualityComparer, IEqualityComparer +{ + public static readonly NameBasedEqualityComparer Instance = new(); + + public bool Equals(GameVerifier? x, GameVerifier? y) + { + return Equals(x as IGameVerifierInfo, y); + } + + public int GetHashCode(GameVerifier? obj) + { + return GetHashCode(obj as IGameVerifierInfo); + } + + public bool Equals(IGameVerifierInfo? x, IGameVerifierInfo? y) + { + if (ReferenceEquals(x, y)) + return true; + if (x is null) + return false; + if (y is null) + return false; + return x.Name == y.Name; + } + + public int GetHashCode(IGameVerifierInfo? obj) + { + return obj?.Name.GetHashCode() ?? 0; + } +} \ No newline at end of file diff --git a/src/ModVerify/Verifiers/VerifierErrorCodes.cs b/src/ModVerify/Verifiers/VerifierErrorCodes.cs index 57d3209..04a900d 100644 --- a/src/ModVerify/Verifiers/VerifierErrorCodes.cs +++ b/src/ModVerify/Verifiers/VerifierErrorCodes.cs @@ -10,14 +10,19 @@ public static class VerifierErrorCodes public const string GenericExceptionErrorCode = "MV00"; - public const string FileCorrupt = "ENG00"; + public const string BinaryFileCorrupt = "BIN00"; public const string FileNotFound = "FILE00"; public const string FilePathTooLong = "FILE01"; public const string InvalidFilePath = "FILE02"; - public const string DuplicateFound = "DUP00"; + public const string Duplicate = "DUP00"; + public const string MissingXRef = "XREF00"; + + public const string NameTooLong = "NAME00"; + + public const string MissingPreset = "SFX00"; public const string SampleNotPCM = "WAV00"; public const string SampleNotMono = "WAV01"; @@ -36,4 +41,6 @@ public static class VerifierErrorCodes public const string XmlDataBeforeHeader = "XML08"; public const string XmlMissingNode = "XML09"; public const string XmlUnsupportedTag = "XML10"; + public const string XmlElementsInTag = "XML11"; + public const string XmlUnexceptedElementName = "XML12"; } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/SfxEvent.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/SfxEvent.cs index aed53f2..43f437d 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/SfxEvent.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/SfxEvent.cs @@ -1,11 +1,14 @@ using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; using System.Linq; using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.Xml; using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Data; namespace PG.StarWarsGame.Engine.Audio.Sfx; +[DebuggerDisplay("{Name}")] public sealed class SfxEvent : NamedXmlObject { private byte _minVolume = DefaultMinVolume; @@ -25,11 +28,11 @@ public sealed class SfxEvent : NamedXmlObject public const byte MaxPan2dValue = 100; public const byte MinPriorityValue = 1; public const byte MaxPriorityValue = 5; - public const byte MaxProbability = 100; - public const sbyte MinMaxInstances = 0; - public const sbyte InfinitivePlayCount = -1; - public const float MinLoopSeconds = 0.0f; - public const float MinVolumeSaturation = 0.0f; + public const byte MaxProbabilityValue = 100; + public const sbyte MinMaxInstancesValue = 0; + public const sbyte InfinitivePlayCountValue = -1; + public const float MinLoopSecondsValue = 0.0f; + public const float MinVolumeSaturationValue = 0.0f; // Default values which are not the default value of the type public const byte DefaultPriority = 3; @@ -45,6 +48,11 @@ public sealed class SfxEvent : NamedXmlObject public const byte DefaultMaxPan2d = 50; public const float DefaultVolumeSaturationDistance = 300.0f; + internal readonly List PreSamplesInternal = new(); + internal readonly List SamplesInternal = new(); + internal readonly List PostSamplesInternal = new(); + internal readonly List LocalizedTextIDsInternal = new(); + public bool IsPreset { get; internal set; } public bool Is3D { get; internal set; } = DefaultIs3d; @@ -69,14 +77,14 @@ public sealed class SfxEvent : NamedXmlObject public IEnumerable AllSamples => PreSamples.Concat(Samples).Concat(PostSamples); - public IReadOnlyList PreSamples { get; internal set; } = []; - - public IReadOnlyList Samples { get; internal set; } = []; + public IReadOnlyList PreSamples { get; } - public IReadOnlyList PostSamples { get; internal set; } = []; + public IReadOnlyList Samples { get; } - public IReadOnlyList LocalizedTextIDs { get; internal set; } = []; + public IReadOnlyList PostSamples { get; } + public IReadOnlyList LocalizedTextIDs { get; } + public byte Priority { get; internal set; } = DefaultPriority; public byte Probability { get; internal set; } = DefaultProbability; @@ -161,9 +169,13 @@ public uint MaxPostdelay internal SfxEvent(string name, Crc32 nameCrc, XmlLocationInfo location) : base(name, nameCrc, location) { + PreSamples = new ReadOnlyCollection(PreSamplesInternal); + Samples = new ReadOnlyCollection(SamplesInternal); + PostSamples = new ReadOnlyCollection(PostSamplesInternal); + LocalizedTextIDs = new ReadOnlyCollection(LocalizedTextIDsInternal); } - internal override void CoerceValues() + internal void FixupValues() { AdjustMinMaxValues(ref _minVolume, ref _maxVolume); AdjustMinMaxValues(ref _minPitch, ref _maxPitch); @@ -187,6 +199,8 @@ internal override void CoerceValues() */ public void ApplyPreset(SfxEvent preset) { + Preset = preset; + Is3D = preset.Is3D; Is2D = preset.Is2D; IsGui = preset.IsGui; @@ -194,13 +208,11 @@ public void ApplyPreset(SfxEvent preset) IsUnitResponseVo = preset.IsUnitResponseVo; IsAmbientVo = preset.IsAmbientVo; IsLocalized = preset.IsLocalized; - Preset = preset; - UsePresetName = preset.Name; PlaySequentially = preset.PlaySequentially; - PreSamples = preset.PreSamples; - Samples = preset.Samples; - PostSamples = preset.PostSamples; - LocalizedTextIDs = preset.LocalizedTextIDs; + SetList(PreSamplesInternal, preset.PreSamplesInternal); + SetList(SamplesInternal, preset.SamplesInternal); + SetList(PostSamplesInternal, preset.PostSamplesInternal); + SetList(LocalizedTextIDsInternal, preset.LocalizedTextIDsInternal); Priority = preset.Priority; Probability = preset.Probability; PlayCount = preset.PlayCount; @@ -219,6 +231,13 @@ public void ApplyPreset(SfxEvent preset) MaxPitch = preset.MaxPitch; MinPan2D = preset.MinPan2D; MaxPan2D = preset.MaxPan2D; + return; + + static void SetList(List target, List source) + { + target.Clear(); + target.AddRange(source); + } } private static void AdjustMinMaxValues(ref byte minValue, ref byte maxValue) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/SfxEventGameManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/SfxEventGameManager.cs index b037939..99f1c31 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/SfxEventGameManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Audio/Sfx/SfxEventGameManager.cs @@ -7,11 +7,14 @@ using PG.StarWarsGame.Engine.ErrorReporting; using PG.StarWarsGame.Engine.IO.Repositories; using PG.StarWarsGame.Engine.Localization; -using PG.StarWarsGame.Engine.Xml.Parsers; +using PG.StarWarsGame.Engine.Xml; namespace PG.StarWarsGame.Engine.Audio.Sfx; -internal class SfxEventGameManager(GameRepository repository, GameEngineErrorReporterWrapper errorReporter, IServiceProvider serviceProvider) +internal class SfxEventGameManager( + GameRepository repository, + GameEngineErrorReporterWrapper errorReporter, + IServiceProvider serviceProvider) : GameManagerBase(repository, errorReporter, serviceProvider), ISfxEventGameManager { public IEnumerable InstalledLanguages { get; private set; } = []; @@ -24,42 +27,20 @@ protected override async Task InitializeCoreAsync(CancellationToken token) Logger?.LogInformation("Parsing SFXEvents..."); - var contentParser = new XmlContainerContentParser(ServiceProvider, ErrorReporter); - contentParser.XmlParseError += OnParseError; - try - { - await Task.Run(() => contentParser.ParseEntriesFromFileListXml( - "DATA\\XML\\SFXEventFiles.XML", - GameRepository, - "DATA\\XML", - NamedEntries, - VerifyFilePathLength), - token); - } - finally - { - contentParser.XmlParseError -= OnParseError; - } - } - - private void OnParseError(object sender, XmlContainerParserErrorEventArgs e) - { - if (e.ErrorInXmlFileList || e.HasException) - { - e.Continue = false; - ErrorReporter.Report(new InitializationError + var contentParser = new PetroglyphStarWarsGameXmlParser(GameRepository, + new PetroglyphStarWarsGameXmlParseSettings { GameManager = ToString(), - Message = GetMessage(e) - }); - } - } - - private static string GetMessage(XmlContainerParserErrorEventArgs errorEventArgs) - { - if (errorEventArgs.HasException) - return $"Error while parsing SFXEvent XML file '{errorEventArgs.File}': {errorEventArgs.Exception.Message}"; - return "Could not find SFXEventFiles.xml"; + InvalidObjectXmlFailsInitialization = true, + InvalidFilesListXmlFailsInitialization = true + }, ServiceProvider, ErrorReporter); + + await Task.Run(() => contentParser.ParseEntriesFromFileListXml( + "DATA\\XML\\SFXEventFiles.XML", + "DATA\\XML", + NamedEntries, + VerifyFilePathLength), + token); } private void VerifyFilePathLength(string filePath) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager.cs index 0b8687e..a012a50 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager.cs @@ -1,27 +1,21 @@ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.CommandBar.Components; -using PG.StarWarsGame.Engine.CommandBar.Xml; using PG.StarWarsGame.Engine.ErrorReporting; using PG.StarWarsGame.Engine.GameConstants; using PG.StarWarsGame.Engine.IO.Repositories; using PG.StarWarsGame.Engine.Rendering; using PG.StarWarsGame.Engine.Rendering.Font; -using PG.StarWarsGame.Engine.Xml.Parsers; -using PG.StarWarsGame.Files.Binary; using PG.StarWarsGame.Files.MTD.Files; using PG.StarWarsGame.Files.MTD.Services; using System; using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using AnakinRaW.CommonUtilities.Collections; +using System.Numerics; +using PG.StarWarsGame.Engine.GameObjects; namespace PG.StarWarsGame.Engine.CommandBar; -internal class CommandBarGameManager( +internal partial class CommandBarGameManager( GameRepository repository, PGRender pgRender, IGameConstants gameConstants, @@ -33,16 +27,14 @@ internal class CommandBarGameManager( private readonly ICrc32HashingService _hashingService = serviceProvider.GetRequiredService(); private readonly IMtdFileService _mtdFileService = serviceProvider.GetRequiredService(); private readonly Dictionary _groups = new(); - private readonly PGRender _pgRender = pgRender; - private bool _megaTextureExists; private FontData? _defaultFont; public ICollection Components => Entries; public IReadOnlyDictionary Groups => _groups; - public FontData? DefaultFont + public FontData? DefaultFont { get { @@ -56,7 +48,7 @@ private set } } - public IMtdFile? MegaTextureFile + public IMtdFile? MtdFile { get { @@ -70,224 +62,39 @@ private set } } - protected override async Task InitializeCoreAsync(CancellationToken token) + public string? MegaTextureFileName { - Logger?.LogInformation("Creating command bar components..."); - - var contentParser = new XmlContainerContentParser(ServiceProvider, ErrorReporter); - contentParser.XmlParseError += OnParseError; - - var parsedCommandBarComponents = new FrugalValueListDictionary(); - - try - { - await Task.Run(() => contentParser.ParseEntriesFromFileListXml( - "DATA\\XML\\CommandBarComponentFiles.XML", - GameRepository, - ".\\DATA\\XML", - parsedCommandBarComponents, - VerifyFilePathLength), - token); - } - finally - { - contentParser.XmlParseError -= OnParseError; - } - - // Create Scene - // Create Camera - // Resize(true) - - foreach (var parsedCommandBarComponent in parsedCommandBarComponents.Values) + get { - var component = CommandBarBaseComponent.Create(parsedCommandBarComponent, ErrorReporter); - if (component is not null) - { - var crc = _hashingService.GetCrc32(component.Name, PGConstants.DefaultPGEncoding); - NamedEntries.Add(crc, component); - } + ThrowIfNotInitialized(); + return field; } - - SetComponentGroup(Components); - SetMegaTexture(); - SetDefaultFont(); - - LinkComponentsToShell(); - LinkComponentsWithActions(); - } - - private void LinkComponentsWithActions() - { - var nameLookup = SupportedCommandBarComponentData.GetComponentIdsForEngine(GameRepository.EngineType); - - foreach (var idPair in nameLookup) + private set { - var crc = _hashingService.GetCrc32(idPair.Value, PGConstants.DefaultPGEncoding); - if (NamedEntries.TryGetFirstValue(crc, out var component)) - component.Id = idPair.Key; + ThrowIfAlreadyInitialized(); + field = value; } } - private void LinkComponentsToShell() - { - if (!Groups.TryGetValue(CommandBarConstants.ShellGroupName, out var shellGroup)) - return; - var modelCache = new Dictionary(); - foreach (var component in Components) - { - if (component.Type == CommandBarComponentType.Shell) - continue; + public Vector3 CommandBarScale { get; } - foreach (var shellComponent in shellGroup.Components) - { - if (LinkToShell(component, shellComponent as CommandBarShellComponent, modelCache)) - break; - } - } + public Vector3 CommandBarOffset { get; internal set; } - foreach (var model in modelCache.Values) - model?.Dispose(); - } - - private bool LinkToShell( - CommandBarBaseComponent component, - CommandBarShellComponent? shell, - IDictionary modelCache) + public bool IconExists(GameObject gameObject) { - if (shell is null) - { - ErrorReporter.Assert( - EngineAssert.FromNullOrEmpty( - [component.Name], $"Cannot link component '{component}' because shell component is null.")); - return false; - } - - var componentName = component.Name; - if (string.IsNullOrEmpty(componentName)) - return false; - - var modelPath = shell.ModelPath; - if (string.IsNullOrEmpty(modelPath)) - return false; - - if (!modelCache.TryGetValue(shell.Name, out var model)) - { - model = _pgRender.LoadModelAndAnimations(modelPath.AsSpan(), null, true); - modelCache.Add(shell.Name, model); - } - - if (model is null) - { - ErrorReporter.Assert( - EngineAssert.FromNullOrEmpty( - [$"component='{component.Name}'", $"shell='{shell.Name}'"], - $"Cannot link component '{componentName}' to shell '{shell.Name}' because model '{modelPath}' could not be loaded.")); - return false; - } + ThrowIfNotInitialized(); + if (gameObject == null) + throw new ArgumentNullException(nameof(gameObject)); - if (!model.IsModel) - { - ErrorReporter.Assert( - EngineAssert.FromNullOrEmpty( - [$"component='{component.Name}'", $"shell='{shell.Name}'"], - $"Cannot link component '{componentName}' to shell '{shell.Name}' because the loaded file '{modelPath}' is not a model.")); + if (MtdFile is null) return false; - } - var boneIndex = model.IndexOfBone(componentName); - - if (boneIndex == -1) + if (string.IsNullOrEmpty(gameObject.IconName)) return false; - component.Bone = boneIndex; - component.ParentShell = shell; - return true; - } - private void SetDefaultFont() - { - // The code is only triggered iff at least one Text CommandbarBar component existed - if (Components.FirstOrDefault(x => x is CommandBarTextComponent or CommandBarTextButtonComponent) is null) - return; + var crc = _hashingService.GetCrc32Upper(gameObject.IconName, PGConstants.DefaultPGEncoding); - if (_defaultFont is null) - { - // TODO: From GameConstants - var fontName = PGConstants.DefaultUnicodeFontName; - var size = 11; - var font = fontManager.CreateFont(fontName, size, true, false, false, 1.0f); - if (font is null) - ErrorReporter.Assert(EngineAssert.FromNullOrEmpty([ToString()], $"Unable to create Default from name {fontName}")); - DefaultFont = font; - } - } - - private void SetMegaTexture() - { - // The code is only triggered iff at least one Shell CommandbarBar component existed - if (Components.FirstOrDefault(x => x is CommandBarShellComponent) is null) - return; - // Note: The tag is not used by the engine - var mtdPath = FileSystem.Path.Combine("DATA\\ART\\TEXTURES", $"{CommandBarConstants.MegaTextureBaseName}.mtd"); - using var megaTexture = GameRepository.TryOpenFile(mtdPath); - - try - { - MegaTextureFile = megaTexture is null ? null : _mtdFileService.Load(megaTexture); - } - catch (BinaryCorruptedException e) - { - var message = $"Failed to load MTD file '{mtdPath}': {e.Message}"; - Logger?.LogError(e, message); - ErrorReporter.Assert(EngineAssert.Create(EngineAssertKind.CorruptBinary, mtdPath, [], message)); - } - _megaTextureExists = GameRepository.TextureRepository.FileExists($"{CommandBarConstants.MegaTextureBaseName}.tga"); - } - - private void SetComponentGroup(IEnumerable components) - { - var groupData = components - .Where(x => !string.IsNullOrEmpty(x.XmlData.Group)) - .GroupBy(x => x.XmlData.Group!, StringComparer.Ordinal); - - foreach (var grouping in groupData) - { - var group = new CommandBarComponentGroup(grouping.Key, grouping); - _groups.Add(grouping.Key, group); - foreach (var component in grouping) - component.Group = group; - } - } - - private void OnParseError(object sender, XmlContainerParserErrorEventArgs e) - { - if (e.ErrorInXmlFileList || e.HasException) - { - e.Continue = false; - ErrorReporter.Report(new InitializationError - { - GameManager = ToString(), - Message = GetMessage(e) - }); - } - } - - private static string GetMessage(XmlContainerParserErrorEventArgs errorEventArgs) - { - if (errorEventArgs.HasException) - return $"Error while parsing CommandBar XML file '{errorEventArgs.File}': {errorEventArgs.Exception.Message}"; - return "Could not find CommandBarComponentFiles.xml"; - } - - private void VerifyFilePathLength(string filePath) - { - if (filePath.Length > PGConstants.MaxCommandBarDatabaseFileName) - { - ErrorReporter.Report(new InitializationError - { - GameManager = ToString(), - Message = $"CommandBar file '{filePath}' is longer than {PGConstants.MaxCommandBarDatabaseFileName} characters." - }); - } + return MtdFile.Content.Contains(crc); } -} +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager_Initialization.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager_Initialization.cs new file mode 100644 index 0000000..4835ed3 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager_Initialization.cs @@ -0,0 +1,243 @@ +using AnakinRaW.CommonUtilities.Collections; +using Microsoft.Extensions.Logging; +using PG.Commons.Hashing; +using PG.StarWarsGame.Engine.CommandBar.Components; +using PG.StarWarsGame.Engine.CommandBar.Xml; +using PG.StarWarsGame.Engine.ErrorReporting; +using PG.StarWarsGame.Engine.Rendering; +using PG.StarWarsGame.Engine.Xml; +using PG.StarWarsGame.Files.Binary; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Threading; +using System.Threading.Tasks; + +namespace PG.StarWarsGame.Engine.CommandBar; + +internal partial class CommandBarGameManager +{ + protected override async Task InitializeCoreAsync(CancellationToken token) + { + Logger?.LogInformation("Creating command bar components..."); + + var contentParser = new PetroglyphStarWarsGameXmlParser(GameRepository, new PetroglyphStarWarsGameXmlParseSettings + { + GameManager = ToString(), + InvalidObjectXmlFailsInitialization = true, + InvalidFilesListXmlFailsInitialization = true + }, ServiceProvider, ErrorReporter); + + var parsedCommandBarComponents = new FrugalValueListDictionary(); + + await Task.Run(() => contentParser.ParseEntriesFromFileListXml( + ".\\Data\\XML\\CommandBarComponentFiles.xml", + ".\\DATA\\XML\\", + parsedCommandBarComponents, + VerifyFilePathLength), + token); + + CommandBarOffset = new Vector3(-0.5f, -0.5f, 0.0f); + + // Create Scene + // Create Camera + // Resize(force: true) + + foreach (var parsedCommandBarComponent in parsedCommandBarComponents.Values) + { + var component = CommandBarBaseComponent.Create(parsedCommandBarComponent, ErrorReporter); + if (component is not null) + { + var crc = _hashingService.GetCrc32(component.Name, PGConstants.DefaultPGEncoding); + NamedEntries.Add(crc, component); + } + + if (component is CommandBarShellComponent shellComponent) + SetModelTransform(shellComponent); + } + + SetComponentGroup(Components); + SetMegaTexture(); + SetDefaultFont(); + + LinkComponentsToShell(); + LinkComponentsWithActions(); + + + // CommandBarClass::Set_Encyclopedia_Delay_Time(this); + // CommandBarClass::Find_Neighbors(this); + + // CommandBarClass::Load_Hero_Particles(this); + // CommandBarClass::Load_Corruption_Particle(this); + } + + private void SetModelTransform(CommandBarShellComponent shellComponent) + { + if (string.IsNullOrEmpty(shellComponent.ModelPath) || + !GameRepository.ModelRepository.FileExists(shellComponent.ModelPath)) + return; + shellComponent.SetOffsetAndScale(CommandBarOffset, CommandBarScale); + } + + private void LinkComponentsWithActions() + { + var nameLookup = SupportedCommandBarComponentData.GetComponentIdsForEngine(GameRepository.EngineType); + foreach (var idPair in nameLookup) + { + // The engine does not uppercase the name here + var crc = _hashingService.GetCrc32(idPair.Value, PGConstants.DefaultPGEncoding); + if (NamedEntries.TryGetFirstValue(crc, out var component)) + { + // NB: Currently we do not have "action" + // but we keep the original method name 'LinkComponentsWithActions' + component.Id = idPair.Key; + } + } + } + + private void LinkComponentsToShell() + { + if (!Groups.TryGetValue(CommandBarConstants.ShellGroupName, out var shellGroup)) + return; + + var modelCache = new Dictionary(); + foreach (var component in Components) + { + if (component.Type == CommandBarComponentType.Shell) + continue; + + foreach (var shellComponent in shellGroup.Components) + { + if (LinkToShell(component, shellComponent as CommandBarShellComponent, modelCache)) + break; + } + } + + foreach (var model in modelCache.Values) + model?.Dispose(); + } + + private bool LinkToShell( + CommandBarBaseComponent component, + CommandBarShellComponent? shell, + IDictionary modelCache) + { + if (shell is null) + { + ErrorReporter.Assert( + EngineAssert.FromNullOrEmpty( + [component.Name], $"Cannot link component '{component}' because shell component is null.")); + return false; + } + + var componentName = component.Name; + if (string.IsNullOrEmpty(componentName)) + return false; + + var modelPath = shell.ModelPath; + if (string.IsNullOrEmpty(modelPath)) + return false; + + if (!modelCache.TryGetValue(shell.Name, out var model)) + { + model = pgRender.LoadModelAndAnimations(modelPath.AsSpan(), null, true); + modelCache.Add(shell.Name, model); + } + + if (model is null) + { + ErrorReporter.Assert( + EngineAssert.FromNullOrEmpty( + [$"component='{component.Name}'", $"shell='{shell.Name}'"], + $"Cannot link component '{componentName}' to shell '{shell.Name}' because model '{modelPath}' could not be loaded.")); + return false; + } + + if (!model.IsModel) + { + ErrorReporter.Assert( + EngineAssert.FromNullOrEmpty( + [$"component='{component.Name}'", $"shell='{shell.Name}'"], + $"Cannot link component '{componentName}' to shell '{shell.Name}' because the loaded file '{modelPath}' is not a model.")); + return false; + } + + var boneIndex = model.IndexOfBone(componentName); + + if (boneIndex == -1) + return false; + component.Bone = boneIndex; + component.ParentShell = shell; + return true; + } + + private void SetDefaultFont() + { + // The code is only triggered iff at least one Text CommandbarBar component existed + if (Components.FirstOrDefault(x => x is CommandBarTextComponent or CommandBarTextButtonComponent) is null) + return; + + if (_defaultFont is null) + { + // TODO: From GameConstants + var fontName = PGConstants.DefaultUnicodeFontName; + var size = 11; + var font = fontManager.CreateFont(fontName, size, true, false, false, 1.0f); + if (font is null) + ErrorReporter.Assert(EngineAssert.FromNullOrEmpty([ToString()], $"Unable to create Default from name {fontName}")); + DefaultFont = font; + } + } + + private void SetMegaTexture() + { + // The code is only triggered iff at least one Shell CommandbarBar component existed + if (Components.FirstOrDefault(x => x is CommandBarShellComponent) is null) + return; + // Note: The tag is not used by the engine + var mtdPath = FileSystem.Path.Combine("DATA\\ART\\TEXTURES", $"{CommandBarConstants.MegaTextureBaseName}.mtd"); + using var megaTexture = GameRepository.TryOpenFile(mtdPath); + + try + { + MtdFile = megaTexture is null ? null : _mtdFileService.Load(megaTexture); + } + catch (BinaryCorruptedException e) + { + var message = $"Failed to load MTD file '{mtdPath}': {e.Message}"; + Logger?.LogError(e, message); + ErrorReporter.Assert(EngineAssert.Create(EngineAssertKind.CorruptBinary, mtdPath, [], message)); + } + + GameRepository.TextureRepository.FileExists($"{CommandBarConstants.MegaTextureBaseName}.tga", false, out _, out var actualFilePath); + MegaTextureFileName = FileSystem.Path.GetFileName(actualFilePath); + } + + private void SetComponentGroup(IEnumerable components) + { + var groupData = components + .Where(x => !string.IsNullOrEmpty(x.XmlData.Group)) + .GroupBy(x => x.XmlData.Group!, StringComparer.Ordinal); + + foreach (var grouping in groupData) + { + var group = new CommandBarComponentGroup(grouping.Key, grouping); + _groups.Add(grouping.Key, group); + foreach (var component in grouping) + component.Group = group; + } + } + + private void VerifyFilePathLength(string filePath) + { + if (filePath.Length > PGConstants.MaxCommandBarDatabaseFileName) + { + ErrorReporter.Report(new InitializationError + { + GameManager = ToString(), + Message = $"CommandBar file '{filePath}' is longer than {PGConstants.MaxCommandBarDatabaseFileName} characters." + }); + } + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/Components/CommandBarShellComponent.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/Components/CommandBarShellComponent.cs index 799040e..f649d80 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/Components/CommandBarShellComponent.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/Components/CommandBarShellComponent.cs @@ -1,4 +1,7 @@ using PG.StarWarsGame.Engine.CommandBar.Xml; +using System.Numerics; +using PG.StarWarsGame.Engine.Rendering; +using PG.StarWarsGame.Engine.Utilities; namespace PG.StarWarsGame.Engine.CommandBar.Components; @@ -10,10 +13,33 @@ public class CommandBarShellComponent : CommandBarBaseComponent public string? ModelPath { get; } + public Matrix3x4 ModelTransform { get; internal set; } = Matrix3x4.Identity; + public CommandBarShellComponent(CommandBarComponentData xmlData) : base(xmlData) { ModelName = xmlData.ModelName; if (!string.IsNullOrEmpty(ModelName)) ModelPath = $"DATA\\ART\\MODELS\\{ModelName}"; } + + internal void SetOffsetAndScale(Vector3 offset, Vector3 scale) + { + var newOffset = new Vector3(0.0f, 0.0f, 0.0f); + var newScale = new Vector3(1.0f, 1.0f, 1.0f); + if (XmlData.ModelOffsetX) + newOffset.X = offset.X; + if (XmlData.ModelOffsetY) + newOffset.Y = offset.Y; + if (XmlData.ScaleModelX) + newScale.X = scale.X; + if (XmlData.ScaleModelY) + newScale.Y = scale.Y; + + newOffset.X = PGMath.Floor(newOffset.X) + 0.5f; + newOffset.Y = PGMath.Floor(newOffset.Y) + 0.5f; + + ModelTransform = Matrix3x4.Identity + * Matrix3x4.Scale(newScale) + * Matrix3x4.CreateTranslation(newOffset); + } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/ICommandBarGameManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/ICommandBarGameManager.cs index 665d217..6c2749d 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/ICommandBarGameManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/ICommandBarGameManager.cs @@ -1,14 +1,19 @@ using System.Collections.Generic; using PG.StarWarsGame.Engine.CommandBar.Components; +using PG.StarWarsGame.Engine.GameObjects; using PG.StarWarsGame.Files.MTD.Files; namespace PG.StarWarsGame.Engine.CommandBar; public interface ICommandBarGameManager : IGameManager { - IMtdFile? MegaTextureFile { get; } + IMtdFile? MtdFile { get; } + + string? MegaTextureFileName { get; } ICollection Components { get; } IReadOnlyDictionary Groups { get; } + + bool IconExists(GameObject gameObject); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/SupportedCommandBarComponentData.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/SupportedCommandBarComponentData.cs index 7b6eeb6..388599c 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/SupportedCommandBarComponentData.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/SupportedCommandBarComponentData.cs @@ -5,6 +5,8 @@ namespace PG.StarWarsGame.Engine.CommandBar; public static class SupportedCommandBarComponentData { + // Unfortunately we cannot use EnumConversionDictionary, because EaW and use different enum values + // for the same components, so we need to maintain separate dictionaries for each engine. public static IReadOnlyDictionary GetComponentIdsForEngine(GameEngineType engineType) { return engineType switch diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/Xml/CommandBarComponentData.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/Xml/CommandBarComponentData.cs index 057745e..659130a 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/Xml/CommandBarComponentData.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/Xml/CommandBarComponentData.cs @@ -1,15 +1,14 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Numerics; using PG.Commons.Hashing; using PG.Commons.Numerics; -using PG.StarWarsGame.Engine.Xml; using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Data; namespace PG.StarWarsGame.Engine.CommandBar.Xml; -public sealed class CommandBarComponentData(string name, Crc32 crc, XmlLocationInfo location) : NamedXmlObject(name, crc, location) +public sealed class CommandBarComponentData : NamedXmlObject { public const float DefaultScale = 1.0f; public const float DefaultBlinkRate = 0.2f; @@ -17,18 +16,31 @@ public sealed class CommandBarComponentData(string name, Crc32 crc, XmlLocationI public static readonly Vector2 DefaultOffsetWidescreenValue = new(9.9999998e17f, 9.9999998e17f); public static readonly Vector4Int WhiteColor = new(255, 255, 255, 255); - public IReadOnlyList SelectedTextureNames { get; internal set; } = []; - public IReadOnlyList BlankTextureNames { get; internal set; } = []; - public IReadOnlyList IconAlternateTextureNames { get; internal set; } = []; - public IReadOnlyList MouseOverTextureNames { get; internal set; } = []; - public IReadOnlyList BarTextureNames { get; internal set; } = []; - public IReadOnlyList BarOverlayNames { get; internal set; } = []; - public IReadOnlyList AlternateFontNames { get; internal set; } = []; - public IReadOnlyList TooltipTexts { get; internal set; } = []; - public IReadOnlyList LowerEffectTextureNames { get; internal set; } = []; - public IReadOnlyList UpperEffectTextureNames { get; internal set; } = []; - public IReadOnlyList OverlayTextureNames { get; internal set; } = []; - public IReadOnlyList Overlay2TextureNames { get; internal set; } = []; + internal readonly List SelectedTextureNamesInternal = []; + internal readonly List BlankTextureNamesInternal = []; + internal readonly List IconAlternateTextureNamesInternal = []; + internal readonly List MouseOverTextureNamesInternal = []; + internal readonly List BarTextureNamesInternal = []; + internal readonly List BarOverlayNamesInternal = []; + internal readonly List AlternateFontNamesInternal = []; + internal readonly List TooltipTextsInternal = []; + internal readonly List LowerEffectTextureNamesInternal = []; + internal readonly List UpperEffectTextureNamesInternal = []; + internal readonly List OverlayTextureNamesInternal = []; + internal readonly List Overlay2TextureNamesInternal = []; + + public IReadOnlyList SelectedTextureNames { get; } + public IReadOnlyList BlankTextureNames { get; } + public IReadOnlyList IconAlternateTextureNames { get; } + public IReadOnlyList MouseOverTextureNames { get; } + public IReadOnlyList BarTextureNames { get; } + public IReadOnlyList BarOverlayNames { get; } + public IReadOnlyList AlternateFontNames { get; } + public IReadOnlyList TooltipTexts { get; } + public IReadOnlyList LowerEffectTextureNames { get; } + public IReadOnlyList UpperEffectTextureNames { get; } + public IReadOnlyList OverlayTextureNames { get; } + public IReadOnlyList Overlay2TextureNames { get; } public string? IconTextureName { get; internal set; } public string? DisabledTextureName { get; internal set; } @@ -128,22 +140,26 @@ public sealed class CommandBarComponentData(string name, Crc32 crc, XmlLocationI public Vector4Int? TextColor2 { get; internal set; } public Vector4Int? MaxBarColor { get; internal set; } = WhiteColor; - internal override void CoerceValues() + public CommandBarComponentData(string name, Crc32 crc, XmlLocationInfo location) : base(name, crc, location) { - base.CoerceValues(); - if (AlternateFontNames.Count == 0) - return; - var newFontNames = new string[AlternateFontNames.Count]; - for (var i = 0; i < AlternateFontNames.Count; i++) - { - var current = AlternateFontNames[i]; + SelectedTextureNames = new ReadOnlyCollection(SelectedTextureNamesInternal); + BlankTextureNames = new ReadOnlyCollection(BlankTextureNamesInternal); + IconAlternateTextureNames = new ReadOnlyCollection(IconAlternateTextureNamesInternal); + MouseOverTextureNames = new ReadOnlyCollection(MouseOverTextureNamesInternal); + BarTextureNames = new ReadOnlyCollection(BarTextureNamesInternal); + BarOverlayNames = new ReadOnlyCollection(BarOverlayNamesInternal); + AlternateFontNames = new ReadOnlyCollection(AlternateFontNamesInternal); + TooltipTexts = new ReadOnlyCollection(TooltipTextsInternal); + LowerEffectTextureNames = new ReadOnlyCollection(LowerEffectTextureNamesInternal); + UpperEffectTextureNames = new ReadOnlyCollection(UpperEffectTextureNamesInternal); + OverlayTextureNames = new ReadOnlyCollection(OverlayTextureNamesInternal); + Overlay2TextureNames = new ReadOnlyCollection(Overlay2TextureNamesInternal); + } - if (current.AsSpan().IndexOf('_') != -1) - newFontNames[i] = current.Replace('_', ' '); - else - newFontNames[i] = current; - } - AlternateFontNames = new ReadOnlyCollection(newFontNames); + internal void FixupValues() + { + for (var i = 0; i < AlternateFontNamesInternal.Count; i++) + AlternateFontNamesInternal[i] = AlternateFontNamesInternal[i].Replace('_', ' '); } } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Commons/MultiNameReferenceList.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Commons/MultiNameReferenceList.cs new file mode 100644 index 0000000..ff828a4 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Commons/MultiNameReferenceList.cs @@ -0,0 +1,57 @@ +using System.Collections; +using System.Collections.Generic; + +namespace PG.StarWarsGame.Engine.Commons; + +public class MultiNameReferenceList : IReadOnlyList +{ + private bool _replace; + private readonly List _list = []; + + public string this[int index] => _list[index]; + + public int Count => _list.Count; + + public MultiNameReferenceList() + { + } + + public MultiNameReferenceList(MultiNameReferenceList list) + { + foreach (var name in list) + _list.Add(name); + + _replace = true; + } + + internal void AddRange(IEnumerable names) + { + if (_replace) + Clear(); + _replace = false; + _list.AddRange(names); + } + + internal void Add(string name) + { + if (_replace) + Clear(); + _replace = false; + _list.Add(name); + } + + internal void Clear() + { + _list.Clear(); + } + + public IEnumerator GetEnumerator() + { + return _list.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/EngineAssert.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/EngineAssert.cs index 5cc4c49..38a07a9 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/EngineAssert.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/EngineAssert.cs @@ -9,7 +9,7 @@ namespace PG.StarWarsGame.Engine.ErrorReporting; public sealed class EngineAssert { - private static readonly string ThisNameSpace = typeof(EngineAssert).Namespace!; + private static readonly string ThisNamespace = typeof(EngineAssert).Namespace!; private const string NullLiteral = "NULL"; public string Value { get; } @@ -66,7 +66,7 @@ internal static EngineAssert Create(EngineAssertKind kind, object? value, IEnume { var frame = trace.GetFrame(i); var method = frame.GetMethod(); - if (method.DeclaringType is null || method.DeclaringType.Namespace?.Equals(ThisNameSpace) == false) + if (method.DeclaringType is null || method.DeclaringType.Namespace?.Equals(ThisNamespace) == false) return frame; } return null; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/EngineAssertKind.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/EngineAssertKind.cs index e3a2d69..f2d1ccd 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/EngineAssertKind.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/EngineAssertKind.cs @@ -7,4 +7,5 @@ public enum EngineAssertKind InvalidValue, CorruptBinary, FileNotFound, + DuplicateEntry } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/GameEngineErrorReporter.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/GameEngineErrorReporter.cs deleted file mode 100644 index 0030344..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/GameEngineErrorReporter.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace PG.StarWarsGame.Engine.ErrorReporting; - -public abstract class GameEngineErrorReporter : IGameEngineErrorReporter -{ - public virtual void Report(XmlError error) - { - } - - public virtual void Report(InitializationError error) - { - } - - public virtual void Assert(EngineAssert assert) - { - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/GameEngineErrorReporterWrapper.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/GameEngineErrorReporterWrapper.cs index a636aba..a8280cc 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/GameEngineErrorReporterWrapper.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/GameEngineErrorReporterWrapper.cs @@ -1,50 +1,26 @@ using System; using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; namespace PG.StarWarsGame.Engine.ErrorReporting; -internal sealed class GameEngineErrorReporterWrapper : XmlErrorReporter, IGameEngineErrorReporter +internal sealed class GameEngineErrorReporterWrapper(IGameEngineErrorReporter? errorReporter) + : XmlErrorReporter, IGameEngineErrorReporter { internal event EventHandler? InitializationError; - private readonly IGameEngineErrorReporter? _errorReporter; - - public GameEngineErrorReporterWrapper(IGameEngineErrorReporter? errorReporter) - { - if (errorReporter is null) - return; - _errorReporter = errorReporter; - } - - public void Report(XmlError error) + public override void Report(XmlError error) { - _errorReporter?.Report(error); + errorReporter?.Report(error); } public void Report(InitializationError error) { InitializationError?.Invoke(this, error); - _errorReporter?.Report(error); + errorReporter?.Report(error); } public void Assert(EngineAssert assert) { - _errorReporter?.Assert(assert); - } - - public override void Report(IPetroglyphXmlParser parser, XmlParseErrorEventArgs error) - { - if (_errorReporter is null) - return; - - Report(new XmlError - { - FileLocation = error.Location, - Parser = parser, - Message = error.Message, - ErrorKind = error.ErrorKind, - Element = error.Element - }); + errorReporter?.Assert(assert); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/IGameEngineErrorReporter.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/IGameEngineErrorReporter.cs index 1294fd1..933d3b5 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/IGameEngineErrorReporter.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/IGameEngineErrorReporter.cs @@ -1,9 +1,9 @@ -namespace PG.StarWarsGame.Engine.ErrorReporting; +using PG.StarWarsGame.Files.XML.ErrorHandling; -public interface IGameEngineErrorReporter -{ - void Report(XmlError error); +namespace PG.StarWarsGame.Engine.ErrorReporting; +public interface IGameEngineErrorReporter : IXmlParserErrorReporter +{ void Report(InitializationError error); void Assert(EngineAssert assert); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/XmlError.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/XmlError.cs deleted file mode 100644 index 83e6e9f..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/ErrorReporting/XmlError.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Xml.Linq; -using PG.StarWarsGame.Files.XML; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Engine.ErrorReporting; - -public sealed class XmlError -{ - public required XmlLocationInfo FileLocation { get; init; } - - public required IPetroglyphXmlParser Parser { get; init; } - - public XElement? Element { get; init; } - - public required XmlParseErrorKind ErrorKind { get; init; } - - public required string Message { get; init; } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/FocHardcodedConstants.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/FocHardcodedConstants.cs deleted file mode 100644 index d7eb4af..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/FocHardcodedConstants.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; - -namespace PG.StarWarsGame.Engine; - -public static class FocHardcodedConstants -{ - /// - /// These models / particles are hardcoded into StarWarsG.exe. - /// - public static IList HardcodedModels { get; } = new List - { - "i_tutorial_arrow.alo", - "p_hero_empire_fx.alo", - "i_tactical_corrupt.alo", - "p_icon_corrupt.alo", - "w_planet_select_neutral.alo", - "i_game_arrow.alo", - "i_galactic_radar.alo", - "W_TextScroll.alo" - }; -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/GameConstants.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/GameConstants.cs index 1c36061..3874e07 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/GameConstants.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/GameConstants.cs @@ -6,7 +6,10 @@ namespace PG.StarWarsGame.Engine.GameConstants; -internal class GameConstants(GameRepository repository, GameEngineErrorReporterWrapper errorReporter, IServiceProvider serviceProvider) +internal class GameConstants( + GameRepository repository, + GameEngineErrorReporterWrapper errorReporter, + IServiceProvider serviceProvider) : GameManagerBase(repository, errorReporter, serviceProvider), IGameConstants { protected override Task InitializeCoreAsync(CancellationToken token) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/GameConstantsXml.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/GameConstantsXml.cs index e98ed52..5eae395 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/GameConstantsXml.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameConstants/GameConstantsXml.cs @@ -1,3 +1,6 @@ -namespace PG.StarWarsGame.Engine.GameConstants; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Data; -public class GameConstantsXml; \ No newline at end of file +namespace PG.StarWarsGame.Engine.GameConstants; + +public class GameConstantsXml(XmlLocationInfo location) : XmlObject(location); \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameManagerBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameManagerBase.cs index 607121d..c3dd55d 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameManagerBase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameManagerBase.cs @@ -12,7 +12,10 @@ namespace PG.StarWarsGame.Engine; -internal abstract class GameManagerBase(GameRepository repository, GameEngineErrorReporterWrapper errorReporter, IServiceProvider serviceProvider) +internal abstract class GameManagerBase( + GameRepository repository, + GameEngineErrorReporterWrapper errorReporter, + IServiceProvider serviceProvider) : GameManagerBase(repository, errorReporter, serviceProvider), IGameManager { protected readonly FrugalValueListDictionary NamedEntries = new(); @@ -40,11 +43,17 @@ internal abstract class GameManagerBase protected readonly GameEngineErrorReporterWrapper ErrorReporter; public bool IsInitialized => _initialized; + + public GameEngineType EngineType { get; } - protected GameManagerBase(GameRepository repository, GameEngineErrorReporterWrapper errorReporter, IServiceProvider serviceProvider) + protected GameManagerBase( + GameRepository repository, + GameEngineErrorReporterWrapper errorReporter, + IServiceProvider serviceProvider) { GameRepository = repository ?? throw new ArgumentNullException(nameof(repository)); ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + EngineType = repository.EngineType; Logger = serviceProvider.GetService()?.CreateLogger(GetType()); FileSystem = serviceProvider.GetRequiredService(); ErrorReporter = errorReporter ?? throw new ArgumentNullException(nameof(errorReporter)); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObject.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObject.cs index de7ed01..89823df 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObject.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObject.cs @@ -1,26 +1,32 @@ -using System; +using PG.Commons.Hashing; +using PG.StarWarsGame.Engine.Commons; +using PG.StarWarsGame.Engine.Utilities; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Data; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.Xml; -using PG.StarWarsGame.Files.XML; +using System.Diagnostics; namespace PG.StarWarsGame.Engine.GameObjects; +[DebuggerDisplay("{Name} ({ClassificationName})")] public sealed class GameObject : NamedXmlObject { - internal GameObject(string type, string name, Crc32 nameCrc, GameObjectType estimatedType, XmlLocationInfo location) - : base(name, nameCrc, location) - { - Type = type ?? throw new ArgumentNullException(nameof(type)); - EstimatedType = estimatedType; - LandTerrainModelMapping = new ReadOnlyDictionary(InternalLandTerrainModelMapping); - } + internal readonly List<(string terrain, string model)> InternalLandTerrainModelMapping = []; + + internal int Id { get; } + + public int Index { get; } - public string Type { get; } + public string ClassificationName { get; } - public GameObjectType EstimatedType { get; } + public string? VariantOfExistingTypeName { get; internal set; } + public GameObject? VariantOfExistingType { get; internal set; } + + public bool IsLoadingComplete { get; internal set; } + public string? GalacticModel { get; internal set; } public string? DestroyedGalacticModel { get; internal set; } @@ -29,8 +35,6 @@ internal GameObject(string type, string name, Crc32 nameCrc, GameObjectType esti public string? SpaceModel { get; internal set; } - public string? TacticalModel { get; internal set; } - public string? GalacticFleetOverrideModel { get; internal set; } public string? GuiModel { get; internal set; } @@ -39,46 +43,57 @@ internal GameObject(string type, string name, Crc32 nameCrc, GameObjectType esti public string? LandAnimOverrideModel { get; internal set; } - public string? XxxSpaceModeModel { get; internal set; } + public string SpaceAnimOverrideModel { get; internal set; } public string? DamagedSmokeAssetModel { get; internal set; } - public IReadOnlyDictionary LandTerrainModelMapping { get; } + public IReadOnlyList<(string terrain, string model)> LandTerrainModelMappingValues { get; } + + public string? IconName { get; internal set; } + + public MultiNameReferenceList GroundCompanyUnits { get; internal set; } = []; - internal Dictionary InternalLandTerrainModelMapping { get; } = new(StringComparer.OrdinalIgnoreCase); + internal GameObject( + string name, + string classification, + Crc32 nameCrc, + int index, + XmlLocationInfo location) + : base(name, nameCrc, location) + { + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(index), "Index must be greater than 0."); + Index = index; + Id = (int)nameCrc; + ClassificationName = classification ?? throw new ArgumentNullException(nameof(classification)); + LandTerrainModelMappingValues = new ReadOnlyCollection<(string, string)>(InternalLandTerrainModelMapping); + } - /// - /// Gets all model files (including particles) the game object references. - /// - public IEnumerable Models + internal void ApplyBaseType(GameObject baseType) { - get - { - var models = new HashSet(StringComparer.OrdinalIgnoreCase); - AddNotEmpty(models, GalacticModel); - AddNotEmpty(models, DestroyedGalacticModel); - AddNotEmpty(models, LandModel); - AddNotEmpty(models, SpaceModel); - AddNotEmpty(models, TacticalModel); - AddNotEmpty(models, GalacticFleetOverrideModel); - AddNotEmpty(models, GuiModel); - AddNotEmpty(models, ModelName); - AddNotEmpty(models, LandAnimOverrideModel, s => s.EndsWith(".alo", StringComparison.OrdinalIgnoreCase)); - AddNotEmpty(models, XxxSpaceModeModel); - AddNotEmpty(models, DamagedSmokeAssetModel); - foreach (var model in InternalLandTerrainModelMapping.Values) - models.Add(model); - - return models; - } + // The following properties must not be inherited from the base type: + // ID, CRC, Name, Location, IsLoadingComplete, ClassificationName and VariantOfExistingType[Name], LuaScript + + // TODO + GalacticModel = baseType.GalacticModel; + DestroyedGalacticModel = baseType.DestroyedGalacticModel; + LandModel = baseType.LandModel; + SpaceModel = baseType.SpaceModel; + GalacticFleetOverrideModel = baseType.GalacticFleetOverrideModel; + GuiModel = baseType.GuiModel; + ModelName = baseType.ModelName; + LandAnimOverrideModel = baseType.LandAnimOverrideModel; + SpaceAnimOverrideModel = baseType.SpaceAnimOverrideModel; + DamagedSmokeAssetModel = baseType.DamagedSmokeAssetModel; + InternalLandTerrainModelMapping.ClearAddRange(baseType.InternalLandTerrainModelMapping); + IconName = baseType.IconName; + GroundCompanyUnits = new(baseType.GroundCompanyUnits); } - private static void AddNotEmpty(ISet set, string? value, Predicate? predicate = null) + internal void PostLoadFixup() { - if (value is null) - return; - if (predicate is null || predicate(value)) - set.Add(value); + // This method is different than the fixup that is performed after parsing the object. + // IDK why there are two separate fixups. This fixup performs more value coercions } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectType.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectType.cs deleted file mode 100644 index 0b84517..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectType.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace PG.StarWarsGame.Engine.GameObjects; - -public enum GameObjectType -{ - Unknown = 0, - CinematicObject = 1, - Container = 2, - GenericHeroUnit = 3, - GroundBase = 4, - GroundBuildable = 5, - GroundCompany = 6, - GroundInfantry = 7, - GroundStructure = 8, - GroundVehicle = 9, - HeroCompany = 10, - HeroUnit = 11, - IndigenousUnit = 12, - LandBombingUnit = 13, - LandPrimarySkydome = 14, - LandSecondarySkydome = 15, - Marker = 16, - MiscObject = 17, - MobileDefenseUnit = 18, - MultiplayerStructureMarker = 19, - Particle = 20, - Planet = 21, - Projectile = 22, - Prop = 23, - ScriptMarker = 24, - SecondaryStructure = 25, - SlaveCompany = 26, - SlaveUnit = 27, - SpaceBuildable = 28, - SpacePrimarySkydome = 29, - SpaceProp = 30, - SpaceSecondarySkydome = 31, - SpaceUnit = 32, - SpecialEffect = 33, - SpecialStructure = 34, - Squadron = 35, - StarBase = 36, - TechBuilding = 37, - TransportUnit = 38, - UniqueUint = 39, - UpgradeUnit = 40 -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectTypeGameManager.Initialization.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectTypeGameManager.Initialization.cs new file mode 100644 index 0000000..9b1437a --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectTypeGameManager.Initialization.cs @@ -0,0 +1,166 @@ +using System; +using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine.ErrorReporting; +using PG.StarWarsGame.Engine.Xml; +using PG.StarWarsGame.Engine.Xml.Parsers; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AnakinRaW.CommonUtilities.FileSystem; + +namespace PG.StarWarsGame.Engine.GameObjects; + +internal partial class GameObjectTypeGameManager +{ + protected override async Task InitializeCoreAsync(CancellationToken token) + { + Logger?.LogInformation("Parsing GameObjects..."); + await Task.Run(ParseGameObjectDatabases, token); + } + + private void ParseGameObjectDatabases() + { + var gameParser = new PetroglyphStarWarsGameXmlParser(GameRepository, + new PetroglyphStarWarsGameXmlParseSettings + { + GameManager = ToString(), + InvalidFilesListXmlFailsInitialization = true, + InvalidObjectXmlFailsInitialization = false, + }, ServiceProvider, ErrorReporter); + + var xmlFileList = gameParser.ParseFileList(@"DATA\XML\GAMEOBJECTFILES.XML").Files + .Select(x => FileSystem.Path.Combine(@".\DATA\XML\", x)) + .Where(VerifyFilePathLength) + .ToList(); + + var gameObjectFileParser = new GameObjectFileParser(EngineType, ServiceProvider, ErrorReporter); + + var allLoaded = false; + + // This also acts a guard against infinite loops in case of unexpected circular dependencies or + // when a unit declares itself as its own + for (var passNumber = 0; !allLoaded && passNumber < 10; passNumber++) + { + Logger?.LogDebug("***** Parsing game object types - pass {PassNumber} *****", passNumber); + + if (passNumber != 0) + gameObjectFileParser.OverlayLoad = true; + + foreach (var gameObjectXmlFile in xmlFileList) + { + if (passNumber == 0) + { + try + { + gameObjectFileParser.GameObjectParsed += OnGameObjectParsed!; + ParseSingleGameObjectFile(gameObjectXmlFile, gameParser, gameObjectFileParser); + } + finally + { + gameObjectFileParser.GameObjectParsed -= OnGameObjectParsed!; + } + } + else + { + foreach (var gameObject in _gameObjects) + { + if (!gameObject.IsLoadingComplete && IsSameFile(gameObject.Location.XmlFile, gameObjectXmlFile)) + ParseSingleGameObjectFile(gameObjectXmlFile, gameParser, gameObjectFileParser); + } + } + } + + + PostLoadFixup(); + //SFXEventReferenceClass::Static_Post_Load_Fixup(); + //SpeechEventReferenceClass::Static_Post_Load_Fixup(); + //MusicEventReferenceClass::Static_Post_Load_Fixup(); + //FactionReferenceClass::Static_Post_Load_Fixup(); + //... + + allLoaded = true; + + foreach (var gameObject in _gameObjects) + { + gameObject.PostLoadFixup(); + if (!gameObject.IsLoadingComplete) + allLoaded = false; + } + } + + // TODO: The Engine is now asserting some SFX files of all types + } + + private void PostLoadFixup() + { + // In the engine, this is the so-called static post load fixup. + foreach (var gameObject in _gameObjects) + { + var baseTypeName = gameObject.VariantOfExistingTypeName; + if (string.IsNullOrEmpty(baseTypeName) || baseTypeName.Equals("None", StringComparison.OrdinalIgnoreCase)) + continue; + + // At this point the engine would assert, if the base type was not found. + // We do not, because the game reports this error for every iteration of the loading loop, + // which would bloat error logs. + var baseNameHash = _hashingService.GetCrc32Upper(baseTypeName, PGConstants.DefaultPGEncoding); + NamedEntries.TryGetFirstValue(baseNameHash, out var baseType); + gameObject.VariantOfExistingType = baseType; + } + } + + private bool IsSameFile(string filePathA, string filePathB) + { + return FileSystem.Path.AreEqual(filePathA, filePathB); + } + + private void OnGameObjectParsed(object sender, GameObjectParsedEventArgs e) + { + if (!e.Unique) + { + var entries = NamedEntries.GetValues(e.GameObject.Crc32) + .Select(x => x.Name); + ErrorReporter.Assert(EngineAssert.Create( + EngineAssertKind.DuplicateEntry, + e.GameObject.Crc32, entries, + $"Error: Game object type {e.GameObject.Name} is defined multiple times.")); + } + + if (NamedEntries.ValueCount >= 0x10000) + { + ErrorReporter.Assert( + EngineAssert.Create( + EngineAssertKind.ValueOutOfRange, + NamedEntries.ValueCount, + [ToString()], + "Too many game object types defined.")); + } + + _gameObjects.Add(e.GameObject); + } + + private void ParseSingleGameObjectFile( + string file, + PetroglyphStarWarsGameXmlParser gameParser, + GameObjectFileParser gameObjectFileParser) + { + gameParser.ParseObjectsFromContainerFile(file, gameObjectFileParser, NamedEntries); + } + + private bool VerifyFilePathLength(string filePath) + { + if (filePath.Length > PGConstants.MaxGameObjectDatabaseFileName) + { + // Technically this is an assert in the engine, but in Release Mode, the game CTDs. + // Thus, we rank this as an initialization error. + ErrorReporter.Report(new InitializationError + { + GameManager = ToString(), + Message = $"Game object file '{filePath}' is longer than {PGConstants.MaxGameObjectDatabaseFileName} characters." + }); + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectTypeGameManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectTypeGameManager.cs index 407d9c5..14aba19 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectTypeGameManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectTypeGameManager.cs @@ -1,39 +1,85 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; +using PG.Commons.Hashing; using PG.StarWarsGame.Engine.ErrorReporting; using PG.StarWarsGame.Engine.IO.Repositories; -using PG.StarWarsGame.Engine.Xml.Parsers; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Microsoft.Extensions.DependencyInjection; namespace PG.StarWarsGame.Engine.GameObjects; -internal class GameObjectTypeGameManager(GameRepository repository, GameEngineErrorReporterWrapper errorReporter, IServiceProvider serviceProvider) - : GameManagerBase(repository, errorReporter, serviceProvider), IGameObjectTypeGameManager +internal partial class GameObjectTypeGameManager : GameManagerBase, IGameObjectTypeGameManager { - protected override async Task InitializeCoreAsync(CancellationToken token) - { - Logger?.LogInformation("Parsing GameObjects..."); - - var contentParser = new XmlContainerContentParser(ServiceProvider, ErrorReporter); + private readonly List _gameObjects; + private readonly ICrc32HashingService _hashingService; - await Task.Run(() => contentParser.ParseEntriesFromFileListXml( - "DATA\\XML\\GAMEOBJECTFILES.XML", - GameRepository, - ".\\DATA\\XML", - NamedEntries, - VerifyFilePathLength), token); + public GameObjectTypeGameManager( + GameRepository repository, + GameEngineErrorReporterWrapper errorReporter, + IServiceProvider serviceProvider) + : base(repository, errorReporter, serviceProvider) + { + _hashingService = serviceProvider.GetRequiredService(); + _gameObjects = []; + GameObjects = new ReadOnlyCollection(_gameObjects); + } + + public IReadOnlyList GameObjects + { + get + { + ThrowIfNotInitialized(); + return field; + } } - private void VerifyFilePathLength(string filePath) + public IEnumerable GetModels(GameObject gameObject) { - if (filePath.Length > PGConstants.MaxGameObjectDatabaseFileName) + var models = new HashSet(StringComparer.OrdinalIgnoreCase); + AddNotEmpty(gameObject.GalacticModel); + AddNotEmpty(gameObject.DestroyedGalacticModel); + AddNotEmpty(gameObject.LandModel); + AddNotEmpty(gameObject.SpaceModel); + AddNotEmpty(gameObject.GalacticFleetOverrideModel); + AddNotEmpty(gameObject.GuiModel); + AddNotEmpty(gameObject.ModelName); + AddNotEmpty(gameObject.LandAnimOverrideModel); + AddNotEmpty(gameObject.SpaceAnimOverrideModel); + AddNotEmpty(gameObject.DamagedSmokeAssetModel); + AddTerrainMappingModels(); + return models; + + + void AddNotEmpty(string? value, Predicate? predicate = null) + { + if (string.IsNullOrEmpty(value)) + return; + if (predicate is null || predicate(value)) + models.Add(value); + } + + + void AddTerrainMappingModels() { - ErrorReporter.Report(new InitializationError + var visitedEnvTypes = new HashSet(); + foreach (var mapping in gameObject.LandTerrainModelMappingValues) { - GameManager = ToString(), - Message = $"Game object file '{filePath}' is longer than {PGConstants.MaxGameObjectDatabaseFileName} characters." - }); + if (MapEnvironmentTypeConversion.ConversionDictionary.TryStringToEnum(mapping.terrain, out var envType)) + { + // The engine only uses the first model for each environment type. + if (visitedEnvTypes.Add(envType)) + AddNotEmpty(mapping.model); + } + } } } + + public GameObject? FindObjectType(string? name) + { + if (string.IsNullOrEmpty(name)) + return null; + var nameCrc = _hashingService.GetCrc32Upper(name, PGConstants.DefaultPGEncoding); + NamedEntries.TryGetFirstValue(nameCrc, out var gameObject); + return gameObject; + } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/IGameObjectTypeGameManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/IGameObjectTypeGameManager.cs index f31273f..ef72db3 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/IGameObjectTypeGameManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/IGameObjectTypeGameManager.cs @@ -1,3 +1,25 @@ -namespace PG.StarWarsGame.Engine.GameObjects; +using System.Collections.Generic; -public interface IGameObjectTypeGameManager : IGameManager; \ No newline at end of file +namespace PG.StarWarsGame.Engine.GameObjects; + +public interface IGameObjectTypeGameManager : IGameManager +{ + // List represent XML load order + IReadOnlyList GameObjects { get; } + + /// + /// Retrieves the collection of model and particle names directly associated with the specified . + /// + /// + /// Death Clones, Projectiles, Hardpoints, etc. are not included in the returned collection. + /// + /// + /// The for which to retrieve the associated model names. + /// + /// + /// An containing the names of the models associated with the specified . + /// + IEnumerable GetModels(GameObject gameObject); + + GameObject? FindObjectType(string name); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/MapEnvironmentType.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/MapEnvironmentType.cs new file mode 100644 index 0000000..2faf565 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/MapEnvironmentType.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using PG.StarWarsGame.Engine.Xml; + +namespace PG.StarWarsGame.Engine.GameObjects; + +// TODO: Not sure, this is the correct namespace + +public enum MapEnvironmentType +{ + Temperate = 0x0, + Arctic = 0x1, + Desert = 0x2, + Forest = 0x3, + Swamp = 0x4, + Volcanic = 0x5, + Urban = 0x6, + Space = 0x7, +} + +// TODO: To separate GameManager that holds these Conversion instances +public static class MapEnvironmentTypeConversion +{ + public static readonly EnumConversionDictionary ConversionDictionary = new([ + new KeyValuePair("Temperate", MapEnvironmentType.Temperate), + new KeyValuePair("Arctic", MapEnvironmentType.Arctic), + new KeyValuePair("Desert", MapEnvironmentType.Desert), + new KeyValuePair("Forest", MapEnvironmentType.Forest), + new KeyValuePair("Swamp", MapEnvironmentType.Swamp), + new KeyValuePair("Volcanic", MapEnvironmentType.Volcanic), + new KeyValuePair("Urban", MapEnvironmentType.Urban), + new KeyValuePair("Space", MapEnvironmentType.Space) + ]); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/ComponentTextureEntry.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/ComponentTextureEntry.cs index f1ffb04..6d188e7 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/ComponentTextureEntry.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/ComponentTextureEntry.cs @@ -1,5 +1,8 @@ -namespace PG.StarWarsGame.Engine.GuiDialog; +using System.Diagnostics; +namespace PG.StarWarsGame.Engine.GuiDialog; + +[DebuggerDisplay("Type:{ComponentType}, Texture: {Texture}")] public readonly struct ComponentTextureEntry(GuiComponentType componentType, string texture, bool isOverride) { public string Texture { get; } = texture; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/ExtensionMethods.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/ExtensionMethods.cs new file mode 100644 index 0000000..dff28af --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/ExtensionMethods.cs @@ -0,0 +1,22 @@ +namespace PG.StarWarsGame.Engine.GuiDialog; + +public static class ExtensionMethods +{ + extension(GuiComponentType componentType) + { + public bool IsButton() + { + return componentType <= GuiComponentType.ButtonRightDisabled; + } + + public bool SupportsSpecialTextureMode() + { + return componentType is GuiComponentType.ButtonMiddle + or GuiComponentType.ButtonMiddleMouseOver + or GuiComponentType.ButtonMiddlePressed + or GuiComponentType.ButtonMiddleDisabled + or GuiComponentType.Scanlines + or GuiComponentType.FrameBackground; + } + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager.cs index 879c16f..d6b4ca9 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using PG.Commons.Hashing; @@ -13,21 +14,33 @@ namespace PG.StarWarsGame.Engine.GuiDialog; -internal partial class GuiDialogGameManager(GameRepository repository, GameEngineErrorReporterWrapper errorReporter, IServiceProvider serviceProvider) +internal partial class GuiDialogGameManager( + GameRepository repository, + GameEngineErrorReporterWrapper errorReporter, + IServiceProvider serviceProvider) : GameManagerBase(repository, errorReporter, serviceProvider), IGuiDialogManager { private readonly IMtdFileService _mtdFileService = serviceProvider.GetRequiredService(); private readonly ICrc32HashingService _hashingService = serviceProvider.GetRequiredService(); - // Unlike other strings for this game, the component's (aka gadget) name is case-sensitive. - private readonly Dictionary> _perComponentTextures = new(StringComparer.Ordinal); - private readonly Dictionary _defaultTextures = new(); - private ReadOnlyDictionary _defaultTexturesRo = null!; - private bool _megaTextureExists; private string? _megaTextureFileName; - + [field: MaybeNull, AllowNull] + private ReadOnlyDictionary> PerComponentTextures + { + get + { + ThrowIfNotInitialized(); + return field!; + } + set + { + ThrowIfAlreadyInitialized(); + field = value; + } + } + public IMtdFile? MtdFile { get @@ -61,7 +74,7 @@ public IReadOnlyCollection Components get { ThrowIfNotInitialized(); - return _perComponentTextures.Keys; + return PerComponentTextures.Keys!; } } @@ -70,14 +83,19 @@ public IReadOnlyDictionary DefaultTextu get { ThrowIfNotInitialized(); - return _defaultTexturesRo; + return field!; + } + internal set + { + ThrowIfAlreadyInitialized(); + field = value; } } public IReadOnlyDictionary GetTextureEntries(string component, out bool componentExist) { - if (!_perComponentTextures.TryGetValue(component, out var textures)) + if (!PerComponentTextures.TryGetValue(component, out var textures)) { Logger?.LogDebug("The component '{Component}' has no overrides. Using default textures.", component); componentExist = false; @@ -85,15 +103,15 @@ public IReadOnlyDictionary GetTextureEn } componentExist = true; - return new ReadOnlyDictionary(textures); + return textures; } public bool TryGetTextureEntry(string component, GuiComponentType key, out ComponentTextureEntry texture) { - if (!_perComponentTextures.TryGetValue(component, out var textures)) + if (!PerComponentTextures.TryGetValue(component, out var textures)) { Logger?.LogDebug("The component '{Component}' has no overrides. Using default textures.", component); - textures = _defaultTextures; + textures = DefaultTextureEntries; } return textures.TryGetValue(key, out texture); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager_Initialization.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager_Initialization.cs index c3814ea..2bac738 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager_Initialization.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager_Initialization.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading; @@ -8,13 +7,13 @@ using Microsoft.Extensions.Logging; using PG.StarWarsGame.Engine.ErrorReporting; using PG.StarWarsGame.Engine.GuiDialog.Xml; -using PG.StarWarsGame.Engine.Xml.Parsers.File; -using PG.StarWarsGame.Engine.Xml.Tags; +using PG.StarWarsGame.Engine.Xml; +using PG.StarWarsGame.Engine.Xml.Parsers; using PG.StarWarsGame.Files.Binary; namespace PG.StarWarsGame.Engine.GuiDialog; -partial class GuiDialogGameManager +internal partial class GuiDialogGameManager { public const int MegaTextureMaxFilePathLength = 255; @@ -22,24 +21,14 @@ protected override Task InitializeCoreAsync(CancellationToken token) { return Task.Run(() => { - var guiDialogParser = new GuiDialogParser(ServiceProvider, ErrorReporter); - - _defaultTexturesRo = new ReadOnlyDictionary(_defaultTextures); - - Logger?.LogInformation("Parsing GuiDialogs..."); - using var fileStream = GameRepository.TryOpenFile("DATA\\XML\\GUIDIALOGS.XML"); - - if (fileStream is null) - { - ErrorReporter.Report(new InitializationError + var engineParser = new PetroglyphStarWarsGameXmlParser(GameRepository, + new PetroglyphStarWarsGameXmlParseSettings { GameManager = ToString(), - Message = "Unable to find GuiDialogs.xml" - }); - return; - } + InvalidObjectXmlFailsInitialization = true + }, ServiceProvider, ErrorReporter); - var guiDialogs = guiDialogParser.ParseFile(fileStream); + var guiDialogs = engineParser.ParseFile("DATA\\XML\\GUIDIALOGS.XML", new GuiDialogParser(ServiceProvider, ErrorReporter)); if (guiDialogs is null) { ErrorReporter.Report(new InitializationError @@ -50,10 +39,8 @@ protected override Task InitializeCoreAsync(CancellationToken token) return; } - GuiDialogsXml = guiDialogs; - InitializeTextures(guiDialogs.TextureData); - + GuiDialogsXml = guiDialogs; }, token); } @@ -62,7 +49,8 @@ private void InitializeTextures(GuiDialogsXmlTextureData textureData) InitializeMegaTextures(textureData); var textures = textureData.Textures; - + + IReadOnlyDictionary defaultTextures; if (textures.Count == 0) { ErrorReporter.Report(new InitializationError @@ -70,47 +58,70 @@ private void InitializeTextures(GuiDialogsXmlTextureData textureData) GameManager = ToString(), Message = "No Textures defined in GuiDialogs.xml" }); + + defaultTextures = new ReadOnlyDictionary( + new Dictionary()); } else { - var defaultCandidate = textures.First(); - // Regardless of its name, the game treats the first entry as default. - var defaultTextures = InitializeComponentTextures(defaultCandidate, true, out var invalidKeys); - foreach (var entry in defaultTextures) - _defaultTextures.Add(entry.Key, entry.Value); + var defaultCandidate = textures.First(); + defaultTextures = InitializeComponentTextures(defaultCandidate, null, out var invalidKeys); + ReportInvalidComponent(in invalidKeys); } + var perComponentTextures = new Dictionary>(); + foreach (var componentTextureData in textures.Skip(1)) { // The game only uses the *first* entry. - if (_perComponentTextures.ContainsKey(componentTextureData.Component)) + if (perComponentTextures.ContainsKey(componentTextureData.Component)) continue; - _perComponentTextures.Add(componentTextureData.Component, InitializeComponentTextures(componentTextureData, false, out var invalidKeys)); + perComponentTextures.Add( + componentTextureData.Component, + InitializeComponentTextures(componentTextureData, defaultTextures, out var invalidKeys)); + ReportInvalidComponent(in invalidKeys); } + + DefaultTextureEntries = defaultTextures; + PerComponentTextures = + new ReadOnlyDictionary>( + perComponentTextures); } - private Dictionary InitializeComponentTextures(XmlComponentTextureData textureData, bool isDefaultComponent, out FrugalList invalidKeys) + private ReadOnlyDictionary InitializeComponentTextures( + XmlComponentTextureData textureData, + IReadOnlyDictionary? defaultTextures, + out FrugalList invalidKeys) { - invalidKeys = new FrugalList(); + invalidKeys = []; var result = new Dictionary(); + var isDefaultComponent = defaultTextures is null; + if (!isDefaultComponent) { - // This assumes that _defaultTextures is already filled - foreach (var key in _defaultTextures.Keys) - result.Add(key, _defaultTextures[key]); + foreach (var key in defaultTextures!.Keys) + result.Add(key, defaultTextures[key]); } - + if (textureData.Textures.Count == 0) + { + ErrorReporter.Report(new InitializationError + { + GameManager = ToString(), + Message = $"No Textures defined for component '{textureData.Component}' in GuiDialogs.xml" + }); + } + foreach (var keyText in textureData.Textures.Keys) { - if (!ComponentTextureKeyExtensions.TryConvertToKey(keyText.AsSpan(), out var key)) + if (!GuiDialogParser.ComponentTypeDictionary.TryStringToEnum(keyText, out var key)) { invalidKeys.Add(keyText); continue; @@ -119,8 +130,8 @@ private Dictionary InitializeComponentT var textureValue = textureData.Textures.GetLastValue(keyText); result[key] = new ComponentTextureEntry(key, textureValue, !isDefaultComponent); } - - return result; + + return new ReadOnlyDictionary(result); } private void InitializeMegaTextures(GuiDialogsXmlTextureData guiDialogs) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/GuiDialogsXml.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/GuiDialogsXml.cs index 72cc5a0..dbaaed6 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/GuiDialogsXml.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/GuiDialogsXml.cs @@ -1,5 +1,5 @@ -using PG.StarWarsGame.Engine.Xml; -using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Data; namespace PG.StarWarsGame.Engine.GuiDialog.Xml; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/GuiDialogsXmlTextureData.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/GuiDialogsXmlTextureData.cs index 6496417..c5e56b8 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/GuiDialogsXmlTextureData.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/GuiDialogsXmlTextureData.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; -using PG.StarWarsGame.Engine.Xml; using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Data; namespace PG.StarWarsGame.Engine.GuiDialog.Xml; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/XmlComponentTextureData.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/XmlComponentTextureData.cs index f102457..6b91547 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/XmlComponentTextureData.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/Xml/XmlComponentTextureData.cs @@ -1,7 +1,7 @@ using System; using AnakinRaW.CommonUtilities.Collections; -using PG.StarWarsGame.Engine.Xml; using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Data; namespace PG.StarWarsGame.Engine.GuiDialog.Xml; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/HardcodedEngineAssets.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/HardcodedEngineAssets.cs new file mode 100644 index 0000000..92a7cdf --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/HardcodedEngineAssets.cs @@ -0,0 +1,302 @@ +using System; +using System.Collections.Generic; + +namespace PG.StarWarsGame.Engine; + +public static class HardcodedEngineAssets +{ + /// + /// These models / particles are hardcoded into StarWarsG.exe. + /// + public static IList HardcodedFocModels { get; } = new List + { + "i_tutorial_arrow.alo", + "p_hero_empire_fx.alo", + "i_tactical_corrupt.alo", + "p_icon_corrupt.alo", + "w_planet_select_neutral.alo", + "i_game_arrow.alo", + "i_galactic_radar.alo", + "W_TextScroll.alo" + }; + + + /// + /// These models / particles are hardcoded into StarWarsG.exe. + /// + public static IList HardcodedEawModels { get; } = new List + { + "i_tutorial_arrow.alo", + "p_hero_empire_fx.alo", + "w_planet_select_neutral.alo", + "i_game_arrow.alo", + "i_galactic_radar.alo", + "W_TextScroll.alo" + }; + + public static IList HardcodedFocTextures { get; } = new List + { + "splash.tga", + "SPLASH_E3.tga", + "i_button_temporary.tga", + "i_attention00.tga", + "i_tree_arrow_right.tga", + "load_overlay.tga", + "i_pa_weather_sun.tga", + "i_pa_weather_fire.tga", + "i_pa_weather_snow.tga", + "i_pa_weather_rain.tga", + "i_pa_weather_wind.tga", + "Menuback_Overlay.TGA", + "i_button_space_station.tga", + "i_button_ground_base.tga", + "i_button_space_unitcount.tga", + "i_button_ground_unitcount.tga", + "i_dialogue_blank.tga", + "checked_box.tga", + "Galactic_Back.tga", + "Generic_Space.tga", + "Generic_Land.tga", + "red_dot.tga", + "yellow_dot.tga", + "green_dot.tga", + "i_tree_arrow_2right.tga", + "i_tree_arrow_3right.tga", + "i_tree_arrow_down.tga", + "Generic_Flat_B.tga", + "w_generic_white.tga", + "w_shadow_blob.tga", + "w_light_blob.tga", + "p_particle_master.tga", + "missing_texture_xxx.tga", + "D_master_decal.tga", + "p_particle_depth_master.tga", + "MT_CommandBar.tga", + "MT_CommandBarCompressed.dds", + "DEFAULTPOINTER_00.TGA", + "tractor_beam00.tga", + "i_button_unknown.tga", + "i_icon_stealth.tga", + "e_line2.tga", + "e_line.tga", + "i_bar_icons_space.tga", + "i_bar_icons_land.tga", + "e_topbar2.tga", + "e_topbar.tga", + "e_against_frame.tga", + "missing.tga", + "i_encyclopedia_border.tga", + "i_radar_default_blip.tga", + "select_overlay.tga", + "i_icon_raid.tga", + "maptemp.tga", + "shield_range.tga", + "W_galaxy_line.tga", + "W_galaxy_line_alpha.tga", + "W_galaxy_dot.tga", + "i_sa_defend_mode.tga", + "i_sa_deploy.tga", + "i_sa_interdict.tga", + "i_sa_barrage_area.tga", + "i_sa_capture_vehicles.tga", + "i_sa_spread_out.tga", + "i_sa_power_to_engines.tga", + "i_sa_rocket_attack.tga", + "i_sa_power_to_weapons.tga", + "i_sa_tractor_beam.tga", + "i_sa_fire_energy_weapon.tga", + "i_sa_missile_jammer.tga", + "i_sa_evasive_maneuvers.tga", + "i_sa_all_ships_concentrate_fire.tga", + "i_sa_sprint.tga", + "i_sa_stim_pack.tga", + "i_sa_s_foil_mode.tga", + "i_sa_maximum_firepower.tga", + "i_sa_swap_weapons.tga", + "i_sa_full_salvo.tga", + "i_sa_force_cloak.tga", + "i_sa_sensor_jamming.tga", + "i_sa_force_crush.tga", + "i_sa_force_push.tga", + "i_sa_force_lighting.tga", + "i_sa_flame_thrower.tga", + "i_sa_jetpack_jump.tga", + "i_sa_force_protect.tga", + "i_sa_hack_turret.tga", + "i_sa_repair_vehicle.tga", + "i_sa_sticky_bomb.tga", + "i_sa_electronic_scramble.tga", + "i_sa_area_heal.tga", + "i_sa_join_me.tga", + "i_sa_tow_cable_attack.tga", + "i_sa_sensor_ping.tga", + "i_sa_cover_me.tga", + "i_sa_Harmonic_bomb.tga", + "i_sa_drop_bomb.tga", + "i_sa_weaken_enemy.tga", + "i_sa_drain_life.tga", + "i_sa_blast.tga", + "i_sa_shield_flare.tga", + "i_sa_deploy_squad.tga", + "i_sa_stun.tga", + "i_sa_contaminate.tga", + "i_sa_berserker.tga", + "i_sa_force_sight.tga", + "i_sa_saber_throw.tga", + "i_sa_laser_defense.tga", + "i_sa_force_confuse.tga", + "i_sa_leech_shields.tga", + "i_sa_tactical_bribe.tga", + "i_sa_cluster_bomb.tga", + "i_sa_place_remote_bomb.tga", + "i_sa_detonate_remote_bomb.tga", + "i_sa_infection.tga", + "i_sa_proximity_mines.tga", + "i_sa_buzz_droids.tga", + "i_sa_summon.tga", + "i_sa_corrupt_systems.tga", + "i_sa_hunt.tga", + "i_sa_lure.tga", + "i_sa_self_destruct.tga", + "i_sa_deploy_stormtroopers.tga", + "i_sa_ion_cannon_shot.tga", + "i_sa_lucky_shot.tga", + "lightning_default.tga", + "Mon_Mothma.tga", + "Tarkin.tga", + "W_Cable.tga", + "i_information00.tga", + "W_Laser_Pill.tga", + "lensflare.tga", + "W_Space_FOW_Grid.tga", + "W_Space_Reinforce_FOW_Grid.tga", + }; + + public static IList HardcodedEawTextures { get; } = new List + { + "splash.tga", + "i_button_temporary.tga", + "i_attention00.tga", + "i_tree_arrow_right.tga", + "load_overlay.tga", + "i_pa_weather_sun.tga", + "i_pa_weather_fire.tga", + "i_pa_weather_snow.tga", + "i_pa_weather_rain.tga", + "i_pa_weather_wind.tga", + "Menuback_Overlay.TGA", + "i_button_space_station.tga", + "i_button_ground_base.tga", + "i_button_space_unitcount.tga", + "i_button_ground_unitcount.tga", + "i_dialogue_blank.tga", + "checked_box.tga", + "Galactic_Back.tga", + "Generic_Space.tga", + "Generic_Land.tga", + "red_dot.tga", + "yellow_dot.tga", + "green_dot.tga", + "i_tree_arrow_2right.tga", + "i_tree_arrow_3right.tga", + "i_tree_arrow_down.tga", + "Generic_Flat_B.tga", + "w_generic_white.tga", + "w_shadow_blob.tga", + "w_light_blob.tga", + "p_particle_master.tga", + "missing_texture_xxx.tga", + "D_master_decal.tga", + "p_particle_depth_master.tga", + "MT_CommandBar.tga", + "MT_CommandBarCompressed.dds", + "DEFAULTPOINTER_00.TGA", + "tractor_beam00.tga", + "i_button_unknown.tga", + "i_icon_stealth.tga", + "e_line2.tga", + "e_line.tga", + "i_bar_icons_space.tga", + "i_bar_icons_land.tga", + "e_topbar2.tga", + "e_topbar.tga", + "e_against_frame.tga", + "missing.tga", + "i_encyclopedia_border.tga", + "i_radar_default_blip.tga", + "select_overlay.tga", + "i_icon_raid.tga", + "W_galaxy_line.tga", + "W_galaxy_line_alpha.tga", + "W_galaxy_dot.tga", + "i_sa_defend_mode.tga", + "i_sa_deploy.tga", + "i_sa_interdict.tga", + "i_sa_barrage_area.tga", + "i_sa_capture_vehicles.tga", + "i_sa_spread_out.tga", + "i_sa_power_to_engines.tga", + "i_sa_rocket_attack.tga", + "i_sa_power_to_weapons.tga", + "i_sa_tractor_beam.tga", + "i_sa_fire_energy_weapon.tga", + "i_sa_missile_jammer.tga", + "i_sa_evasive_maneuvers.tga", + "i_sa_all_ships_concentrate_fire.tga", + "i_sa_sprint.tga", + "i_sa_s_foil_mode.tga", + "i_sa_maximum_firepower.tga", + "i_sa_force_crush.tga", + "i_sa_force_push.tga", + "i_sa_force_lighting.tga", + "i_sa_flame_thrower.tga", + "i_sa_jetpack_jump.tga", + "i_sa_force_protect.tga", + "i_sa_hack_turret.tga", + "i_sa_repair_vehicle.tga", + "i_sa_sticky_bomb.tga", + "i_sa_electronic_scramble.tga", + "i_sa_area_heal.tga", + "i_sa_join_me.tga", + "i_sa_tow_cable_attack.tga", + "i_sa_sensor_ping.tga", + "i_sa_cover_me.tga", + "i_sa_Harmonic_bomb.tga", + "i_sa_drop_bomb.tga", + "i_sa_weaken_enemy.tga", + "i_sa_hunt.tga", + "i_sa_lure.tga", + "i_sa_self_destruct.tga", + "i_sa_deploy_stormtroopers.tga", + "i_sa_ion_cannon_shot.tga", + "i_sa_lucky_shot.tga", + "lightning_default.tga", + "Mon_Mothma.tga", + "Tarkin.tga", + "W_Cable.tga", + "i_information00.tga", + "W_Laser_Pill.tga", + "W_Space_FOW_Grid.tga", + "W_Space_Reinforce_FOW_Grid.tga", + }; + + public static IList GetHardcodedModels(GameEngineType engine) + { + return engine switch + { + GameEngineType.Eaw => HardcodedEawModels, + GameEngineType.Foc => HardcodedFocModels, + _ => throw new ArgumentOutOfRangeException(nameof(engine), engine, null) + }; + } + + public static IList GetHardcodedTextures(GameEngineType engine) + { + return engine switch + { + GameEngineType.Eaw => HardcodedEawTextures, + GameEngineType.Foc => HardcodedFocTextures, + _ => throw new ArgumentOutOfRangeException(nameof(engine), engine, null) + }; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/IRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/IRepository.cs index 4ca45db..89c2d86 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/IRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/IRepository.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.IO; namespace PG.StarWarsGame.Engine.IO; @@ -9,6 +10,7 @@ public interface IRepository Stream OpenFile(ReadOnlySpan filePath, bool megFileOnly = false); bool FileExists(string filePath, bool megFileOnly = false); + bool FileExists(string filePath, bool megFileOnly, out bool inMeg, [NotNullWhen(true)] out string? actualFilePath); bool FileExists(ReadOnlySpan filePath, bool megFileOnly = false); bool FileExists(ReadOnlySpan filePath, bool megFileOnly, out bool pathTooLong); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/MultiPassRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/MultiPassRepository.cs index 0fce4d3..fd95e5b 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/MultiPassRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/MultiPassRepository.cs @@ -1,9 +1,10 @@ -using System; -using System.IO; -using System.IO.Abstractions; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using PG.StarWarsGame.Engine.IO.Repositories; using PG.StarWarsGame.Engine.Utilities; +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Abstractions; namespace PG.StarWarsGame.Engine.IO; @@ -30,6 +31,22 @@ public bool FileExists(string filePath, bool megFileOnly = false) return FileExists(filePath.AsSpan(), megFileOnly); } + public bool FileExists(string filePath, bool megFileOnly, out bool inMeg, [NotNullWhen(true)] out string? actualFilePath) + { + var multiPassSb = new ValueStringBuilder(stackalloc char[PGConstants.MaxMegEntryPathLength]); + var destinationSb = new ValueStringBuilder(stackalloc char[PGConstants.MaxMegEntryPathLength]); + var result = MultiPassAction(filePath, ref multiPassSb, ref destinationSb, megFileOnly); + var fileFound = result.FileFound; + inMeg = result.InMeg; + if (!fileFound) + actualFilePath = null; + else + actualFilePath = result.InMeg ? result.MegDataEntryReference.Path : result.FilePath.ToString(); + multiPassSb.Dispose(); + destinationSb.Dispose(); + return fileFound; + } + public bool FileExists(ReadOnlySpan filePath, bool megFileOnly = false) { return FileExists(filePath, megFileOnly, out _); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs index 7abd1b9..d35254d 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs @@ -1,14 +1,15 @@ -using System; +using AnakinRaW.CommonUtilities.FileSystem; +using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine.IO.Utilities; +using PG.StarWarsGame.Engine.Utilities; +using PG.StarWarsGame.Files.MEG.Binary; +using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using AnakinRaW.CommonUtilities.FileSystem; -using Microsoft.Extensions.Logging; -using PG.StarWarsGame.Engine.IO.Utilities; -using PG.StarWarsGame.Engine.Utilities; -using PG.StarWarsGame.Files.MEG.Binary; namespace PG.StarWarsGame.Engine.IO.Repositories; @@ -32,6 +33,20 @@ public bool FileExists(string filePath, bool megFileOnly = false) return FileExists(filePath.AsSpan(), megFileOnly); } + public bool FileExists(string filePath, bool megFileOnly, out bool inMeg, [NotNullWhen(true)] out string? actualFilePath) + { + var sb = new ValueStringBuilder(stackalloc char[PGConstants.MaxMegEntryPathLength]); + var fileFound = FindFile(filePath, ref sb, megFileOnly); + var fileExists = fileFound.FileFound; + inMeg = fileFound.InMeg; + if (!fileExists) + actualFilePath = null; + else + actualFilePath = fileFound.InMeg ? fileFound.MegDataEntryReference.Path : fileFound.FilePath.ToString(); + sb.Dispose(); + return fileExists; + } + public bool FileExists(ReadOnlySpan filePath, bool megFileOnly = false) { return FileExists(filePath, megFileOnly, out _); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj index 326fd1d..8d201e4 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj @@ -23,10 +23,10 @@ - - - - + + + + @@ -39,7 +39,4 @@ - - - \ 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 index 57b1fa3..89ac95d 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj.DotSettings +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj.DotSettings @@ -4,4 +4,6 @@ True True True - True \ No newline at end of file + True + True + True \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PGConstants.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/PGConstants.cs index b7a9218..a53709c 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PGConstants.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PGConstants.cs @@ -15,8 +15,11 @@ public static class PGConstants public const int MaxSFXEventDatabaseFileName = 259; public const int MaxSFXEventName = 255; public const int MaxGameObjectDatabaseFileName = 127; - public const int MaxCommandBarDatabaseFileName = 259; + public const int MaxCommandBarDatabaseFileName = 271; public const int MaxCommandBarComponentName = 255; + // The actual engine's buffer size that holds name. + public const int MaxCommandBarComponentNameBuffer = 263; + public const int MaxGameObjectTypeName = 127; public const int MaxGuiDialogMegaTextureFileName = 255; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs index 0132d10..73bd755 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs @@ -12,7 +12,7 @@ public static void ContributeServices(IServiceCollection serviceCollection) // Singletons serviceCollection.AddSingleton(sp => new GameRepositoryFactory(sp)); serviceCollection.AddSingleton(sp => new GameLanguageManagerProvider(sp)); - serviceCollection.AddSingleton(sp => new PetroglyphXmlFileParserFactory(sp)); + serviceCollection.AddSingleton(sp => new XmlObjectParserFactory(sp)); serviceCollection.AddSingleton(sp => new PetroglyphStarWarsGameEngineService(sp)); } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs index 02a8d9f..42a2416 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphStarWarsGameEngineService.cs @@ -51,6 +51,7 @@ public async Task InitializeAsync( void OnInitializationError(object sender, InitializationError e) { + _logger?.LogWarning("Engine initialization failed for {Manager}: {Message}", e.GameManager, e.Message); if (cancelOnInitializationError) cts.Cancel(); } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/EawModelAnimationTypes.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/EawModelAnimationTypes.cs deleted file mode 100644 index f1a89c3..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/EawModelAnimationTypes.cs +++ /dev/null @@ -1,109 +0,0 @@ -namespace PG.StarWarsGame.Engine.Rendering.Animations; - -public static class EawModelAnimationTypes -{ - public static readonly ModelAnimationType Idle = new(GameEngineType.Eaw, 0x0); - public static readonly ModelAnimationType SpaceIdle = new(GameEngineType.Eaw, 0x1); - public static readonly ModelAnimationType Move = new(GameEngineType.Eaw, 0x2); - public static readonly ModelAnimationType TurnLeft = new(GameEngineType.Eaw, 0x3); - public static readonly ModelAnimationType TurnRight = new(GameEngineType.Eaw, 0x4); - public static readonly ModelAnimationType Attack = new(GameEngineType.Eaw, 0x5); - public static readonly ModelAnimationType AttackIdle = new(GameEngineType.Eaw, 0x6); - public static readonly ModelAnimationType Die = new(GameEngineType.Eaw, 0x7); - public static readonly ModelAnimationType Rotate = new(GameEngineType.Eaw, 0x8); - public static readonly ModelAnimationType SpecialA = new(GameEngineType.Eaw, 0x9); - public static readonly ModelAnimationType SpecialB = new(GameEngineType.Eaw, 0xa); - public static readonly ModelAnimationType SpecialC = new(GameEngineType.Eaw, 0xb); - public static readonly ModelAnimationType TransitionToLeftTurn = new(GameEngineType.Eaw, 0xc); - public static readonly ModelAnimationType TransitionFromLeftTurn = new(GameEngineType.Eaw, 0xd); - public static readonly ModelAnimationType TransitionToRightTurn = new(GameEngineType.Eaw, 0xe); - public static readonly ModelAnimationType TransitionFromRightTurn = new(GameEngineType.Eaw, 0xf); - public static readonly ModelAnimationType TransitionToMove = new(GameEngineType.Eaw, 0x10); - public static readonly ModelAnimationType TransitionFromMoveFrame0 = new(GameEngineType.Eaw, 0x11); - public static readonly ModelAnimationType TransitionFromMoveFrame40 = new(GameEngineType.Eaw, 0x12); - public static readonly ModelAnimationType TransitionFromMoveFrame80 = new(GameEngineType.Eaw, 0x13); - public static readonly ModelAnimationType TransitionFromMoveFrame120 = new(GameEngineType.Eaw, 0x14); - public static readonly ModelAnimationType TurnLeftHalf = new(GameEngineType.Eaw, 0x15); - public static readonly ModelAnimationType TurnLeftQuarter = new(GameEngineType.Eaw, 0x16); - public static readonly ModelAnimationType TurnRightHalf = new(GameEngineType.Eaw, 0x17); - public static readonly ModelAnimationType TurnRightQuarter = new(GameEngineType.Eaw, 0x18); - public static readonly ModelAnimationType Deploy = new(GameEngineType.Eaw, 0x19); - public static readonly ModelAnimationType Undeploy = new(GameEngineType.Eaw, 0x1a); - public static readonly ModelAnimationType Cinematic = new(GameEngineType.Eaw, 0x1b); - public static readonly ModelAnimationType BlockBlaster = new(GameEngineType.Eaw, 0x1c); - public static readonly ModelAnimationType RedirectBlaster = new(GameEngineType.Eaw, 0x1d); - public static readonly ModelAnimationType IdleBlockBlaster = new(GameEngineType.Eaw, 0x1e); - public static readonly ModelAnimationType ForceWhirlwindAttack = new(GameEngineType.Eaw, 0x1f); - public static readonly ModelAnimationType ForceWhirlwindDie = new(GameEngineType.Eaw, 0x20); - public static readonly ModelAnimationType ForceTelekinesisAttack = new(GameEngineType.Eaw, 0x21); - public static readonly ModelAnimationType ForceTelekinesisHold = new(GameEngineType.Eaw, 0x22); - public static readonly ModelAnimationType ForceTelekinesisRelease = new(GameEngineType.Eaw, 0x23); - public static readonly ModelAnimationType ForceTelekinesisDie = new(GameEngineType.Eaw, 0x24); - public static readonly ModelAnimationType EarthquakeAttack = new(GameEngineType.Eaw, 0x25); - public static readonly ModelAnimationType EarthquakeHold = new(GameEngineType.Eaw, 0x26); - public static readonly ModelAnimationType EarthquakeRelease = new(GameEngineType.Eaw, 0x27); - public static readonly ModelAnimationType ForceLightningAttack = new(GameEngineType.Eaw, 0x28); - public static readonly ModelAnimationType ForceLightningDie = new(GameEngineType.Eaw, 0x29); - public static readonly ModelAnimationType ForceRun = new(GameEngineType.Eaw, 0x2a); - public static readonly ModelAnimationType TransportLanding = new(GameEngineType.Eaw, 0x2b); - public static readonly ModelAnimationType TransportLeaving = new(GameEngineType.Eaw, 0x2c); - public static readonly ModelAnimationType FlameAttack = new(GameEngineType.Eaw, 0x2d); - public static readonly ModelAnimationType Demolition = new(GameEngineType.Eaw, 0x2e); - public static readonly ModelAnimationType BombToss = new(GameEngineType.Eaw, 0x2f); - public static readonly ModelAnimationType Jump = new(GameEngineType.Eaw, 0x30); - public static readonly ModelAnimationType FlyIdle = new(GameEngineType.Eaw, 0x31); - public static readonly ModelAnimationType FlyLand = new(GameEngineType.Eaw, 0x32); - public static readonly ModelAnimationType LandIdle = new(GameEngineType.Eaw, 0x33); - public static readonly ModelAnimationType Land = new(GameEngineType.Eaw, 0x34); - public static readonly ModelAnimationType HcWin = new(GameEngineType.Eaw, 0x35); - public static readonly ModelAnimationType HcLose = new(GameEngineType.Eaw, 0x36); - public static readonly ModelAnimationType HcDraw = new(GameEngineType.Eaw, 0x37); - public static readonly ModelAnimationType ShieldOn = new(GameEngineType.Eaw, 0x38); - public static readonly ModelAnimationType ShieldOff = new(GameEngineType.Eaw, 0x39); - public static readonly ModelAnimationType CableAttackDie = new(GameEngineType.Eaw, 0x3a); - public static readonly ModelAnimationType DeployedCableAttackDie = new(GameEngineType.Eaw, 0x3b); - public static readonly ModelAnimationType DeployedDie = new(GameEngineType.Eaw, 0x3c); - public static readonly ModelAnimationType RunAroundOnFire = new(GameEngineType.Eaw, 0x3d); - public static readonly ModelAnimationType FireDie = new(GameEngineType.Eaw, 0x3e); - public static readonly ModelAnimationType PoundAttack = new(GameEngineType.Eaw, 0x3f); - public static readonly ModelAnimationType EatAttack = new(GameEngineType.Eaw, 0x40); - public static readonly ModelAnimationType EatDie = new(GameEngineType.Eaw, 0x41); - public static readonly ModelAnimationType MoveWalk = new(GameEngineType.Eaw, 0x42); - public static readonly ModelAnimationType MoveCrouch = new(GameEngineType.Eaw, 0x43); - public static readonly ModelAnimationType StructureOpen = new(GameEngineType.Eaw, 0x44); - public static readonly ModelAnimationType StructureHold = new(GameEngineType.Eaw, 0x45); - public static readonly ModelAnimationType StructureClose = new(GameEngineType.Eaw, 0x46); - public static readonly ModelAnimationType IdleCrouch = new(GameEngineType.Eaw, 0x47); - public static readonly ModelAnimationType TurnLeftCrouch = new(GameEngineType.Eaw, 0x48); - public static readonly ModelAnimationType TurnRightCrouch = new(GameEngineType.Eaw, 0x49); - public static readonly ModelAnimationType Build = new(GameEngineType.Eaw, 0x4a); - public static readonly ModelAnimationType TransitionOwnership = new(GameEngineType.Eaw, 0x4b); - public static readonly ModelAnimationType SelfDestruct = new(GameEngineType.Eaw, 0x4c); - public static readonly ModelAnimationType Attention = new(GameEngineType.Eaw, 0x4d); - public static readonly ModelAnimationType Celebrate = new(GameEngineType.Eaw, 0x4e); - public static readonly ModelAnimationType FlinchLeft = new(GameEngineType.Eaw, 0x4f); - public static readonly ModelAnimationType FlinchRight = new(GameEngineType.Eaw, 0x50); - public static readonly ModelAnimationType FlinchFront = new(GameEngineType.Eaw, 0x51); - public static readonly ModelAnimationType FlinchBack = new(GameEngineType.Eaw, 0x52); - public static readonly ModelAnimationType AttackFlinchLeft = new(GameEngineType.Eaw, 0x53); - public static readonly ModelAnimationType AttackFlinchRight = new(GameEngineType.Eaw, 0x54); - public static readonly ModelAnimationType AttackFlinchFront = new(GameEngineType.Eaw, 0x55); - public static readonly ModelAnimationType AttackFlinchBack = new(GameEngineType.Eaw, 0x56); - public static readonly ModelAnimationType Talk = new(GameEngineType.Eaw, 0x57); - public static readonly ModelAnimationType TalkGesture = new(GameEngineType.Eaw, 0x58); - public static readonly ModelAnimationType TalkQuestion = new(GameEngineType.Eaw, 0x59); - public static readonly ModelAnimationType Hacking = new(GameEngineType.Eaw, 0x5a); - public static readonly ModelAnimationType Repairing = new(GameEngineType.Eaw, 0x5b); - public static readonly ModelAnimationType Choke = new(GameEngineType.Eaw, 0x5c); - public static readonly ModelAnimationType ChokeDie = new(GameEngineType.Eaw, 0x5d); - public static readonly ModelAnimationType DropTroopers = new(GameEngineType.Eaw, 0x5e); - public static readonly ModelAnimationType RopeSlide = new(GameEngineType.Eaw, 0x5f); - public static readonly ModelAnimationType RopeLand = new(GameEngineType.Eaw, 0x60); - public static readonly ModelAnimationType RopeDrop = new(GameEngineType.Eaw, 0x61); - public static readonly ModelAnimationType RopeLift = new(GameEngineType.Eaw, 0x62); - public static readonly ModelAnimationType Alarm = new(GameEngineType.Eaw, 0x63); - public static readonly ModelAnimationType Warning = new(GameEngineType.Eaw, 0x64); - public static readonly ModelAnimationType Crushed = new(GameEngineType.Eaw, 0x65); - public static readonly ModelAnimationType PowerDown = new(GameEngineType.Eaw, 0x66); - public static readonly ModelAnimationType PowerUp = new(GameEngineType.Eaw, 0x67); -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/FocModelAnimationTypes.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/FocModelAnimationTypes.cs deleted file mode 100644 index 743491a..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/FocModelAnimationTypes.cs +++ /dev/null @@ -1,124 +0,0 @@ -namespace PG.StarWarsGame.Engine.Rendering.Animations; - -public static class FocModelAnimationTypes -{ - public static readonly ModelAnimationType Idle = new(GameEngineType.Foc, 0x0); - public static readonly ModelAnimationType SpaceIdle = new(GameEngineType.Foc, 0x1); - public static readonly ModelAnimationType Move = new(GameEngineType.Foc, 0x2); - public static readonly ModelAnimationType TurnLeft = new(GameEngineType.Foc, 0x3); - public static readonly ModelAnimationType TurnRight = new(GameEngineType.Foc, 0x4); - public static readonly ModelAnimationType Attack = new(GameEngineType.Foc, 0x5); - public static readonly ModelAnimationType AttackIdle = new(GameEngineType.Foc, 0x6); - public static readonly ModelAnimationType Die = new(GameEngineType.Foc, 0x7); - public static readonly ModelAnimationType Rotate = new(GameEngineType.Foc, 0x8); - public static readonly ModelAnimationType SpecialA = new(GameEngineType.Foc, 0x9); - public static readonly ModelAnimationType SpecialB = new(GameEngineType.Foc, 0xa); - public static readonly ModelAnimationType SpecialC = new(GameEngineType.Foc, 0xb); - public static readonly ModelAnimationType TransitionToLeftTurn = new(GameEngineType.Foc, 0xc); - public static readonly ModelAnimationType TransitionFromLeftTurn = new(GameEngineType.Foc, 0xd); - public static readonly ModelAnimationType TransitionToRightTurn = new(GameEngineType.Foc, 0xe); - public static readonly ModelAnimationType TransitionFromRightTurn = new(GameEngineType.Foc, 0xf); - public static readonly ModelAnimationType TransitionToMove = new(GameEngineType.Foc, 0x10); - public static readonly ModelAnimationType TransitionFromMoveFrame0 = new(GameEngineType.Foc, 0x11); - public static readonly ModelAnimationType TransitionFromMoveFrame40 = new(GameEngineType.Foc, 0x12); - public static readonly ModelAnimationType TransitionFromMoveFrame80 = new(GameEngineType.Foc, 0x13); - public static readonly ModelAnimationType TransitionFromMoveFrame120 = new(GameEngineType.Foc, 0x14); - public static readonly ModelAnimationType TurnLeftHalf = new(GameEngineType.Foc, 0x15); - public static readonly ModelAnimationType TurnLeftQuarter = new(GameEngineType.Foc, 0x16); - public static readonly ModelAnimationType TurnRightHalf = new(GameEngineType.Foc, 0x17); - public static readonly ModelAnimationType TurnRightQuarter = new(GameEngineType.Foc, 0x18); - public static readonly ModelAnimationType Deploy = new(GameEngineType.Foc, 0x19); - public static readonly ModelAnimationType Undeploy = new(GameEngineType.Foc, 0x1a); - public static readonly ModelAnimationType Cinematic = new(GameEngineType.Foc, 0x1b); - public static readonly ModelAnimationType BlockBlaster = new(GameEngineType.Foc, 0x1c); - public static readonly ModelAnimationType RedirectBlaster = new(GameEngineType.Foc, 0x1d); - public static readonly ModelAnimationType IdleBlockBlaster = new(GameEngineType.Foc, 0x1e); - public static readonly ModelAnimationType ForceWhirlwindAttack = new(GameEngineType.Foc, 0x1f); - public static readonly ModelAnimationType ForceWhirlwindDie = new(GameEngineType.Foc, 0x20); - public static readonly ModelAnimationType ForceTelekinesisAttack = new(GameEngineType.Foc, 0x21); - public static readonly ModelAnimationType ForceTelekinesisHold = new(GameEngineType.Foc, 0x22); - public static readonly ModelAnimationType ForceTelekinesisRelease = new(GameEngineType.Foc, 0x23); - public static readonly ModelAnimationType ForceTelekinesisDie = new(GameEngineType.Foc, 0x24); - public static readonly ModelAnimationType EarthquakeAttack = new(GameEngineType.Foc, 0x25); - public static readonly ModelAnimationType EarthquakeHold = new(GameEngineType.Foc, 0x26); - public static readonly ModelAnimationType EarthquakeRelease = new(GameEngineType.Foc, 0x27); - public static readonly ModelAnimationType ForceLightningAttack = new(GameEngineType.Foc, 0x28); - public static readonly ModelAnimationType ForceLightningDie = new(GameEngineType.Foc, 0x29); - public static readonly ModelAnimationType ForceRun = new(GameEngineType.Foc, 0x2a); - public static readonly ModelAnimationType TransportLanding = new(GameEngineType.Foc, 0x2b); - public static readonly ModelAnimationType TransportLeaving = new(GameEngineType.Foc, 0x2c); - public static readonly ModelAnimationType FlameAttack = new(GameEngineType.Foc, 0x2d); - public static readonly ModelAnimationType Demolition = new(GameEngineType.Foc, 0x2e); - public static readonly ModelAnimationType BombToss = new(GameEngineType.Foc, 0x2f); - public static readonly ModelAnimationType Jump = new(GameEngineType.Foc, 0x30); - public static readonly ModelAnimationType FlyIdle = new(GameEngineType.Foc, 0x31); - public static readonly ModelAnimationType FlyLand = new(GameEngineType.Foc, 0x32); - public static readonly ModelAnimationType LandIdle = new(GameEngineType.Foc, 0x33); - public static readonly ModelAnimationType Land = new(GameEngineType.Foc, 0x34); - public static readonly ModelAnimationType HcWin = new(GameEngineType.Foc, 0x35); - public static readonly ModelAnimationType HcLose = new(GameEngineType.Foc, 0x36); - public static readonly ModelAnimationType HcDraw = new(GameEngineType.Foc, 0x37); - public static readonly ModelAnimationType ShieldOn = new(GameEngineType.Foc, 0x38); - public static readonly ModelAnimationType ShieldOff = new(GameEngineType.Foc, 0x39); - public static readonly ModelAnimationType CableAttackDie = new(GameEngineType.Foc, 0x3a); - public static readonly ModelAnimationType DeployedCableAttackDie = new(GameEngineType.Foc, 0x3b); - public static readonly ModelAnimationType DeployedDie = new(GameEngineType.Foc, 0x3c); - public static readonly ModelAnimationType RunAroundOnFire = new(GameEngineType.Foc, 0x3d); - public static readonly ModelAnimationType FireDie = new(GameEngineType.Foc, 0x3e); - public static readonly ModelAnimationType PoundAttack = new(GameEngineType.Foc, 0x3f); - public static readonly ModelAnimationType EatAttack = new(GameEngineType.Foc, 0x40); - public static readonly ModelAnimationType EatDie = new(GameEngineType.Foc, 0x41); - public static readonly ModelAnimationType MoveWalk = new(GameEngineType.Foc, 0x42); - public static readonly ModelAnimationType MoveCrouch = new(GameEngineType.Foc, 0x43); - public static readonly ModelAnimationType StructureOpen = new(GameEngineType.Foc, 0x44); - public static readonly ModelAnimationType StructureHold = new(GameEngineType.Foc, 0x45); - public static readonly ModelAnimationType StructureClose = new(GameEngineType.Foc, 0x46); - public static readonly ModelAnimationType IdleCrouch = new(GameEngineType.Foc, 0x47); - public static readonly ModelAnimationType TurnLeftCrouch = new(GameEngineType.Foc, 0x48); - public static readonly ModelAnimationType TurnRightCrouch = new(GameEngineType.Foc, 0x49); - public static readonly ModelAnimationType Build = new(GameEngineType.Foc, 0x4a); - public static readonly ModelAnimationType TransitionOwnership = new(GameEngineType.Foc, 0x4b); - public static readonly ModelAnimationType SelfDestruct = new(GameEngineType.Foc, 0x4c); - public static readonly ModelAnimationType Attention = new(GameEngineType.Foc, 0x4d); - public static readonly ModelAnimationType Celebrate = new(GameEngineType.Foc, 0x4e); - public static readonly ModelAnimationType FlinchLeft = new(GameEngineType.Foc, 0x4f); - public static readonly ModelAnimationType FlinchRight = new(GameEngineType.Foc, 0x50); - public static readonly ModelAnimationType FlinchFront = new(GameEngineType.Foc, 0x51); - public static readonly ModelAnimationType FlinchBack = new(GameEngineType.Foc, 0x52); - public static readonly ModelAnimationType AttackFlinchLeft = new(GameEngineType.Foc, 0x53); - public static readonly ModelAnimationType AttackFlinchRight = new(GameEngineType.Foc, 0x54); - public static readonly ModelAnimationType AttackFlinchFront = new(GameEngineType.Foc, 0x55); - public static readonly ModelAnimationType AttackFlinchBack = new(GameEngineType.Foc, 0x56); - public static readonly ModelAnimationType Talk = new(GameEngineType.Foc, 0x57); - public static readonly ModelAnimationType TalkGesture = new(GameEngineType.Foc, 0x58); - public static readonly ModelAnimationType TalkQuestion = new(GameEngineType.Foc, 0x59); - public static readonly ModelAnimationType Hacking = new(GameEngineType.Foc, 0x5a); - public static readonly ModelAnimationType Repairing = new(GameEngineType.Foc, 0x5b); - public static readonly ModelAnimationType Choke = new(GameEngineType.Foc, 0x5c); - public static readonly ModelAnimationType ChokeDie = new(GameEngineType.Foc, 0x5d); - public static readonly ModelAnimationType DropTroopers = new(GameEngineType.Foc, 0x5e); - public static readonly ModelAnimationType RopeSlide = new(GameEngineType.Foc, 0x5f); - public static readonly ModelAnimationType RopeLand = new(GameEngineType.Foc, 0x60); - public static readonly ModelAnimationType RopeDrop = new(GameEngineType.Foc, 0x61); - public static readonly ModelAnimationType RopeLift = new(GameEngineType.Foc, 0x62); - public static readonly ModelAnimationType Alarm = new(GameEngineType.Foc, 0x63); - public static readonly ModelAnimationType Warning = new(GameEngineType.Foc, 0x64); - public static readonly ModelAnimationType Crushed = new(GameEngineType.Foc, 0x65); - public static readonly ModelAnimationType PowerDown = new(GameEngineType.Foc, 0x66); - public static readonly ModelAnimationType PowerUp = new(GameEngineType.Foc, 0x67); - public static readonly ModelAnimationType SpinMove = new(GameEngineType.Foc, 0x68); - public static readonly ModelAnimationType ForceRevealBegin = new(GameEngineType.Foc, 0x69); - public static readonly ModelAnimationType ForceRevealLoop = new(GameEngineType.Foc, 0x6a); - public static readonly ModelAnimationType ForceRevealEnd = new(GameEngineType.Foc, 0x6b); - public static readonly ModelAnimationType SaberThrow = new(GameEngineType.Foc, 0x6c); - public static readonly ModelAnimationType SaberControl = new(GameEngineType.Foc, 0x6d); - public static readonly ModelAnimationType SaberCatch = new(GameEngineType.Foc, 0x6e); - public static readonly ModelAnimationType SaberSpin = new(GameEngineType.Foc, 0x6f); - public static readonly ModelAnimationType ContaminateAttack = new(GameEngineType.Foc, 0x70); - public static readonly ModelAnimationType ContaminateLoop = new(GameEngineType.Foc, 0x71); - public static readonly ModelAnimationType ContaminateRelease = new(GameEngineType.Foc, 0x72); - public static readonly ModelAnimationType DeployedWalk = new(GameEngineType.Foc, 0x73); - public static readonly ModelAnimationType PadBuild = new(GameEngineType.Foc, 0x74); - public static readonly ModelAnimationType PadSell = new(GameEngineType.Foc, 0x75); - public static readonly ModelAnimationType Heal = new(GameEngineType.Foc, 0x76); -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/ModelAnimationType.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/ModelAnimationType.cs index 867ba08..31ecb22 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/ModelAnimationType.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/ModelAnimationType.cs @@ -1,47 +1,127 @@ -using System; +namespace PG.StarWarsGame.Engine.Rendering.Animations; -namespace PG.StarWarsGame.Engine.Rendering.Animations; - -public readonly struct ModelAnimationType : IEquatable +public enum ModelAnimationType { - public GameEngineType TargetEngine { get; } - - public int Value { get; } - - internal ModelAnimationType(GameEngineType engine, int value) - { - TargetEngine = engine; - Value = value; - } - - public override string ToString() - { - var nameLookup = SupportedModelAnimationTypes.GetAnimationTypesForEngine(TargetEngine); - return $"'{nameLookup[this]}' ({Value})"; - } - - public bool Equals(ModelAnimationType other) - { - return TargetEngine == other.TargetEngine && Value == other.Value; - } - - public override bool Equals(object? obj) - { - return obj is ModelAnimationType other && Equals(other); - } - - public override int GetHashCode() - { - return HashCode.Combine((int)TargetEngine, Value); - } - - public static bool operator ==(ModelAnimationType left, ModelAnimationType right) - { - return left.Equals(right); - } - - public static bool operator !=(ModelAnimationType left, ModelAnimationType right) - { - return !(left == right); - } -} \ No newline at end of file + Invalid = -1, + None = -1, + Idle = 0x0, + SpaceIdle = 0x1, + Move = 0x2, + TurnLeft = 0x3, + TurnRight = 0x4, + Attack = 0x5, + AttackIdle = 0x6, + Die = 0x7, + Rotate = 0x8, + SpecialA = 0x9, + SpecialB = 0xa, + SpecialC = 0xb, + TransitionToLeftTurn = 0xc, + TransitionFromLeftTurn = 0xd, + TransitionToRightTurn = 0xe, + TransitionFromRightTurn = 0xf, + TransitionToMove = 0x10, + TransitionFromMoveFrame0 = 0x11, + TransitionFromMoveFrame40 = 0x12, + TransitionFromMoveFrame80 = 0x13, + TransitionFromMoveFrame120 = 0x14, + TurnLeftHalf = 0x15, + TurnLeftQuarter = 0x16, + TurnRightHalf = 0x17, + TurnRightQuarter = 0x18, + Deploy = 0x19, + Undeploy = 0x1a, + Cinematic = 0x1b, + BlockBlaster = 0x1c, + RedirectBlaster = 0x1d, + IdleBlockBlaster = 0x1e, + ForceWhirlwindAttack = 0x1f, + ForceWhirlwindDie = 0x20, + ForceTelekinesisAttack = 0x21, + ForceTelekinesisHold = 0x22, + ForceTelekinesisRelease = 0x23, + ForceTelekinesisDie = 0x24, + EarthquakeAttack = 0x25, + EarthquakeHold = 0x26, + EarthquakeRelease = 0x27, + ForceLightningAttack = 0x28, + ForceLightningDie = 0x29, + ForceRun = 0x2a, + TransportLanding = 0x2b, + TransportLeaving = 0x2c, + FlameAttack = 0x2d, + Demolition = 0x2e, + BombToss = 0x2f, + Jump = 0x30, + FlyIdle = 0x31, + FlyLand = 0x32, + LandIdle = 0x33, + Land = 0x34, + HcWin = 0x35, + HcLose = 0x36, + HcDraw = 0x37, + ShieldOn = 0x38, + ShieldOff = 0x39, + CableAttackDie = 0x3a, + DeployedCableAttackDie = 0x3b, + DeployedDie = 0x3c, + RunAroundOnFire = 0x3d, + FireDie = 0x3e, + PoundAttack = 0x3f, + EatAttack = 0x40, + EatDie = 0x41, + MoveWalk = 0x42, + MoveCrouch = 0x43, + StructureOpen = 0x44, + StructureHold = 0x45, + StructureClose = 0x46, + IdleCrouch = 0x47, + TurnLeftCrouch = 0x48, + TurnRightCrouch = 0x49, + Build = 0x4a, + TransitionOwnership = 0x4b, + SelfDestruct = 0x4c, + Attention = 0x4d, + Celebrate = 0x4e, + FlinchLeft = 0x4f, + FlinchRight = 0x50, + FlinchFront = 0x51, + FlinchBack = 0x52, + AttackFlinchLeft = 0x53, + AttackFlinchRight = 0x54, + AttackFlinchFront = 0x55, + AttackFlinchBack = 0x56, + Talk = 0x57, + TalkGesture = 0x58, + TalkQuestion = 0x59, + Hacking = 0x5a, + Repairing = 0x5b, + Choke = 0x5c, + ChokeDie = 0x5d, + DropTroopers = 0x5e, + RopeSlide = 0x5f, + RopeLand = 0x60, + RopeDrop = 0x61, + RopeLift = 0x62, + Alarm = 0x63, + Warning = 0x64, + Crushed = 0x65, + PowerDown = 0x66, + PowerUp = 0x67, + // These are exclusive to FoC + SpinMove = 0x68, + ForceRevealBegin = 0x69, + ForceRevealLoop = 0x6a, + ForceRevealEnd = 0x6b, + SaberThrow = 0x6c, + SaberControl = 0x6d, + SaberCatch = 0x6e, + SaberSpin = 0x6f, + ContaminateAttack = 0x70, + ContaminateLoop = 0x71, + ContaminateRelease = 0x72, + DeployedWalk = 0x73, + PadBuild = 0x74, + PadSell = 0x75, + Heal = 0x76, +}; \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/SupportedModelAnimationTypes.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/SupportedModelAnimationTypes.cs index 2a7aa3f..302c2fb 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/SupportedModelAnimationTypes.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Animations/SupportedModelAnimationTypes.cs @@ -1,11 +1,16 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; +using System.Linq; namespace PG.StarWarsGame.Engine.Rendering.Animations; public static class SupportedModelAnimationTypes { + // NB: The games uses two different string mappings for animations. + // The strings here are used to identify the animation file name. + // These strings have an underscore '_' as delimiter, + // while the animation names for the LUA functions (Play_Animation("ANIM NAME")) use a space ' ' as delimiter. + public static IReadOnlyDictionary GetAnimationTypesForEngine(GameEngineType engineType) { return engineType switch @@ -15,237 +20,135 @@ public static IReadOnlyDictionary GetAnimationTypesF _ => throw new NotSupportedException() }; } - - [SuppressMessage("ReSharper", "StringLiteralTypo")] - private static readonly Dictionary FocSupportedAnimations = new() - { - { FocModelAnimationTypes.Idle, "IDLE"}, - { FocModelAnimationTypes.SpaceIdle, "SPACE_IDLE"}, - { FocModelAnimationTypes.Move, "MOVE"}, - { FocModelAnimationTypes.TurnLeft, "TURNL"}, - { FocModelAnimationTypes.TurnRight, "TURNR"}, - { FocModelAnimationTypes.Attack, "ATTACK"}, - { FocModelAnimationTypes.AttackIdle, "ATTACKIDLE"}, - { FocModelAnimationTypes.Die, "DIE"}, - { FocModelAnimationTypes.Rotate, "ROTATE"}, - { FocModelAnimationTypes.SpecialA, "SPECIAL_A"}, - { FocModelAnimationTypes.SpecialB, "SPECIAL_B"}, - { FocModelAnimationTypes.SpecialC, "SPECIAL_C"}, - { FocModelAnimationTypes.TransitionToLeftTurn, "TURNL_BEGIN"}, - { FocModelAnimationTypes.TransitionFromLeftTurn, "TURNL_END"}, - { FocModelAnimationTypes.TransitionToRightTurn, "TURNR_BEGIN"}, - { FocModelAnimationTypes.TransitionFromRightTurn, "TURNR_END"}, - { FocModelAnimationTypes.TransitionToMove, "MOVESTART"}, - { FocModelAnimationTypes.TransitionFromMoveFrame0, "MOVE_ENDONE"}, - { FocModelAnimationTypes.TransitionFromMoveFrame40, "MOVE_ENDTWO"}, - { FocModelAnimationTypes.TransitionFromMoveFrame80, "MOVE_ENDTHREE"}, - { FocModelAnimationTypes.TransitionFromMoveFrame120, "MOVE_ENDFOUR"}, - { FocModelAnimationTypes.TurnLeftHalf, "TURNL_HALF"}, - { FocModelAnimationTypes.TurnLeftQuarter, "TURNL_QUARTER"}, - { FocModelAnimationTypes.TurnRightHalf, "TURNR_HALF"}, - { FocModelAnimationTypes.TurnRightQuarter, "TURNR_QUARTER"}, - { FocModelAnimationTypes.Deploy, "DEPLOY"}, - { FocModelAnimationTypes.Undeploy, "UNDEPLOY"}, - { FocModelAnimationTypes.Cinematic, "CINEMATIC"}, - { FocModelAnimationTypes.BlockBlaster, "BLOCK_BLASTER"}, - { FocModelAnimationTypes.RedirectBlaster, "REDIRECT_BLASTER"}, - { FocModelAnimationTypes.IdleBlockBlaster, "IDLE_BLOCKBLASTER"}, - { FocModelAnimationTypes.ForceWhirlwindAttack, "FW_ATTACK"}, - { FocModelAnimationTypes.ForceWhirlwindDie, "FW_DIE"}, - { FocModelAnimationTypes.ForceTelekinesisAttack, "FTK_ATTACK"}, - { FocModelAnimationTypes.ForceTelekinesisHold, "FTK_HOLD"}, - { FocModelAnimationTypes.ForceTelekinesisRelease, "FTK_RELEASE"}, - { FocModelAnimationTypes.ForceTelekinesisDie, "FTK_DIE"}, - { FocModelAnimationTypes.EarthquakeAttack, "FB_ATTACK"}, - { FocModelAnimationTypes.EarthquakeHold, "FB_HOLD"}, - { FocModelAnimationTypes.EarthquakeRelease, "FB_RELEASE"}, - { FocModelAnimationTypes.ForceLightningAttack, "FL_ATTACK"}, - { FocModelAnimationTypes.ForceLightningDie, "FL_DIE"}, - { FocModelAnimationTypes.ForceRun, "FORCE_RUN"}, - { FocModelAnimationTypes.TransportLanding, "LAND"}, - { FocModelAnimationTypes.TransportLeaving, "TAKEOFF"}, - { FocModelAnimationTypes.FlameAttack, "FLAME_ATTACK"}, - { FocModelAnimationTypes.Demolition, "DEMOLITION"}, - { FocModelAnimationTypes.BombToss, "BOMBTOSS"}, - { FocModelAnimationTypes.Jump, "JUMP"}, - { FocModelAnimationTypes.FlyIdle, "FLYIDLE"}, - { FocModelAnimationTypes.FlyLand, "FLYLAND"}, - { FocModelAnimationTypes.LandIdle, "FLYLANDIDLE"}, - { FocModelAnimationTypes.Land, "FLYLANDDROP"}, - { FocModelAnimationTypes.HcWin, "HC_WIN"}, - { FocModelAnimationTypes.HcLose, "HC_LOSE"}, - { FocModelAnimationTypes.HcDraw, "HC_DRAW"}, - { FocModelAnimationTypes.ShieldOn, "SHIELD_ON"}, - { FocModelAnimationTypes.ShieldOff, "SHIELD_OFF"}, - { FocModelAnimationTypes.CableAttackDie, "CA_DIE"}, - { FocModelAnimationTypes.DeployedCableAttackDie, "DEPLOYED_CA_DIE"}, - { FocModelAnimationTypes.DeployedDie, "DEPLOYED_DIE"}, - { FocModelAnimationTypes.RunAroundOnFire, "FIRE_MOVE"}, - { FocModelAnimationTypes.FireDie, "FIRE_DIE"}, - { FocModelAnimationTypes.PoundAttack, "POUND_ATTACK"}, - { FocModelAnimationTypes.EatAttack, "EAT_ATTACK"}, - { FocModelAnimationTypes.EatDie, "EATEN_DIE"}, - { FocModelAnimationTypes.MoveWalk, "WALKMOVE"}, - { FocModelAnimationTypes.MoveCrouch, "CROUCHMOVE"}, - { FocModelAnimationTypes.StructureOpen, "OPEN"}, - { FocModelAnimationTypes.StructureHold, "HOLD"}, - { FocModelAnimationTypes.StructureClose, "CLOSE"}, - { FocModelAnimationTypes.IdleCrouch, "CROUCHIDLE"}, - { FocModelAnimationTypes.TurnLeftCrouch, "CROUCHTURNL"}, - { FocModelAnimationTypes.TurnRightCrouch, "CROUCHTURNR"}, - { FocModelAnimationTypes.Build, "BUILD"}, - { FocModelAnimationTypes.TransitionOwnership, "TRANS"}, - { FocModelAnimationTypes.SelfDestruct, "SELF_DESTRUCT"}, - { FocModelAnimationTypes.Attention, "ATTENTION"}, - { FocModelAnimationTypes.Celebrate, "CELEBRATE"}, - { FocModelAnimationTypes.FlinchLeft, "FLINCHL"}, - { FocModelAnimationTypes.FlinchRight, "FLINCHR"}, - { FocModelAnimationTypes.FlinchFront, "FLINCHF"}, - { FocModelAnimationTypes.FlinchBack, "FLINCHB"}, - { FocModelAnimationTypes.AttackFlinchLeft, "ATTACKFLINCHL"}, - { FocModelAnimationTypes.AttackFlinchRight, "ATTACKFLINCHR"}, - { FocModelAnimationTypes.AttackFlinchFront, "ATTACKFLINCHF"}, - { FocModelAnimationTypes.AttackFlinchBack, "ATTACKFLINCHB"}, - { FocModelAnimationTypes.Talk, "TALK"}, - { FocModelAnimationTypes.TalkGesture, "TALKGESTURE"}, - { FocModelAnimationTypes.TalkQuestion, "TALKQUESTION"}, - { FocModelAnimationTypes.Hacking, "HACKING"}, - { FocModelAnimationTypes.Repairing, "REPAIRING"}, - { FocModelAnimationTypes.Choke, "CHOKE"}, - { FocModelAnimationTypes.ChokeDie, "CHOKEDEATH"}, - { FocModelAnimationTypes.DropTroopers, "TROOPDROP"}, - { FocModelAnimationTypes.RopeSlide, "ROPESLIDE"}, - { FocModelAnimationTypes.RopeLand, "ROPELAND"}, - { FocModelAnimationTypes.RopeDrop, "ROPE_DROP"}, - { FocModelAnimationTypes.RopeLift, "ROPE_LIFT"}, - { FocModelAnimationTypes.Alarm, "ALARM"}, - { FocModelAnimationTypes.Warning, "WARNING"}, - { FocModelAnimationTypes.Crushed, "CRUSHED"}, - { FocModelAnimationTypes.PowerDown, "POWERDOWN"}, - { FocModelAnimationTypes.PowerUp, "POWERUP"}, - { FocModelAnimationTypes.SpinMove, "SPINMOVE"}, - { FocModelAnimationTypes.ForceRevealBegin, "FORCE_REVEAL_BEGIN"}, - { FocModelAnimationTypes.ForceRevealLoop, "FORCE_REVEAL_LOOP"}, - { FocModelAnimationTypes.ForceRevealEnd, "FORCE_REVEAL_END"}, - { FocModelAnimationTypes.SaberThrow, "SWORD_THROW"}, - { FocModelAnimationTypes.SaberControl, "SWORD_CONTROL"}, - { FocModelAnimationTypes.SaberCatch, "SWORD_CATCH"}, - { FocModelAnimationTypes.SaberSpin, "SWORDSPIN"}, - { FocModelAnimationTypes.ContaminateAttack, "CONTAMINATE_ATTACK"}, - { FocModelAnimationTypes.ContaminateLoop, "CONTAMINATE_LOOP"}, - { FocModelAnimationTypes.ContaminateRelease, "CONTAMINATE_RELEASE"}, - { FocModelAnimationTypes.DeployedWalk, "WALK"}, - { FocModelAnimationTypes.PadBuild, "PAD_BUILD"}, - { FocModelAnimationTypes.PadSell, "PAD_SELL"}, - { FocModelAnimationTypes.Heal, "HEAL"}, - }; - - [SuppressMessage("ReSharper", "StringLiteralTypo")] + private static readonly Dictionary EawSupportedAnimations = new() { - { EawModelAnimationTypes.Idle, "IDLE"}, - { EawModelAnimationTypes.SpaceIdle, "SPACE_IDLE"}, - { EawModelAnimationTypes.Move, "MOVE"}, - { EawModelAnimationTypes.TurnLeft, "TURNL"}, - { EawModelAnimationTypes.TurnRight, "TURNR"}, - { EawModelAnimationTypes.Attack, "ATTACK"}, - { EawModelAnimationTypes.AttackIdle, "ATTACKIDLE"}, - { EawModelAnimationTypes.Die, "DIE"}, - { EawModelAnimationTypes.Rotate, "ROTATE"}, - { EawModelAnimationTypes.SpecialA, "SPECIAL_A"}, - { EawModelAnimationTypes.SpecialB, "SPECIAL_B"}, - { EawModelAnimationTypes.SpecialC, "SPECIAL_C"}, - { EawModelAnimationTypes.TransitionToLeftTurn, "TURNL_BEGIN"}, - { EawModelAnimationTypes.TransitionFromLeftTurn, "TURNL_END"}, - { EawModelAnimationTypes.TransitionToRightTurn, "TURNR_BEGIN"}, - { EawModelAnimationTypes.TransitionFromRightTurn, "TURNR_END"}, - { EawModelAnimationTypes.TransitionToMove, "MOVESTART"}, - { EawModelAnimationTypes.TransitionFromMoveFrame0, "MOVE_ENDONE"}, - { EawModelAnimationTypes.TransitionFromMoveFrame40, "MOVE_ENDTWO"}, - { EawModelAnimationTypes.TransitionFromMoveFrame80, "MOVE_ENDTHREE"}, - { EawModelAnimationTypes.TransitionFromMoveFrame120, "MOVE_ENDFOUR"}, - { EawModelAnimationTypes.TurnLeftHalf, "TURNL_HALF"}, - { EawModelAnimationTypes.TurnLeftQuarter, "TURNL_QUARTER"}, - { EawModelAnimationTypes.TurnRightHalf, "TURNR_HALF"}, - { EawModelAnimationTypes.TurnRightQuarter, "TURNR_QUARTER"}, - { EawModelAnimationTypes.Deploy, "DEPLOY"}, - { EawModelAnimationTypes.Undeploy, "UNDEPLOY"}, - { EawModelAnimationTypes.Cinematic, "CINEMATIC"}, - { EawModelAnimationTypes.BlockBlaster, "BLOCK_BLASTER"}, - { EawModelAnimationTypes.RedirectBlaster, "REDIRECT_BLASTER"}, - { EawModelAnimationTypes.IdleBlockBlaster, "IDLE_BLOCKBLASTER"}, - { EawModelAnimationTypes.ForceWhirlwindAttack, "FW_ATTACK"}, - { EawModelAnimationTypes.ForceWhirlwindDie, "FW_DIE"}, - { EawModelAnimationTypes.ForceTelekinesisAttack, "FTK_ATTACK"}, - { EawModelAnimationTypes.ForceTelekinesisHold, "FTK_HOLD"}, - { EawModelAnimationTypes.ForceTelekinesisRelease, "FTK_RELEASE"}, - { EawModelAnimationTypes.ForceTelekinesisDie, "FTK_DIE"}, - { EawModelAnimationTypes.EarthquakeAttack, "FB_ATTACK"}, - { EawModelAnimationTypes.EarthquakeHold, "FB_HOLD"}, - { EawModelAnimationTypes.EarthquakeRelease, "FB_RELEASE"}, - { EawModelAnimationTypes.ForceLightningAttack, "FL_ATTACK"}, - { EawModelAnimationTypes.ForceLightningDie, "FL_DIE"}, - { EawModelAnimationTypes.ForceRun, "FORCE_RUN"}, - { EawModelAnimationTypes.TransportLanding, "LAND"}, - { EawModelAnimationTypes.TransportLeaving, "TAKEOFF"}, - { EawModelAnimationTypes.FlameAttack, "FLAME_ATTACK"}, - { EawModelAnimationTypes.Demolition, "DEMOLITION"}, - { EawModelAnimationTypes.BombToss, "BOMBTOSS"}, - { EawModelAnimationTypes.Jump, "JUMP"}, - { EawModelAnimationTypes.FlyIdle, "FLYIDLE"}, - { EawModelAnimationTypes.FlyLand, "FLYLAND"}, - { EawModelAnimationTypes.LandIdle, "FLYLANDIDLE"}, - { EawModelAnimationTypes.Land, "FLYLANDDROP"}, - { EawModelAnimationTypes.HcWin, "HC_WIN"}, - { EawModelAnimationTypes.HcLose, "HC_LOSE"}, - { EawModelAnimationTypes.HcDraw, "HC_DRAW"}, - { EawModelAnimationTypes.ShieldOn, "SHIELD_ON"}, - { EawModelAnimationTypes.ShieldOff, "SHIELD_OFF"}, - { EawModelAnimationTypes.CableAttackDie, "CA_DIE"}, - { EawModelAnimationTypes.DeployedCableAttackDie, "DEPLOYED_CA_DIE"}, - { EawModelAnimationTypes.DeployedDie, "DEPLOYED_DIE"}, - { EawModelAnimationTypes.RunAroundOnFire, "FIRE_MOVE"}, - { EawModelAnimationTypes.FireDie, "FIRE_DIE"}, - { EawModelAnimationTypes.PoundAttack, "POUND_ATTACK"}, - { EawModelAnimationTypes.EatAttack, "EAT_ATTACK"}, - { EawModelAnimationTypes.EatDie, "EATEN_DIE"}, - { EawModelAnimationTypes.MoveWalk, "WALKMOVE"}, - { EawModelAnimationTypes.MoveCrouch, "CROUCHMOVE"}, - { EawModelAnimationTypes.StructureOpen, "OPEN"}, - { EawModelAnimationTypes.StructureHold, "HOLD"}, - { EawModelAnimationTypes.StructureClose, "CLOSE"}, - { EawModelAnimationTypes.IdleCrouch, "CROUCHIDLE"}, - { EawModelAnimationTypes.TurnLeftCrouch, "CROUCHTURNL"}, - { EawModelAnimationTypes.TurnRightCrouch, "CROUCHTURNR"}, - { EawModelAnimationTypes.Build, "BUILD"}, - { EawModelAnimationTypes.TransitionOwnership, "TRANS"}, - { EawModelAnimationTypes.SelfDestruct, "SELF_DESTRUCT"}, - { EawModelAnimationTypes.Attention, "ATTENTION"}, - { EawModelAnimationTypes.Celebrate, "CELEBRATE"}, - { EawModelAnimationTypes.FlinchLeft, "FLINCHL"}, - { EawModelAnimationTypes.FlinchRight, "FLINCHR"}, - { EawModelAnimationTypes.FlinchFront, "FLINCHF"}, - { EawModelAnimationTypes.FlinchBack, "FLINCHB"}, - { EawModelAnimationTypes.AttackFlinchLeft, "ATTACKFLINCHL"}, - { EawModelAnimationTypes.AttackFlinchRight, "ATTACKFLINCHR"}, - { EawModelAnimationTypes.AttackFlinchFront, "ATTACKFLINCHF"}, - { EawModelAnimationTypes.AttackFlinchBack, "ATTACKFLINCHB"}, - { EawModelAnimationTypes.Talk, "TALK"}, - { EawModelAnimationTypes.TalkGesture, "TALKGESTURE"}, - { EawModelAnimationTypes.TalkQuestion, "TALKQUESTION"}, - { EawModelAnimationTypes.Hacking, "HACKING"}, - { EawModelAnimationTypes.Repairing, "REPAIRING"}, - { EawModelAnimationTypes.Choke, "CHOKE"}, - { EawModelAnimationTypes.ChokeDie, "CHOKEDEATH"}, - { EawModelAnimationTypes.DropTroopers, "TROOPDROP"}, - { EawModelAnimationTypes.RopeSlide, "ROPESLIDE"}, - { EawModelAnimationTypes.RopeLand, "ROPELAND"}, - { EawModelAnimationTypes.RopeDrop, "ROPE_DROP"}, - { EawModelAnimationTypes.RopeLift, "ROPE_LIFT"}, - { EawModelAnimationTypes.Alarm, "ALARM"}, - { EawModelAnimationTypes.Warning, "WARNING"}, - { EawModelAnimationTypes.Crushed, "CRUSHED"}, - { EawModelAnimationTypes.PowerDown, "POWERDOWN"}, - { EawModelAnimationTypes.PowerUp, "POWERUP"} + { ModelAnimationType.Idle, "IDLE"}, + { ModelAnimationType.SpaceIdle, "SPACE_IDLE"}, + { ModelAnimationType.Move, "MOVE"}, + { ModelAnimationType.TurnLeft, "TURNL"}, + { ModelAnimationType.TurnRight, "TURNR"}, + { ModelAnimationType.Attack, "ATTACK"}, + { ModelAnimationType.AttackIdle, "ATTACKIDLE"}, + { ModelAnimationType.Die, "DIE"}, + { ModelAnimationType.Rotate, "ROTATE"}, + { ModelAnimationType.SpecialA, "SPECIAL_A"}, + { ModelAnimationType.SpecialB, "SPECIAL_B"}, + { ModelAnimationType.SpecialC, "SPECIAL_C"}, + { ModelAnimationType.TransitionToLeftTurn, "TURNL_BEGIN"}, + { ModelAnimationType.TransitionFromLeftTurn, "TURNL_END"}, + { ModelAnimationType.TransitionToRightTurn, "TURNR_BEGIN"}, + { ModelAnimationType.TransitionFromRightTurn, "TURNR_END"}, + { ModelAnimationType.TransitionToMove, "MOVESTART"}, + { ModelAnimationType.TransitionFromMoveFrame0, "MOVE_ENDONE"}, + { ModelAnimationType.TransitionFromMoveFrame40, "MOVE_ENDTWO"}, + { ModelAnimationType.TransitionFromMoveFrame80, "MOVE_ENDTHREE"}, + { ModelAnimationType.TransitionFromMoveFrame120, "MOVE_ENDFOUR"}, + { ModelAnimationType.TurnLeftHalf, "TURNL_HALF"}, + { ModelAnimationType.TurnLeftQuarter, "TURNL_QUARTER"}, + { ModelAnimationType.TurnRightHalf, "TURNR_HALF"}, + { ModelAnimationType.TurnRightQuarter, "TURNR_QUARTER"}, + { ModelAnimationType.Deploy, "DEPLOY"}, + { ModelAnimationType.Undeploy, "UNDEPLOY"}, + { ModelAnimationType.Cinematic, "CINEMATIC"}, + { ModelAnimationType.BlockBlaster, "BLOCK_BLASTER"}, + { ModelAnimationType.RedirectBlaster, "REDIRECT_BLASTER"}, + { ModelAnimationType.IdleBlockBlaster, "IDLE_BLOCKBLASTER"}, + { ModelAnimationType.ForceWhirlwindAttack, "FW_ATTACK"}, + { ModelAnimationType.ForceWhirlwindDie, "FW_DIE"}, + { ModelAnimationType.ForceTelekinesisAttack, "FTK_ATTACK"}, + { ModelAnimationType.ForceTelekinesisHold, "FTK_HOLD"}, + { ModelAnimationType.ForceTelekinesisRelease, "FTK_RELEASE"}, + { ModelAnimationType.ForceTelekinesisDie, "FTK_DIE"}, + { ModelAnimationType.EarthquakeAttack, "FB_ATTACK"}, + { ModelAnimationType.EarthquakeHold, "FB_HOLD"}, + { ModelAnimationType.EarthquakeRelease, "FB_RELEASE"}, + { ModelAnimationType.ForceLightningAttack, "FL_ATTACK"}, + { ModelAnimationType.ForceLightningDie, "FL_DIE"}, + { ModelAnimationType.ForceRun, "FORCE_RUN"}, + { ModelAnimationType.TransportLanding, "LAND"}, + { ModelAnimationType.TransportLeaving, "TAKEOFF"}, + { ModelAnimationType.FlameAttack, "FLAME_ATTACK"}, + { ModelAnimationType.Demolition, "DEMOLITION"}, + { ModelAnimationType.BombToss, "BOMBTOSS"}, + { ModelAnimationType.Jump, "JUMP"}, + { ModelAnimationType.FlyIdle, "FLYIDLE"}, + { ModelAnimationType.FlyLand, "FLYLAND"}, + { ModelAnimationType.LandIdle, "FLYLANDIDLE"}, + { ModelAnimationType.Land, "FLYLANDDROP"}, + { ModelAnimationType.HcWin, "HC_WIN"}, + { ModelAnimationType.HcLose, "HC_LOSE"}, + { ModelAnimationType.HcDraw, "HC_DRAW"}, + { ModelAnimationType.ShieldOn, "SHIELD_ON"}, + { ModelAnimationType.ShieldOff, "SHIELD_OFF"}, + { ModelAnimationType.CableAttackDie, "CA_DIE"}, + { ModelAnimationType.DeployedCableAttackDie, "DEPLOYED_CA_DIE"}, + { ModelAnimationType.DeployedDie, "DEPLOYED_DIE"}, + { ModelAnimationType.RunAroundOnFire, "FIRE_MOVE"}, + { ModelAnimationType.FireDie, "FIRE_DIE"}, + { ModelAnimationType.PoundAttack, "POUND_ATTACK"}, + { ModelAnimationType.EatAttack, "EAT_ATTACK"}, + { ModelAnimationType.EatDie, "EATEN_DIE"}, + { ModelAnimationType.MoveWalk, "WALKMOVE"}, + { ModelAnimationType.MoveCrouch, "CROUCHMOVE"}, + { ModelAnimationType.StructureOpen, "OPEN"}, + { ModelAnimationType.StructureHold, "HOLD"}, + { ModelAnimationType.StructureClose, "CLOSE"}, + { ModelAnimationType.IdleCrouch, "CROUCHIDLE"}, + { ModelAnimationType.TurnLeftCrouch, "CROUCHTURNL"}, + { ModelAnimationType.TurnRightCrouch, "CROUCHTURNR"}, + { ModelAnimationType.Build, "BUILD"}, + { ModelAnimationType.TransitionOwnership, "TRANS"}, + { ModelAnimationType.SelfDestruct, "SELF_DESTRUCT"}, + { ModelAnimationType.Attention, "ATTENTION"}, + { ModelAnimationType.Celebrate, "CELEBRATE"}, + { ModelAnimationType.FlinchLeft, "FLINCHL"}, + { ModelAnimationType.FlinchRight, "FLINCHR"}, + { ModelAnimationType.FlinchFront, "FLINCHF"}, + { ModelAnimationType.FlinchBack, "FLINCHB"}, + { ModelAnimationType.AttackFlinchLeft, "ATTACKFLINCHL"}, + { ModelAnimationType.AttackFlinchRight, "ATTACKFLINCHR"}, + { ModelAnimationType.AttackFlinchFront, "ATTACKFLINCHF"}, + { ModelAnimationType.AttackFlinchBack, "ATTACKFLINCHB"}, + { ModelAnimationType.Talk, "TALK"}, + { ModelAnimationType.TalkGesture, "TALKGESTURE"}, + { ModelAnimationType.TalkQuestion, "TALKQUESTION"}, + { ModelAnimationType.Hacking, "HACKING"}, + { ModelAnimationType.Repairing, "REPAIRING"}, + { ModelAnimationType.Choke, "CHOKE"}, + { ModelAnimationType.ChokeDie, "CHOKEDEATH"}, + { ModelAnimationType.DropTroopers, "TROOPDROP"}, + { ModelAnimationType.RopeSlide, "ROPESLIDE"}, + { ModelAnimationType.RopeLand, "ROPELAND"}, + { ModelAnimationType.RopeDrop, "ROPE_DROP"}, + { ModelAnimationType.RopeLift, "ROPE_LIFT"}, + { ModelAnimationType.Alarm, "ALARM"}, + { ModelAnimationType.Warning, "WARNING"}, + { ModelAnimationType.Crushed, "CRUSHED"}, + { ModelAnimationType.PowerDown, "POWERDOWN"}, + { ModelAnimationType.PowerUp, "POWERUP"} }; + + // ReSharper disable StringLiteralTypo + private static readonly Dictionary FocSupportedAnimations = + + EawSupportedAnimations.Concat(new Dictionary + { + { ModelAnimationType.SpinMove, "SPINMOVE" }, + { ModelAnimationType.ForceRevealBegin, "FORCE_REVEAL_BEGIN" }, + { ModelAnimationType.ForceRevealLoop, "FORCE_REVEAL_LOOP" }, + { ModelAnimationType.ForceRevealEnd, "FORCE_REVEAL_END" }, + { ModelAnimationType.SaberThrow, "SWORD_THROW" }, + { ModelAnimationType.SaberControl, "SWORD_CONTROL" }, + { ModelAnimationType.SaberCatch, "SWORD_CATCH" }, + { ModelAnimationType.SaberSpin, "SWORDSPIN" }, + { ModelAnimationType.ContaminateAttack, "CONTAMINATE_ATTACK" }, + { ModelAnimationType.ContaminateLoop, "CONTAMINATE_LOOP" }, + { ModelAnimationType.ContaminateRelease, "CONTAMINATE_RELEASE" }, + { ModelAnimationType.DeployedWalk, "WALK" }, + { ModelAnimationType.PadBuild, "PAD_BUILD" }, + { ModelAnimationType.PadSell, "PAD_SELL" }, + { ModelAnimationType.Heal, "HEAL" }, + }) + .ToDictionary(x => x.Key, x => x.Value); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/FontManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/FontManager.cs index 00ca4f2..75654f2 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/FontManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/FontManager.cs @@ -15,7 +15,10 @@ internal class FontManager : GameManagerBase, IFontManager private ISet _fontNames = null!; - public FontManager(GameRepository repository, GameEngineErrorReporterWrapper errorReporter, IServiceProvider serviceProvider) + public FontManager( + GameRepository repository, + GameEngineErrorReporterWrapper errorReporter, + IServiceProvider serviceProvider) : base(repository, errorReporter, serviceProvider) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -24,7 +27,14 @@ public FontManager(GameRepository repository, GameEngineErrorReporterWrapper err _fontManager = new NetFontManager(); } - public IReadOnlyCollection FontNames => [.._fontNames]; + public IReadOnlyCollection FontNames + { + get + { + ThrowIfNotInitialized(); + return [.._fontNames]; + } + } public FontData? CreateFont(string fontName, int size, bool bold, bool italic, bool staticSize, float stretchFactor) { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/WindowsFontManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/WindowsFontManager.cs index fe8f546..ef2344f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/WindowsFontManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Font/WindowsFontManager.cs @@ -28,7 +28,8 @@ public IEnumerable GetFontFamilies() return fonts; } - static IEnumerable<(Gdi32.ENUMLOGFONTEXDV lpelfe, Gdi32.ENUMTEXTMETRIC _, Gdi32.FontType __)> GetFonts(Gdi32.SafeHDC hdc) + private static IEnumerable<(Gdi32.ENUMLOGFONTEXDV lpelfe, Gdi32.ENUMTEXTMETRIC _, Gdi32.FontType __)> GetFonts( + Gdi32.SafeHDC hdc) { return Gdi32.EnumFontFamiliesEx(hdc, lfCharSet: CharacterSet.DEFAULT_CHARSET); } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Matrix3x4.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Matrix3x4.cs new file mode 100644 index 0000000..136f647 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Rendering/Matrix3x4.cs @@ -0,0 +1,198 @@ +using System; +using System.Globalization; +using System.Numerics; + +namespace PG.StarWarsGame.Engine.Rendering; + +public struct Matrix3x4 : IEquatable +{ + public float M11; + public float M12; + public float M13; + public float M14; + public float M21; + public float M22; + public float M23; + public float M24; + public float M31; + public float M32; + public float M33; + public float M34; + + public static Matrix3x4 Identity { get; } = new( + 1f, 0.0f, 0.0f, 0.0f, + 0.0f, 1f, 0.0f, 0.0f, + 0.0f, 0.0f, 1f, 0.0f); + + /// Constructs a Matrix3x4 from the given components. + public Matrix3x4( + float m11, + float m12, + float m13, + float m14, + float m21, + float m22, + float m23, + float m24, + float m31, + float m32, + float m33, + float m34) + { + M11 = m11; + M12 = m12; + M13 = m13; + M14 = m14; + M21 = m21; + M22 = m22; + M23 = m23; + M24 = m24; + M31 = m31; + M32 = m32; + M33 = m33; + M34 = m34; + } + + public static Matrix3x4 Scale(Vector3 scale) + { + return Scale(scale.X, scale.Y, scale.Z); + } + + public static Matrix3x4 Scale(float xScale, float yScale, float zScale) + { + return new Matrix3x4 + { + M11 = xScale, M12 = 0, M13 = 0, M14 = 0, + M21 = 0, M22 = yScale, M23 = 0, M24 = 0, + M31 = 0, M32 = 0, M33 = zScale, M34 = 0 + }; + } + + public static Matrix3x4 CreateTranslation(Vector3 position) + { + return CreateTranslation(position.X, position.Y, position.Z); + } + + public static Matrix3x4 CreateTranslation(float xPosition, float yPosition, float zPosition) + { + return new Matrix3x4 + { + M11 = 1, M12 = 0, M13 = 0, M14 = xPosition, + M21 = 0, M22 = 1, M23 = 0, M24 = yPosition, + M31 = 0, M32 = 0, M33 = 1, M34 = zPosition + }; + } + + public static Matrix3x4 operator *(Matrix3x4 value1, Matrix3x4 value2) + { + Matrix3x4 matrix3x4; + matrix3x4.M11 = value1.M11 * value2.M11 + value1.M12 * value2.M21 + value1.M13 * value2.M31; + matrix3x4.M12 = value1.M11 * value2.M12 + value1.M12 * value2.M22 + value1.M13 * value2.M32; + matrix3x4.M13 = value1.M11 * value2.M13 + value1.M12 * value2.M23 + value1.M13 * value2.M33; + matrix3x4.M14 = value1.M11 * value2.M14 + value1.M12 * value2.M24 + value1.M13 * value2.M34 + value1.M14; + matrix3x4.M21 = value1.M21 * value2.M11 + value1.M22 * value2.M21 + value1.M23 * value2.M31; + matrix3x4.M22 = value1.M21 * value2.M12 + value1.M22 * value2.M22 + value1.M23 * value2.M32; + matrix3x4.M23 = value1.M21 * value2.M13 + value1.M22 * value2.M23 + value1.M23 * value2.M33; + matrix3x4.M24 = value1.M21 * value2.M14 + value1.M22 * value2.M24 + value1.M23 * value2.M34 + value1.M24; + matrix3x4.M31 = value1.M31 * value2.M11 + value1.M32 * value2.M21 + value1.M33 * value2.M31; + matrix3x4.M32 = value1.M31 * value2.M12 + value1.M32 * value2.M22 + value1.M33 * value2.M32; + matrix3x4.M33 = value1.M31 * value2.M13 + value1.M32 * value2.M23 + value1.M33 * value2.M33; + matrix3x4.M34 = value1.M31 * value2.M14 + value1.M32 * value2.M24 + value1.M33 * value2.M34 + value1.M34; + return matrix3x4; + } + + public static Matrix3x4 operator *(Matrix3x4 value1, float value2) + { + Matrix3x4 matrix3x4; + matrix3x4.M11 = value1.M11 * value2; + matrix3x4.M12 = value1.M12 * value2; + matrix3x4.M13 = value1.M13 * value2; + matrix3x4.M14 = value1.M14 * value2; + matrix3x4.M21 = value1.M21 * value2; + matrix3x4.M22 = value1.M22 * value2; + matrix3x4.M23 = value1.M23 * value2; + matrix3x4.M24 = value1.M24 * value2; + matrix3x4.M31 = value1.M31 * value2; + matrix3x4.M32 = value1.M32 * value2; + matrix3x4.M33 = value1.M33 * value2; + matrix3x4.M34 = value1.M34 * value2; + return matrix3x4; + } + + /// + /// Returns a boolean indicating whether the given two matrices are equal. + /// + /// The first matrix to compare. + /// The second matrix to compare. + /// True if the given matrices are equal; False otherwise. + public static bool operator ==(Matrix3x4 value1, Matrix3x4 value2) + { + return value1.M11 == (double)value2.M11 && value1.M22 == (double)value2.M22 && value1.M33 == (double)value2.M33 && + value1.M12 == (double)value2.M12 && value1.M13 == (double)value2.M13 && value1.M14 == (double)value2.M14 && + value1.M21 == (double)value2.M21 && value1.M23 == (double)value2.M23 && value1.M24 == (double)value2.M24 && + value1.M31 == (double)value2.M31 && value1.M32 == (double)value2.M32 && value1.M34 == (double)value2.M34; + } + + /// + /// Returns a boolean indicating whether the given two matrices are not equal. + /// + /// The first matrix to compare. + /// The second matrix to compare. + /// True if the given matrices are not equal; False if they are equal. + public static bool operator !=(Matrix3x4 value1, Matrix3x4 value2) + { + return value1.M11 != (double)value2.M11 || value1.M12 != (double)value2.M12 || value1.M13 != (double)value2.M13 || value1.M14 != (double)value2.M14 || + value1.M21 != (double)value2.M21 || value1.M22 != (double)value2.M22 || value1.M23 != (double)value2.M23 || value1.M24 != (double)value2.M24 || + value1.M31 != (double)value2.M31 || value1.M32 != (double)value2.M32 || value1.M33 != (double)value2.M33 || value1.M34 != (double)value2.M34; + } + + /// + /// Returns a boolean indicating whether this matrix instance is equal to the other given matrix. + /// + /// The matrix to compare this instance to. + /// True if the matrices are equal; False otherwise. + public bool Equals(Matrix3x4 other) + { + return M11 == (double)other.M11 && M22 == (double)other.M22 && M33 == (double)other.M33 && + M12 == (double)other.M12 && M13 == (double)other.M13 && M14 == (double)other.M14 && + M21 == (double)other.M21 && M23 == (double)other.M23 && M24 == (double)other.M24 && + M31 == (double)other.M31 && M32 == (double)other.M32 && M34 == (double)other.M34; + } + + /// + /// Returns a boolean indicating whether the given Object is equal to this matrix instance. + /// + /// The Object to compare against. + /// True if the Object is equal to this matrix; False otherwise. + public override bool Equals(object? obj) => obj is Matrix3x4 other && Equals(other); + + /// Returns a String representing this matrix instance. + /// The string representation. + public override string ToString() + { + var currentCulture = CultureInfo.CurrentCulture; + return string.Format(currentCulture, + "{{ {{M11:{0} M12:{1} M13:{2} M14:{3}}} {{M21:{4} M22:{5} M23:{6} M24:{7}}} {{M31:{8} M32:{9} M33:{10} M34:{11}}}", + M11.ToString(currentCulture), + M12.ToString(currentCulture), + M13.ToString(currentCulture), + M14.ToString(currentCulture), + M21.ToString(currentCulture), + M22.ToString(currentCulture), + M23.ToString(currentCulture), + M24.ToString(currentCulture), + M31.ToString(currentCulture), + M32.ToString(currentCulture), + M33.ToString(currentCulture), + M34.ToString(currentCulture)); + } + + /// Returns the hash code for this instance. + /// The hash code. + public override int GetHashCode() + { + return M11.GetHashCode() + M12.GetHashCode() + M13.GetHashCode() + M14.GetHashCode() + + M21.GetHashCode() + M22.GetHashCode() + M23.GetHashCode() + M24.GetHashCode() + + M31.GetHashCode() + M32.GetHashCode() + M33.GetHashCode() + M34.GetHashCode(); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/ExtensionMethods.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/ExtensionMethods.cs new file mode 100644 index 0000000..e67024f --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/ExtensionMethods.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace PG.StarWarsGame.Engine.Utilities; + +internal static class ExtensionMethods +{ + extension(List list) + { + public void ClearAddRange(IEnumerable items) + { + list.Clear(); + list.AddRange(items); + } + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/PGMath.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/PGMath.cs new file mode 100644 index 0000000..bacabc3 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/PGMath.cs @@ -0,0 +1,12 @@ +using System; + +namespace PG.StarWarsGame.Engine.Utilities; + +internal static class PGMath +{ +#if NETSTANDARD2_1_OR_GREATER || NET + public static float Floor(float value) => MathF.Floor(value); +#else + public static float Floor(float value) => (float)Math.Floor(value); +#endif +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/EnumConversionDictionary.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/EnumConversionDictionary.cs new file mode 100644 index 0000000..275fea8 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/EnumConversionDictionary.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace PG.StarWarsGame.Engine.Xml; + +public sealed class EnumConversionDictionary : IReadOnlyCollection> where T : struct, Enum +{ + // This is the value the engine would give you. + internal const string StringNotFoundDummy = "-BAD VALUE-"; + + // Most consumers call the method TryStringToEnum. Thus, we optimize the class for this case + // and accept performance penalties for the EnumToString case. + private readonly IReadOnlyDictionary _dictionary; + + public int Count => _dictionary.Count; + + public EnumConversionDictionary(IEnumerable> entries) + { + var dictionary = new Dictionary(); + var values = dictionary.Values; + foreach (var entry in entries) + { + if (values.Contains(entry.Value)) + throw new InvalidOperationException($"Enum value {entry.Value} already exists!"); + dictionary.Add(entry.Key.ToUpperInvariant(), entry.Value); + } + _dictionary = dictionary; + } + + public bool TryStringToEnum(string key, out T enumValue) + { + key = key.ToUpperInvariant(); + return _dictionary.TryGetValue(key, out enumValue); + } + + public string EnumToString(T enumValue) + { + foreach (var keyValuePair in _dictionary) + { + if (EqualityComparer.Default.Equals(enumValue, keyValuePair.Value)) + return keyValuePair.Key; + } + + return StringNotFoundDummy; + } + + public IEnumerator> GetEnumerator() + { + return _dictionary.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/IPetroglyphXmlFileParserFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/IPetroglyphXmlFileParserFactory.cs index 4532365..787535a 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/IPetroglyphXmlFileParserFactory.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/IPetroglyphXmlFileParserFactory.cs @@ -1,9 +1,10 @@ -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; +using PG.StarWarsGame.Files.XML.Data; +using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Engine.Xml; public interface IPetroglyphXmlFileParserFactory -{ - IPetroglyphXmlFileContainerParser CreateFileParser(IXmlParserErrorReporter? errorReporter) where T : notnull; +{ + NamedXmlObjectParser CreateNamedXmlObjectParser(GameEngineType engine, IXmlParserErrorReporter? errorReporter) + where T : NamedXmlObject; } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/NamedXmlObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/NamedXmlObjectParser.cs new file mode 100644 index 0000000..d397eb2 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/NamedXmlObjectParser.cs @@ -0,0 +1,74 @@ +using System; +using System.Xml.Linq; +using AnakinRaW.CommonUtilities.Collections; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PG.Commons.Hashing; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Data; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.Parsers; + +namespace PG.StarWarsGame.Engine.Xml; + +public abstract class NamedXmlObjectParser : + XmlObjectParserBase>, INamedXmlObjectParser + where T : NamedXmlObject +{ + protected abstract bool UpperCaseNameForCrc { get; } + protected abstract bool UpperCaseNameForObject { get; } + + protected readonly ICrc32HashingService HashingService; + + protected readonly ILogger? Logger; + + protected NamedXmlObjectParser( + GameEngineType engine, + XmlTagMapper tagMapper, + IXmlParserErrorReporter? errorReporter, + IServiceProvider serviceProvider) : base(engine, tagMapper, errorReporter) + { + HashingService = serviceProvider.GetRequiredService(); + Logger = serviceProvider.GetService()?.CreateLogger(GetType()); + } + + public T Parse(XElement element, IReadOnlyFrugalValueListDictionary parsedEntries, out Crc32 nameCrc) + { + var name = GetXmlObjectName(element, out nameCrc); + var namedXmlObject = CreateXmlObject(name, nameCrc, element, parsedEntries, XmlLocationInfo.FromElement(element)); + ParseObject(namedXmlObject, element, false, parsedEntries); + ValidateAndFixupValues(namedXmlObject, element, parsedEntries); + return namedXmlObject; + } + + protected abstract T CreateXmlObject( + string name, + Crc32 nameCrc, + XElement element, + IReadOnlyFrugalValueListDictionary parsedEntries, + XmlLocationInfo location); + + protected virtual Crc32 CreateNameCrc(string name) + { + return UpperCaseNameForCrc + ? HashingService.GetCrc32Upper(name.AsSpan(), XmlFileConstants.XmlEncoding) + : HashingService.GetCrc32(name.AsSpan(), XmlFileConstants.XmlEncoding); + } + + protected string GetXmlObjectName(XElement element, out Crc32 crc32) + { + GetNameAttributeValue(element, out var name, UpperCaseNameForObject); + crc32 = CreateNameCrc(name); + + if (crc32 == default) + { + ErrorReporter?.Report(new XmlError(this, element) + { + Message = $"Name for XML object of type {typeof(T).Name} cannot be empty.", + ErrorKind = XmlParseErrorKind.InvalidValue + }); + } + + return name; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/ParserNotFoundException.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/ParserNotFoundException.cs index 19bc797..fd4fba3 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/ParserNotFoundException.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/ParserNotFoundException.cs @@ -2,7 +2,7 @@ namespace PG.StarWarsGame.Engine.Xml; -public sealed class ParserNotFoundException : Exception +internal sealed class ParserNotFoundException : Exception { public override string Message { get; } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/CommandBarComponentParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/CommandBarComponentParser.cs deleted file mode 100644 index 297b474..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/CommandBarComponentParser.cs +++ /dev/null @@ -1,365 +0,0 @@ -using System; -using System.Collections.ObjectModel; -using System.Xml.Linq; -using AnakinRaW.CommonUtilities.Collections; -using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.CommandBar.Xml; -using PG.StarWarsGame.Engine.Xml.Tags; -using PG.StarWarsGame.Files.XML; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; - -public sealed class CommandBarComponentParser( - IReadOnlyFrugalValueListDictionary parsedElements, - IServiceProvider serviceProvider, - IXmlParserErrorReporter? errorReporter = null) - : XmlObjectParser(parsedElements, serviceProvider, errorReporter) -{ - public override CommandBarComponentData Parse(XElement element, out Crc32 crc32) - { - var name = GetXmlObjectName(element, out crc32, true); - var component = new CommandBarComponentData(name, crc32, XmlLocationInfo.FromElement(element)); - Parse(component, element, default); - ValidateValues(component, element); - component.CoerceValues(); - return component; - } - - protected override bool ParseTag(XElement tag, CommandBarComponentData componentData) - { - switch (tag.Name.LocalName) - { - case CommandBarComponentTags.SelectedTextureName: - componentData.SelectedTextureNames = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case CommandBarComponentTags.BlankTextureName: - componentData.BlankTextureNames = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case CommandBarComponentTags.IconAlternateTextureName: - componentData.IconAlternateTextureNames = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case CommandBarComponentTags.MouseOverTextureName: - componentData.MouseOverTextureNames = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case CommandBarComponentTags.BarTextureName: - componentData.BarTextureNames = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case CommandBarComponentTags.BarOverlayName: - componentData.BarOverlayNames = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case CommandBarComponentTags.AlternateFontName: - componentData.AlternateFontNames = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case CommandBarComponentTags.TooltipText: - componentData.TooltipTexts = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case CommandBarComponentTags.LowerEffectTextureName: - componentData.LowerEffectTextureNames = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case CommandBarComponentTags.UpperEffectTextureName: - componentData.UpperEffectTextureNames = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case CommandBarComponentTags.OverlayTextureName: - componentData.OverlayTextureNames = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case CommandBarComponentTags.Overlay2TextureName: - componentData.Overlay2TextureNames = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - - case CommandBarComponentTags.IconTextureName: - componentData.IconTextureName = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.DisabledTextureName: - componentData.DisabledTextureName = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.FlashTextureName: - componentData.FlashTextureName = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.BuildTextureName: - componentData.BuildTextureName = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.ModelName: - componentData.ModelName = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.BoneName: - componentData.BoneName = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.CursorTextureName: - componentData.CursorTextureName = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.FontName: - componentData.FontName = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.ClickSfx: - componentData.ClickSfx = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.MouseOverSfx: - componentData.MouseOverSfx = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.RightClickSfx: - componentData.RightClickSfx = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.Type: - componentData.Type = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.Group: - componentData.Group = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.AssociatedText: - componentData.AssociatedText = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - - case CommandBarComponentTags.DragAndDrop: - componentData.DragAndDrop = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.DragSelect: - componentData.DragSelect = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.Receptor: - componentData.Receptor = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.Toggle: - componentData.Toggle = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.Tab: - componentData.Tab = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.Hidden: - componentData.Hidden = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.ClearColor: - componentData.ClearColor = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.Disabled: - componentData.Disabled = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.SwapTexture: - componentData.SwapTexture = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.DrawAdditive: - componentData.DrawAdditive = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.Editable: - componentData.Editable = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.TextOutline: - componentData.TextOutline = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.Stackable: - componentData.Stackable = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.ModelOffsetX: - componentData.ModelOffsetX = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.ModelOffsetY: - componentData.ModelOffsetY = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.ScaleModelX: - componentData.ScaleModelX = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.ScaleModelY: - componentData.ScaleModelY = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.Collideable: - componentData.Collideable = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.TextEmboss: - componentData.TextEmboss = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.ShouldGhost: - componentData.ShouldGhost = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.GhostBaseOnly: - componentData.GhostBaseOnly = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.CrossFade: - componentData.CrossFade = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.LeftJustified: - componentData.LeftJustified = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.RightJustified: - componentData.RightJustified = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.NoShell: - componentData.NoShell = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.SnapDrag: - componentData.SnapDrag = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.SnapLocation: - componentData.SnapLocation = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.OffsetRender: - componentData.OffsetRender = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.BlinkFade: - componentData.BlinkFade = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.NoHiddenCollision: - componentData.NoHiddenCollision = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.ManualOffset: - componentData.ManualOffset = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.SelectedAlpha: - componentData.SelectedAlpha = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.PixelAlign: - componentData.PixelAlign = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.CanDragStack: - componentData.CanDragStack = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.CanAnimate: - componentData.CanAnimate = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.LoopAnim: - componentData.LoopAnim = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.SmoothBar: - componentData.SmoothBar = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.OutlinedBar: - componentData.OutlinedBar = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.DragBack: - componentData.DragBack = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.LowerEffectAdditive: - componentData.LowerEffectAdditive = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.UpperEffectAdditive: - componentData.UpperEffectAdditive = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.ClickShift: - componentData.ClickShift = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.TutorialScene: - componentData.TutorialScene = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.DialogScene: - componentData.DialogScene = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.ShouldRenderAtDragPos: - componentData.ShouldRenderAtDragPos = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.DisableDarken: - componentData.DisableDarken = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.AnimateBack: - componentData.AnimateBack = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.AnimateUpperEffect: - componentData.AnimateUpperEffect = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - - case CommandBarComponentTags.Size: - componentData.Size = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.TextOffset: - componentData.TextOffset = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.TextOffset2: - componentData.TextOffset2 = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.Offset: - componentData.Offset = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.DefaultOffset: - componentData.DefaultOffset = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.DefaultOffsetWidescreen: - componentData.DefaultOffsetWidescreen = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.IconOffset: - componentData.IconOffset = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.MouseOverOffset: - componentData.MouseOverOffset = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.DisabledOffset: - componentData.DisabledOffset = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.BuildDialOffset: - componentData.BuildDialOffset = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.BuildDial2Offset: - componentData.BuildDial2Offset = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.LowerEffectOffset: - componentData.LowerEffectOffset = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.UpperEffectOffset: - componentData.UpperEffectOffset = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.OverlayOffset: - componentData.OverlayOffset = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.Overlay2Offset: - componentData.Overlay2Offset = PetroglyphXmlVector2FParser.Instance.Parse(tag); - return true; - - case CommandBarComponentTags.MaxTextLength: - componentData.MaxTextLength = PetroglyphXmlUnsignedIntegerParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.FontPointSize: - componentData.FontPointSize = (int)PetroglyphXmlUnsignedIntegerParser.Instance.Parse(tag); - return true; - - case CommandBarComponentTags.Scale: - componentData.Scale = PetroglyphXmlFloatParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.BlinkRate: - componentData.BlinkRate = PetroglyphXmlFloatParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.MaxTextWidth: - componentData.MaxTextWidth = PetroglyphXmlFloatParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.BlinkDuration: - componentData.BlinkDuration = PetroglyphXmlFloatParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.ScaleDuration: - componentData.ScaleDuration = PetroglyphXmlFloatParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.AnimFps: - componentData.AnimFps = PetroglyphXmlFloatParser.Instance.Parse(tag); - return true; - - case CommandBarComponentTags.BaseLayer: - componentData.BaseLayer = (int)PetroglyphXmlUnsignedIntegerParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.MaxBarLevel: - componentData.MaxBarLevel = (int)PetroglyphXmlUnsignedIntegerParser.Instance.Parse(tag); - return true; - - case CommandBarComponentTags.Color: - componentData.Color = PetroglyphXmlRgbaColorParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.TextColor: - componentData.TextColor = PetroglyphXmlRgbaColorParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.TextColor2: - componentData.TextColor2 = PetroglyphXmlRgbaColorParser.Instance.Parse(tag); - return true; - case CommandBarComponentTags.MaxBarColor: - componentData.MaxBarColor = PetroglyphXmlRgbaColorParser.Instance.Parse(tag); - return true; - - default: return true; - } - } - - private void ValidateValues(CommandBarComponentData xmlData, XElement element) - { - if (xmlData.Name.Length > PGConstants.MaxCommandBarComponentName) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.TooLongData, - $"CommandbarComponent name '{xmlData.Name}' is too long.")); - } - } - - public override CommandBarComponentData Parse(XElement element) => throw new NotSupportedException(); -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs deleted file mode 100644 index 5cac3e2..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using System.Xml.Linq; -using AnakinRaW.CommonUtilities.Collections; -using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.GameObjects; -using PG.StarWarsGame.Files.XML; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; - -public static class GameObjectXmlTags -{ - public const string LandTerrainModelMapping = "Land_Terrain_Model_Mapping"; - public const string GalacticModelName = "Galactic_Model_Name"; - public const string DestroyedGalacticModelName = "Destroyed_Galactic_Model_Name"; - public const string LandModelName = "Land_Model_Name"; - public const string SpaceModelName = "Space_Model_Name"; - public const string ModelName = "Model_Name"; - public const string TacticalModelName = "Tactical_Model_Name"; - public const string GalacticFleetOverrideModelName = "Galactic_Fleet_Override_Model_Name"; - public const string GuiModelName = "GUI_Model_Name"; - public const string LandModelAnimOverrideName = "Land_Model_Anim_Override_Name"; - public const string XxxSpaceModelName = "xxxSpace_Model_Name"; - public const string DamagedSmokeAssetName = "Damaged_Smoke_Asset_Name"; -} - -public sealed class GameObjectParser( - IReadOnlyFrugalValueListDictionary parsedElements, - IServiceProvider serviceProvider, - IXmlParserErrorReporter? errorReporter = null) - : XmlObjectParser(parsedElements, serviceProvider, errorReporter) -{ - public override GameObject Parse(XElement element, out Crc32 crc32) - { - var name = GetXmlObjectName(element, out crc32, true); - var type = GetTagName(element); - var objectType = EstimateType(type); - var gameObject = new GameObject(type, name, crc32, objectType, XmlLocationInfo.FromElement(element)); - - Parse(gameObject, element, default); - - return gameObject; - } - - protected override bool ParseTag(XElement tag, GameObject xmlObject) - { - switch (tag.Name.LocalName) - { - case GameObjectXmlTags.LandTerrainModelMapping: - var mappingValue = CommaSeparatedStringKeyValueListParser.Instance.Parse(tag); - var dict = xmlObject.InternalLandTerrainModelMapping; - foreach (var keyValuePair in mappingValue) - { - if (!dict.ContainsKey(keyValuePair.key)) - dict.Add(keyValuePair.key, keyValuePair.value); - } - return true; - case GameObjectXmlTags.GalacticModelName: - xmlObject.GalacticModel = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case GameObjectXmlTags.DestroyedGalacticModelName: - xmlObject.DestroyedGalacticModel = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case GameObjectXmlTags.LandModelName: - xmlObject.LandModel = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case GameObjectXmlTags.SpaceModelName: - xmlObject.SpaceModel = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case GameObjectXmlTags.ModelName: - xmlObject.ModelName = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case GameObjectXmlTags.TacticalModelName: - xmlObject.TacticalModel = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case GameObjectXmlTags.GalacticFleetOverrideModelName: - xmlObject.GalacticFleetOverrideModel = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case GameObjectXmlTags.GuiModelName: - xmlObject.GuiModel = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case GameObjectXmlTags.LandModelAnimOverrideName: - xmlObject.LandAnimOverrideModel = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case GameObjectXmlTags.XxxSpaceModelName: - xmlObject.XxxSpaceModeModel = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case GameObjectXmlTags.DamagedSmokeAssetName: - xmlObject.DamagedSmokeAssetModel = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - default: return true; // TODO: Once parsing is complete, switch to false. - } - } - - private static GameObjectType EstimateType(string tagName) - { - if (tagName.StartsWith("Props_")) - return GameObjectType.Prop; - if (tagName.StartsWith("CIN_", StringComparison.OrdinalIgnoreCase)) - return GameObjectType.CinematicObject; - - return tagName switch - { - "Container" => GameObjectType.Container, - "GenericHeroUnit" => GameObjectType.GenericHeroUnit, - "GroundBase" => GameObjectType.GroundBase, - "GroundBuildable" => GameObjectType.GroundBuildable, - "GroundCompany" => GameObjectType.GroundCompany, - "GroundInfantry" => GameObjectType.GroundInfantry, - "GroundStructure" => GameObjectType.GroundStructure, - "GroundVehicle" => GameObjectType.GroundVehicle, - "HeroCompany" => GameObjectType.HeroCompany, - "HeroUnit" => GameObjectType.HeroUnit, - "Indigenous_Unit" => GameObjectType.IndigenousUnit, - "LandBombingUnit" => GameObjectType.LandBombingUnit, - "LandPrimarySkydome" => GameObjectType.LandPrimarySkydome, - "LandSecondarySkydome" => GameObjectType.LandSecondarySkydome, - "Marker" => GameObjectType.Marker, - "MiscObject" => GameObjectType.MiscObject, - "Mobile_Defense_Unit" => GameObjectType.MobileDefenseUnit, - "MultiplayerStructureMarker" => GameObjectType.MultiplayerStructureMarker, - "Particle" => GameObjectType.Particle, - "Planet" => GameObjectType.Planet, - "Projectile" => GameObjectType.Projectile, - "ScriptMarker" => GameObjectType.ScriptMarker, - "SecondaryStructure" => GameObjectType.SecondaryStructure, - "SlaveCompany" => GameObjectType.SlaveCompany, - "Slave_Unit" => GameObjectType.SlaveUnit, - "SpaceBuildable" => GameObjectType.SpaceBuildable, - "SpacePrimarySkydome" => GameObjectType.SpacePrimarySkydome, - "SpaceProp" => GameObjectType.SpaceProp, - "SpaceSecondarySkydome" => GameObjectType.SpaceSecondarySkydome, - "SpaceUnit" => GameObjectType.SpaceUnit, - "SpecialEffect" => GameObjectType.SpecialEffect, - "SpecialStructure" => GameObjectType.SpecialStructure, - "Squadron" => GameObjectType.Squadron, - "StarBase" => GameObjectType.StarBase, - "TechBuilding" => GameObjectType.TechBuilding, - "TransportUnit" => GameObjectType.TransportUnit, - "UniqueUnit" => GameObjectType.UniqueUint, - "UpgradeObject" => GameObjectType.UpgradeUnit, - _ => GameObjectType.Unknown - }; - } - - public override GameObject Parse(XElement element) => throw new NotSupportedException(); -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs deleted file mode 100644 index bc3439f..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs +++ /dev/null @@ -1,204 +0,0 @@ -using System; -using System.Collections.ObjectModel; -using System.Xml.Linq; -using AnakinRaW.CommonUtilities.Collections; -using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.Audio.Sfx; -using PG.StarWarsGame.Engine.Xml.Tags; -using PG.StarWarsGame.Files.XML; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; - -public sealed class SfxEventParser( - IReadOnlyFrugalValueListDictionary parsedElements, - IServiceProvider serviceProvider, - IXmlParserErrorReporter? errorReporter = null) - : XmlObjectParser(parsedElements, serviceProvider, errorReporter) -{ - public override SfxEvent Parse(XElement element, out Crc32 crc32) - { - var name = GetXmlObjectName(element, out crc32, true); - var sfxEvent = new SfxEvent(name, crc32, XmlLocationInfo.FromElement(element)); - Parse(sfxEvent, element, default); - ValidateValues(sfxEvent, element); - sfxEvent.CoerceValues(); - return sfxEvent; - } - - private void ValidateValues(SfxEvent sfxEvent, XElement element) - { - if (sfxEvent.Name.Length > PGConstants.MaxSFXEventName) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.TooLongData, - $"SFXEvent name '{sfxEvent.Name}' is too long.")); - } - - if (sfxEvent is { Is2D: true, Is3D: true }) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"SFXEvent '{sfxEvent.Name}' is defined as 2D and 3D.")); - } - - if (sfxEvent.MinVolume > sfxEvent.MaxVolume) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"{SfxEventXmlTags.MinVolume} should not be higher than {SfxEventXmlTags.MaxVolume} for SFXEvent '{sfxEvent.Name}'")); - } - - if (sfxEvent.MinPitch > sfxEvent.MaxPitch) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"{SfxEventXmlTags.MinPitch} should not be higher than {SfxEventXmlTags.MaxPitch} for SFXEvent '{sfxEvent.Name}'")); - } - - if (sfxEvent.MinPan2D > sfxEvent.MaxPan2D) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"{SfxEventXmlTags.MinPan2D} should not be higher than {SfxEventXmlTags.MaxPan2D} for SFXEvent '{sfxEvent.Name}'")); - } - - if (sfxEvent.MinPredelay > sfxEvent.MaxPredelay) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"{SfxEventXmlTags.MinPredelay} should not be higher than {SfxEventXmlTags.MaxPredelay} for SFXEvent '{sfxEvent.Name}'")); - } - - if (sfxEvent.MinPostdelay > sfxEvent.MaxPostdelay) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"{SfxEventXmlTags.MinPostdelay} should not be higher than {SfxEventXmlTags.MaxPostdelay} for SFXEvent '{sfxEvent.Name}'")); - } - } - - protected override bool ParseTag(XElement tag, SfxEvent sfxEvent) - { - switch (tag.Name.LocalName) - { - case SfxEventXmlTags.OverlapTest: - sfxEvent.OverlapTestName = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.ChainedSfxEvent: - sfxEvent.ChainedSfxEventName = PetroglyphXmlStringParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.UsePreset: - { - var presetName = PetroglyphXmlStringParser.Instance.Parse(tag); - var presetNameCrc = HashingService.GetCrc32Upper(presetName.AsSpan(), PGConstants.DefaultPGEncoding); - if (presetNameCrc != default && ParsedElements.TryGetFirstValue(presetNameCrc, out var preset)) - sfxEvent.ApplyPreset(preset); - else - { - OnParseError(new XmlParseErrorEventArgs(tag, - XmlParseErrorKind.MissingReference, $"Cannot to find preset '{presetName}' for SFXEvent '{sfxEvent.Name}'")); - } - return true; - } - - case SfxEventXmlTags.IsPreset: - sfxEvent.IsPreset = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.Is3D: - sfxEvent.Is3D = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.Is2D: - sfxEvent.Is2D = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.IsGui: - sfxEvent.IsGui = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.IsHudVo: - sfxEvent.IsHudVo = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.IsUnitResponseVo: - sfxEvent.IsUnitResponseVo = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.IsAmbientVo: - sfxEvent.IsAmbientVo = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.Localize: - sfxEvent.IsLocalized = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.PlaySequentially: - sfxEvent.PlaySequentially = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.KillsPreviousObjectSFX: - sfxEvent.KillsPreviousObjectsSfx = PetroglyphXmlBooleanParser.Instance.Parse(tag); - return true; - - case SfxEventXmlTags.Samples: - sfxEvent.Samples = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case SfxEventXmlTags.PreSamples: - sfxEvent.PreSamples = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case SfxEventXmlTags.PostSamples: - sfxEvent.PostSamples = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - case SfxEventXmlTags.TextID: - sfxEvent.LocalizedTextIDs = new ReadOnlyCollection(PetroglyphXmlLooseStringListParser.Instance.Parse(tag)); - return true; - - case SfxEventXmlTags.Priority: - sfxEvent.Priority = (byte)PetroglyphXmlIntegerParser.Instance.ParseWithRange(tag, SfxEvent.MinPriorityValue, SfxEvent.MaxPriorityValue); - return true; - case SfxEventXmlTags.MinPitch: - sfxEvent.MinPitch = (byte)PetroglyphXmlIntegerParser.Instance.ParseWithRange(tag, SfxEvent.MinPitchValue, SfxEvent.MaxPitchValue); - return true; - case SfxEventXmlTags.MaxPitch: - sfxEvent.MaxPitch = (byte)PetroglyphXmlIntegerParser.Instance.ParseWithRange(tag, SfxEvent.MinPitchValue, SfxEvent.MaxPitchValue); - return true; - case SfxEventXmlTags.MinPan2D: - sfxEvent.MinPan2D = (byte)PetroglyphXmlIntegerParser.Instance.ParseWithRange(tag, byte.MinValue, SfxEvent.MaxPan2dValue); - return true; - case SfxEventXmlTags.MaxPan2D: - sfxEvent.MaxPan2D = (byte)PetroglyphXmlIntegerParser.Instance.ParseWithRange(tag, byte.MinValue, SfxEvent.MaxPan2dValue); - return true; - case SfxEventXmlTags.PlayCount: - sfxEvent.PlayCount = (sbyte)PetroglyphXmlIntegerParser.Instance.ParseWithRange(tag, SfxEvent.InfinitivePlayCount, sbyte.MaxValue); - return true; - case SfxEventXmlTags.MaxInstances: - sfxEvent.MaxInstances = (sbyte)PetroglyphXmlIntegerParser.Instance.ParseWithRange(tag, SfxEvent.MinMaxInstances, sbyte.MaxValue); - return true; - - case SfxEventXmlTags.Probability: - sfxEvent.Probability = PetroglyphXmlMax100ByteParser.Instance.ParseWithRange(tag, byte.MinValue, SfxEvent.MaxProbability); - return true; - case SfxEventXmlTags.MinVolume: - sfxEvent.MinVolume = PetroglyphXmlMax100ByteParser.Instance.ParseWithRange(tag, byte.MinValue, SfxEvent.MaxVolumeValue); - return true; - case SfxEventXmlTags.MaxVolume: - sfxEvent.MaxVolume = PetroglyphXmlMax100ByteParser.Instance.ParseWithRange(tag, byte.MinValue, SfxEvent.MaxVolumeValue); - return true; - - case SfxEventXmlTags.MinPredelay: - sfxEvent.MinPredelay = PetroglyphXmlUnsignedIntegerParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.MaxPredelay: - sfxEvent.MaxPredelay = PetroglyphXmlUnsignedIntegerParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.MinPostdelay: - sfxEvent.MinPostdelay = PetroglyphXmlUnsignedIntegerParser.Instance.Parse(tag); - return true; - case SfxEventXmlTags.MaxPostdelay: - sfxEvent.MaxPostdelay = PetroglyphXmlUnsignedIntegerParser.Instance.Parse(tag); - return true; - - case SfxEventXmlTags.LoopFadeInSeconds: - sfxEvent.LoopFadeInSeconds = PetroglyphXmlFloatParser.Instance.ParseAtLeast(tag, SfxEvent.MinLoopSeconds); - return true; - case SfxEventXmlTags.LoopFadeOutSeconds: - sfxEvent.LoopFadeOutSeconds = PetroglyphXmlFloatParser.Instance.ParseAtLeast(tag, SfxEvent.MinLoopSeconds); - return true; - case SfxEventXmlTags.VolumeSaturationDistance: - // I think it was planned at some time to support -1.0 and >= 0.0, since you don't get a warning when -1.0 is coded - // but the Engine coerces anything < 0.0 to 0.0. - sfxEvent.VolumeSaturationDistance = PetroglyphXmlFloatParser.Instance.ParseAtLeast(tag, SfxEvent.MinVolumeSaturation); - return true; - default: return false; - } - } - - public override SfxEvent Parse(XElement element) => throw new NotSupportedException(); -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/CommandBarComponentFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/CommandBarComponentFileParser.cs deleted file mode 100644 index 763d244..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/CommandBarComponentFileParser.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Xml.Linq; -using AnakinRaW.CommonUtilities.Collections; -using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.CommandBar.Xml; -using PG.StarWarsGame.Engine.Xml.Parsers.Data; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Engine.Xml.Parsers.File; - -internal class CommandBarComponentFileParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) - : PetroglyphXmlFileContainerParser(serviceProvider, errorReporter) -{ - protected override void Parse(XElement element, IFrugalValueListDictionary parsedElements, string fileName) - { - var parser = new CommandBarComponentParser(parsedElements, ServiceProvider, ErrorReporter); - - if (!element.HasElements) - { - OnParseError(XmlParseErrorEventArgs.FromEmptyRoot(element)); - return; - } - - foreach (var xElement in element.Elements()) - { - var commandBarComponent = parser.Parse(xElement, out var nameCrc); - parsedElements.Add(nameCrc, commandBarComponent); - } - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileParser.cs deleted file mode 100644 index ffeafff..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileParser.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Xml.Linq; -using AnakinRaW.CommonUtilities.Collections; -using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.GameObjects; -using PG.StarWarsGame.Engine.Xml.Parsers.Data; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Engine.Xml.Parsers.File; - -internal class GameObjectFileParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) - : PetroglyphXmlFileContainerParser(serviceProvider, errorReporter) -{ - protected override void Parse(XElement element, IFrugalValueListDictionary parsedElements, string fileName) - { - var parser = new GameObjectParser(parsedElements, ServiceProvider, ErrorReporter); - - foreach (var xElement in element.Elements()) - { - var gameObject = parser.Parse(xElement, out var nameCrc); - parsedElements.Add(nameCrc, gameObject); - } - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GuiDialogParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GuiDialogParser.cs deleted file mode 100644 index 851aa9f..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GuiDialogParser.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Xml.Linq; -using AnakinRaW.CommonUtilities.Collections; -using PG.StarWarsGame.Engine.GuiDialog.Xml; -using PG.StarWarsGame.Files.XML; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Engine.Xml.Parsers.File; - -internal class GuiDialogParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : - PetroglyphXmlFileParser(serviceProvider, errorReporter) -{ - protected override GuiDialogsXml Parse(XElement element, string fileName) - { - var textures = ParseTextures(element.Element("Textures"), fileName); - return new GuiDialogsXml(textures, XmlLocationInfo.FromElement(element)); - } - - private GuiDialogsXmlTextureData ParseTextures(XElement? element, string fileName) - { - if (element is null) - { - OnParseError(new XmlParseErrorEventArgs(new XmlLocationInfo(fileName, null), XmlParseErrorKind.MissingNode, - "Expected node is missing.")); - return new GuiDialogsXmlTextureData([], new XmlLocationInfo(fileName, null)); - } - - var textures = new List(); - - GetAttributeValue(element, "File", out var megaTexture); - GetAttributeValue(element, "Compressed_File", out var compressedMegaTexture); - - foreach (var texture in element.Elements()) - textures.Add(ParseTexture(texture)); - - if (textures.Count == 0) - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.MissingNode, - "Textures must contain at least one child node.")); - - return new GuiDialogsXmlTextureData(textures, XmlLocationInfo.FromElement(element)) - { - MegaTexture = megaTexture, - CompressedMegaTexture = compressedMegaTexture - }; - } - - private XmlComponentTextureData ParseTexture(XElement texture) - { - var componentId = GetTagName(texture); - var textures = new FrugalValueListDictionary(); - - foreach (var entry in texture.Elements()) - textures.Add(entry.Name.ToString(), PetroglyphXmlStringParser.Instance.Parse(entry)); - - return new XmlComponentTextureData(componentId, textures, XmlLocationInfo.FromElement(texture)); - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs deleted file mode 100644 index 841d805..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Xml.Linq; -using AnakinRaW.CommonUtilities.Collections; -using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.Audio.Sfx; -using PG.StarWarsGame.Engine.Xml.Parsers.Data; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Engine.Xml.Parsers.File; - -internal class SfxEventFileParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) - : PetroglyphXmlFileContainerParser(serviceProvider, errorReporter) -{ - protected override void Parse(XElement element, IFrugalValueListDictionary parsedElements, string fileName) - { - var parser = new SfxEventParser(parsedElements, ServiceProvider, ErrorReporter); - - if (!element.HasElements) - { - OnParseError(XmlParseErrorEventArgs.FromEmptyRoot(element)); - return; - } - - foreach (var xElement in element.Elements()) - { - var sfxEvent = parser.Parse(xElement, out var nameCrc); - parsedElements.Add(nameCrc, sfxEvent); - } - - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameConstantsParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/FileObjects/GameConstantsParser.cs similarity index 50% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameConstantsParser.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/FileObjects/GameConstantsParser.cs index 64de5ec..4c7e0d9 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameConstantsParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/FileObjects/GameConstantsParser.cs @@ -1,16 +1,17 @@ using System; using System.Xml.Linq; using PG.StarWarsGame.Engine.GameConstants; +using PG.StarWarsGame.Files.XML; using PG.StarWarsGame.Files.XML.ErrorHandling; using PG.StarWarsGame.Files.XML.Parsers; -namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; +namespace PG.StarWarsGame.Engine.Xml.Parsers; internal class GameConstantsParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : - PetroglyphXmlFileParser(serviceProvider, errorReporter) + XmlFileParser(serviceProvider, errorReporter) { - protected override GameConstantsXml Parse(XElement element, string fileName) + protected override GameConstantsXml ParseRoot(XElement element, string fileName) { - return new GameConstantsXml(); + return new GameConstantsXml(new XmlLocationInfo(fileName, null)); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/FileObjects/GuiDialogParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/FileObjects/GuiDialogParser.cs new file mode 100644 index 0000000..152ec83 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/FileObjects/GuiDialogParser.cs @@ -0,0 +1,186 @@ +using AnakinRaW.CommonUtilities.Collections; +using PG.StarWarsGame.Engine.GuiDialog; +using PG.StarWarsGame.Engine.GuiDialog.Xml; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.Parsers; +using System; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace PG.StarWarsGame.Engine.Xml.Parsers; + +internal class GuiDialogParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : + XmlFileParser(serviceProvider, errorReporter) +{ + protected override GuiDialogsXml ParseRoot(XElement element, string fileName) + { + using var elementsEnumerator = element.Elements().GetEnumerator(); + + var texturesExist = elementsEnumerator.MoveNext(); + var textures = ParseTextures(texturesExist + ? elementsEnumerator.Current + : null!, + fileName); + + return new GuiDialogsXml(textures, XmlLocationInfo.FromElement(element)); + } + + private GuiDialogsXmlTextureData ParseTextures(XElement? element, string fileName) + { + if (element is null) + { + ErrorReporter?.Report(new XmlError(this, locationInfo: new XmlLocationInfo(fileName, null)) + { + ErrorKind = XmlParseErrorKind.MissingNode, + Message = "Unable to read textures for GUI." + }); + return new GuiDialogsXmlTextureData([], new XmlLocationInfo(fileName, null)); + } + + if (element.Name != "Textures") + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.UnexceptedElementName, + Message = "Unable to read textures for GUI." + }); + } + + var textures = new List(); + + GetAttributeValue(element, "File", out var megaTexture); + GetAttributeValue(element, "Compressed_File", out var compressedMegaTexture); + + foreach (var texture in element.Elements()) + textures.Add(ParseTexture(texture)); + + if (textures.Count == 0) + { + ErrorReporter?.Report(new XmlError(this, element) + { + Message = "Missing default texture specifications in GUI XML file!", + ErrorKind = XmlParseErrorKind.MissingNode + }); + } + + return new GuiDialogsXmlTextureData(textures, XmlLocationInfo.FromElement(element)) + { + MegaTexture = megaTexture, + CompressedMegaTexture = compressedMegaTexture + }; + } + + private XmlComponentTextureData ParseTexture(XElement texture) + { + var componentId = GetTagName(texture); + var textures = new FrugalValueListDictionary(); + + foreach (var entry in texture.Elements()) + textures.Add(entry.Name.ToString(), PetroglyphXmlStringParser.Instance.Parse(entry)); + + return new XmlComponentTextureData(componentId, textures, XmlLocationInfo.FromElement(texture)); + } + + + internal static readonly EnumConversionDictionary ComponentTypeDictionary = new([ + new("Button_Left", GuiComponentType.ButtonLeft), + new("Button_Middle", GuiComponentType.ButtonMiddle), + new("Button_Right", GuiComponentType.ButtonRight), + new("Button_Left_Mouse_Over", GuiComponentType.ButtonLeftMouseOver), + new("Button_Middle_Mouse_Over", GuiComponentType.ButtonMiddleMouseOver), + new("Button_Right_Mouse_Over", GuiComponentType.ButtonRightMouseOver), + new("Button_Left_Pressed", GuiComponentType.ButtonLeftPressed), + new("Button_Middle_Pressed", GuiComponentType.ButtonMiddlePressed), + new("Button_Right_Pressed", GuiComponentType.ButtonRightPressed), + new("Button_Left_Disabled", GuiComponentType.ButtonLeftDisabled), + new("Button_Middle_Disabled", GuiComponentType.ButtonMiddleDisabled), + new("Button_Right_Disabled", GuiComponentType.ButtonRightDisabled), + + new("Check_Off", GuiComponentType.CheckOff), + new("Check_On", GuiComponentType.CheckOn), + + new("Dial_Left", GuiComponentType.DialLeft), + new("Dial_Middle", GuiComponentType.DialMiddle), + new("Dial_Right", GuiComponentType.DialRight), + new("Dial_Plus", GuiComponentType.DialPlus), + new("Dial_Plus_Mouse_Over", GuiComponentType.DialPlusMouseOver), + new("Dial_Plus_Pressed", GuiComponentType.DialPlusPressed), + new("Dial_Minus", GuiComponentType.DialMinus), + new("Dial_Minus_Mouse_Over", GuiComponentType.DialMinusMouseOver), + new("Dial_Minus_Pressed", GuiComponentType.DialMinusPressed), + new("Dial_Tab", GuiComponentType.DialTab), + + new("Frame_Bottom", GuiComponentType.FrameBottom), + new("Frame_Bottom_Left", GuiComponentType.FrameBottomLeft), + new("Frame_Bottom_Right", GuiComponentType.FrameBottomRight), + new("Frame_Background", GuiComponentType.FrameBackground), + new("Frame_Left", GuiComponentType.FrameLeft), + new("Frame_Right", GuiComponentType.FrameRight), + new("Frame_Top", GuiComponentType.FrameTop), + new("Frame_Top_Left", GuiComponentType.FrameTopLeft), + new("Frame_Top_Right", GuiComponentType.FrameTopRight), + new("Frame_Top_Transition_Left", GuiComponentType.FrameTopTransitionLeft), + new("Frame_Top_Transition_Right", GuiComponentType.FrameTopTransitionRight), + new("Frame_Bottom_Transition_Left", GuiComponentType.FrameBottomTransitionLeft), + new("Frame_Bottom_Transition_Right", GuiComponentType.FrameBottomTransitionRight), + new("Frame_Left_Transition_Top", GuiComponentType.FrameLeftTransitionTop), + new("Frame_Left_Transition_Bottom", GuiComponentType.FrameLeftTransitionBottom), + new("Frame_Right_Transition_Top", GuiComponentType.FrameRightTransitionTop), + new("Frame_Right_Transition_Bottom", GuiComponentType.FrameRightTransitionBottom), + + new("Radio_Off", GuiComponentType.RadioOff), + new("Radio_On", GuiComponentType.RadioOn), + new("Radio_Disabled", GuiComponentType.RadioDisabled), + new("Radio_Mouse_Over", GuiComponentType.RadioMouseOver), + + new("Scroll_Down_Button", GuiComponentType.ScrollDownButton), + new("Scroll_Down_Button_Pressed", GuiComponentType.ScrollDownButtonPressed), + new("Scroll_Down_Button_Mouse_Over", GuiComponentType.ScrollDownButtonMouseOver), + new("Scroll_Down_Button_Disabled", GuiComponentType.ScrollDownButtonDisabled), + new("Scroll_Middle", GuiComponentType.ScrollMiddle), + new("Scroll_Middle_Disabled", GuiComponentType.ScrollMiddleDisabled), + new("Scroll_Tab", GuiComponentType.ScrollTab), + new("Scroll_Tab_Disabled", GuiComponentType.ScrollTabDisabled), + new("Scroll_Up_Button", GuiComponentType.ScrollUpButton), + new("Scroll_Up_Button_Pressed", GuiComponentType.ScrollUpButtonPressed), + new("Scroll_Up_Button_Mouse_Over", GuiComponentType.ScrollUpButtonMouseOver), + new("Scroll_Up_Button_Disabled", GuiComponentType.ScrollUpButtonDisabled), + + new("Trackbar_Scroll_Down_Button", GuiComponentType.TrackbarScrollDownButton), + new("Trackbar_Scroll_Down_Button_Pressed", GuiComponentType.TrackbarScrollDownButtonPressed), + new("Trackbar_Scroll_Down_Button_Mouse_Over", GuiComponentType.TrackbarScrollDownButtonMouseOver), + new("Trackbar_Scroll_Down_Button_Disabled", GuiComponentType.TrackbarScrollDownButtonDisabled), + new("Trackbar_Scroll_Middle", GuiComponentType.TrackbarScrollMiddle), + new("Trackbar_Scroll_Middle_Disabled", GuiComponentType.TrackbarScrollMiddleDisabled), + new("Trackbar_Scroll_Tab", GuiComponentType.TrackbarScrollTab), + new("Trackbar_Scroll_Tab_Disabled", GuiComponentType.TrackbarScrollTabDisabled), + new("Trackbar_Scroll_Up_Button", GuiComponentType.TrackbarScrollUpButton), + new("Trackbar_Scroll_Up_Button_Pressed", GuiComponentType.TrackbarScrollUpButtonPressed), + new("Trackbar_Scroll_Up_Button_Mouse_Over", GuiComponentType.TrackbarScrollUpButtonMouseOver), + new("Trackbar_Scroll_Up_Button_Disabled", GuiComponentType.TrackbarScrollUpButtonDisabled), + + new("Small_Frame_Bottom", GuiComponentType.SmallFrameBottom), + new("Small_Frame_Bottom_Left", GuiComponentType.SmallFrameBottomLeft), + new("Small_Frame_Bottom_Right", GuiComponentType.SmallFrameBottomRight), + new("Small_Frame_Left", GuiComponentType.SmallFrameMiddleLeft), + new("Small_Frame_Right", GuiComponentType.SmallFrameMiddleRight), + new("Small_Frame_Top", GuiComponentType.SmallFrameTop), + new("Small_Frame_Top_Left", GuiComponentType.SmallFrameTopLeft), + new("Small_Frame_Top_Right", GuiComponentType.SmallFrameTopRight), + new("Small_Frame_Background", GuiComponentType.SmallFrameBackground), + + new("Combo_Box_Popdown_Button", GuiComponentType.ComboboxPopdown), + new("Combo_Box_Popdown_Button_Pressed", GuiComponentType.ComboboxPopdownPressed), + new("Combo_Box_Popdown_Button_Mouse_Over", GuiComponentType.ComboboxPopdownMouseOver), + new("Combo_Box_Text_Box", GuiComponentType.ComboboxTextBox), + new("Combo_Box_Left_Cap", GuiComponentType.ComboboxLeftCap), + + new("Progress_Bar_Left", GuiComponentType.ProgressLeft), + new("Progress_Bar_Middle_Off", GuiComponentType.ProgressMiddleOff), + new("Progress_Bar_Middle_On", GuiComponentType.ProgressMiddleOn), + new("Progress_Bar_Right", GuiComponentType.ProgressRight), + + new("Scanlines", GuiComponentType.Scanlines), + ]); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/GameObjectFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/GameObjectFileParser.cs new file mode 100644 index 0000000..66efb71 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/GameObjectFileParser.cs @@ -0,0 +1,46 @@ +using AnakinRaW.CommonUtilities.Collections; +using PG.Commons.Hashing; +using PG.StarWarsGame.Engine.ErrorReporting; +using PG.StarWarsGame.Engine.GameObjects; +using PG.StarWarsGame.Files.XML.Parsers; +using System; +using System.IO; + +namespace PG.StarWarsGame.Engine.Xml.Parsers; + +internal class GameObjectFileParser( + GameEngineType engine, + IServiceProvider serviceProvider, + IGameEngineErrorReporter? errorReporter) + : PetroglyphXmlFileParserBase(serviceProvider, errorReporter), IXmlContainerFileParser +{ + public event EventHandler? GameObjectParsed; + + private readonly GameObjectParser _gameObjectParser = new(engine, serviceProvider, errorReporter); + + public INamedXmlObjectParser ElementParser => _gameObjectParser; + + public bool OverlayLoad + { + get; + set + { + field = value; + _gameObjectParser.OverlayLoad = value; + } + } + + public void ParseFile(Stream xmlStream, IFrugalValueListDictionary parsedEntries) + { + var root = GetRootElement(xmlStream, out _); + foreach (var xElement in root.Elements()) + { + var parsedElement = _gameObjectParser.Parse(xElement, parsedEntries, out var entryCrc); + if (!OverlayLoad) + { + parsedEntries.Add(entryCrc, parsedElement); + GameObjectParsed?.Invoke(this, new GameObjectParsedEventArgs(parsedElement, true)); + } + } + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/GameObjectParsedEventArgs.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/GameObjectParsedEventArgs.cs new file mode 100644 index 0000000..692c423 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/GameObjectParsedEventArgs.cs @@ -0,0 +1,17 @@ +using System; +using PG.StarWarsGame.Engine.GameObjects; + +namespace PG.StarWarsGame.Engine.Xml.Parsers; + +internal sealed class GameObjectParsedEventArgs : EventArgs +{ + public bool Unique { get; } + + public GameObject GameObject { get; } + + internal GameObjectParsedEventArgs(GameObject gameObject, bool unique) + { + Unique = unique; + GameObject = gameObject ?? throw new ArgumentNullException(nameof(gameObject)); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/NamedObjects/CommandBarComponentParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/NamedObjects/CommandBarComponentParser.cs new file mode 100644 index 0000000..ced9c3c --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/NamedObjects/CommandBarComponentParser.cs @@ -0,0 +1,584 @@ +using AnakinRaW.CommonUtilities.Collections; +using PG.StarWarsGame.Engine.CommandBar.Xml; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.Parsers; +using System; +using System.Xml.Linq; +using Crc32 = PG.Commons.Hashing.Crc32; + +namespace PG.StarWarsGame.Engine.Xml.Parsers; + +internal class CommandBarComponentParser( + GameEngineType engine, + IServiceProvider serviceProvider, + IXmlParserErrorReporter? errorReporter = null) + : NamedXmlObjectParser(engine, new CommandBarComponentDataXmlTagMapper(serviceProvider), errorReporter, serviceProvider) +{ + protected override bool UpperCaseNameForCrc => true; + protected override bool UpperCaseNameForObject => false; + + protected override CommandBarComponentData CreateXmlObject( + string name, + Crc32 nameCrc, + XElement element, + IReadOnlyFrugalValueListDictionary parsedEntries, + XmlLocationInfo location) + { + return new CommandBarComponentData(name, nameCrc, location); + } + + + protected override void ValidateAndFixupValues(CommandBarComponentData xmlData, XElement element, in IReadOnlyFrugalValueListDictionary parsedEntries) + { + if (xmlData.Name.Length > PGConstants.MaxCommandBarComponentName) + { + ErrorReporter?.Report(new XmlError(this, element) + { + Message = $"CommandbarComponent name '{xmlData.Name}' is too long.", + ErrorKind = XmlParseErrorKind.TooLongData + }); + } + + xmlData.FixupValues(); + } + + private sealed class CommandBarComponentDataXmlTagMapper(IServiceProvider serviceProvider) + : XmlTagMapper(serviceProvider) + { + protected override void BuildMappings() + { + AddMapping( + CommandBarComponentTags.SelectedTextureName, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.SelectedTextureNamesInternal, val, replace)); + AddMapping( + CommandBarComponentTags.BlankTextureName, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.BlankTextureNamesInternal, val, replace)); + AddMapping( + CommandBarComponentTags.IconTextureName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.IconTextureName = val); + AddMapping( + CommandBarComponentTags.IconAlternateTextureName, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.IconAlternateTextureNamesInternal, val, replace)); + AddMapping( + CommandBarComponentTags.MouseOverTextureName, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.MouseOverTextureNamesInternal, val, replace)); + AddMapping( + CommandBarComponentTags.DisabledTextureName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.DisabledTextureName = val); + AddMapping( + CommandBarComponentTags.FlashTextureName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.FlashTextureName = val); + AddMapping( + CommandBarComponentTags.BarTextureName, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.BarTextureNamesInternal, val, replace)); + AddMapping( + CommandBarComponentTags.BarOverlayName, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.BarOverlayNamesInternal, val, replace)); + AddMapping( + CommandBarComponentTags.BuildTextureName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.BuildTextureName = val); + AddMapping( + CommandBarComponentTags.ModelName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.ModelName = val); + AddMapping( + CommandBarComponentTags.BoneName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.BoneName = val); + AddMapping( + CommandBarComponentTags.CursorTextureName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.CursorTextureName = val); + AddMapping( + CommandBarComponentTags.FontName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.FontName = val); + AddMapping( + CommandBarComponentTags.AlternateFontName, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.AlternateFontNamesInternal, val, replace)); + AddMapping( + CommandBarComponentTags.TooltipText, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.TooltipTextsInternal, val, replace)); + AddMapping( + CommandBarComponentTags.ClickSfx, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.ClickSfx = val); + AddMapping( + CommandBarComponentTags.MouseOverSfx, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.MouseOverSfx = val); + AddMapping( + CommandBarComponentTags.LowerEffectTextureName, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.LowerEffectTextureNamesInternal, val, replace)); + AddMapping( + CommandBarComponentTags.UpperEffectTextureName, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.UpperEffectTextureNamesInternal, val, replace)); + AddMapping( + CommandBarComponentTags.OverlayTextureName, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.OverlayTextureNamesInternal, val, replace)); + AddMapping( + CommandBarComponentTags.Overlay2TextureName, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.Overlay2TextureNamesInternal, val, replace)); + AddMapping( + CommandBarComponentTags.RightClickSfx, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.RightClickSfx = val); + AddMapping( + CommandBarComponentTags.Type, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.Type = val); + AddMapping( + CommandBarComponentTags.Group, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.Group = val); + AddMapping( + CommandBarComponentTags.DragAndDrop, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.DragAndDrop = val); + AddMapping( + CommandBarComponentTags.DragSelect, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.DragSelect = val); + AddMapping( + CommandBarComponentTags.Receptor, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.Receptor = val); + AddMapping( + CommandBarComponentTags.Toggle, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.Toggle = val); + AddMapping( + CommandBarComponentTags.Tab, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.Tab = val); + AddMapping( + CommandBarComponentTags.AssociatedText, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.AssociatedText = val); + AddMapping( + CommandBarComponentTags.Hidden, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.Hidden = val); + AddMapping( + CommandBarComponentTags.Scale, + PetroglyphXmlFloatParser.Instance.Parse, + (obj, val) => obj.Scale = val); + AddMapping( + CommandBarComponentTags.Color, + PetroglyphXmlRgbaColorParser.Instance.Parse, + (obj, val) => obj.Color = val); + AddMapping( + CommandBarComponentTags.TextColor, + PetroglyphXmlRgbaColorParser.Instance.Parse, + (obj, val) => obj.TextColor = val); + AddMapping( + CommandBarComponentTags.TextColor2, + PetroglyphXmlRgbaColorParser.Instance.Parse, + (obj, val) => obj.TextColor2 = val); + AddMapping( + CommandBarComponentTags.Size, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.Size = val); + AddMapping( + CommandBarComponentTags.ClearColor, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.ClearColor = val); + AddMapping( + CommandBarComponentTags.Disabled, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.Disabled = val); + AddMapping( + CommandBarComponentTags.SwapTexture, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.SwapTexture = val); + AddMapping( + CommandBarComponentTags.BaseLayer, + x => (int)PetroglyphXmlUnsignedIntegerParser.Instance.Parse(x), + (obj, val) => obj.BaseLayer = val); + AddMapping( + CommandBarComponentTags.DrawAdditive, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.DrawAdditive = val); + AddMapping( + CommandBarComponentTags.TextOffset, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.TextOffset = val); + AddMapping( + CommandBarComponentTags.TextOffset2, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.TextOffset2 = val); + AddMapping( + CommandBarComponentTags.Offset, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.Offset = val); + AddMapping( + CommandBarComponentTags.DefaultOffset, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.DefaultOffset = val); + AddMapping( + CommandBarComponentTags.DefaultOffsetWidescreen, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.DefaultOffsetWidescreen = val); + AddMapping( + CommandBarComponentTags.IconOffset, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.IconOffset = val); + AddMapping( + CommandBarComponentTags.MouseOverOffset, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.MouseOverOffset = val); + AddMapping( + CommandBarComponentTags.DisabledOffset, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.DisabledOffset = val); + AddMapping( + CommandBarComponentTags.BuildDialOffset, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.BuildDialOffset = val); + AddMapping( + CommandBarComponentTags.BuildDial2Offset, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.BuildDial2Offset = val); + AddMapping( + CommandBarComponentTags.LowerEffectOffset, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.LowerEffectOffset = val); + AddMapping( + CommandBarComponentTags.UpperEffectOffset, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.UpperEffectOffset = val); + AddMapping( + CommandBarComponentTags.OverlayOffset, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.OverlayOffset = val); + AddMapping( + CommandBarComponentTags.Overlay2Offset, + PetroglyphXmlVector2FParser.Instance.Parse, + (obj, val) => obj.Overlay2Offset = val); + AddMapping( + CommandBarComponentTags.Editable, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.Editable = val); + AddMapping( + CommandBarComponentTags.MaxTextLength, + PetroglyphXmlUnsignedIntegerParser.Instance.Parse, + (obj, val) => obj.MaxTextLength = val); + AddMapping( + CommandBarComponentTags.BlinkRate, + PetroglyphXmlFloatParser.Instance.Parse, + (obj, val) => obj.BlinkRate = val); + AddMapping( + CommandBarComponentTags.FontPointSize, + x => (int)PetroglyphXmlUnsignedIntegerParser.Instance.Parse(x), + (obj, val) => obj.FontPointSize = val); + AddMapping( + CommandBarComponentTags.TextOutline, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.TextOutline = val); + AddMapping( + CommandBarComponentTags.MaxTextWidth, + PetroglyphXmlFloatParser.Instance.Parse, + (obj, val) => obj.MaxTextWidth = val); + AddMapping( + CommandBarComponentTags.Stackable, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.Stackable = val); + AddMapping( + CommandBarComponentTags.ModelOffsetX, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.ModelOffsetX = val); + AddMapping( + CommandBarComponentTags.ModelOffsetY, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.ModelOffsetY = val); + AddMapping( + CommandBarComponentTags.ScaleModelX, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.ScaleModelX = val); + AddMapping( + CommandBarComponentTags.ScaleModelY, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.ScaleModelY = val); + AddMapping( + CommandBarComponentTags.Collideable, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.Collideable = val); + AddMapping( + CommandBarComponentTags.TextEmboss, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.TextEmboss = val); + AddMapping( + CommandBarComponentTags.ShouldGhost, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.ShouldGhost = val); + AddMapping( + CommandBarComponentTags.GhostBaseOnly, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.GhostBaseOnly = val); + AddMapping( + CommandBarComponentTags.MaxBarLevel, + x => PetroglyphXmlIntegerParser.Instance.Parse(x), + (obj, val) => obj.MaxBarLevel = val); + AddMapping( + CommandBarComponentTags.MaxBarColor, + PetroglyphXmlRgbaColorParser.Instance.Parse, + (obj, val) => obj.MaxBarColor = val); + AddMapping( + CommandBarComponentTags.CrossFade, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.CrossFade = val); + AddMapping( + CommandBarComponentTags.LeftJustified, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.LeftJustified = val); + AddMapping( + CommandBarComponentTags.RightJustified, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.RightJustified = val); + AddMapping( + CommandBarComponentTags.NoShell, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.NoShell = val); + AddMapping( + CommandBarComponentTags.SnapDrag, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.SnapDrag = val); + AddMapping( + CommandBarComponentTags.SnapLocation, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.SnapLocation = val); + AddMapping( + CommandBarComponentTags.BlinkDuration, + PetroglyphXmlFloatParser.Instance.Parse, + (obj, val) => obj.BlinkDuration = val); + AddMapping( + CommandBarComponentTags.ScaleDuration, + PetroglyphXmlFloatParser.Instance.Parse, + (obj, val) => obj.ScaleDuration = val); + AddMapping( + CommandBarComponentTags.OffsetRender, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.OffsetRender = val); + AddMapping( + CommandBarComponentTags.BlinkFade, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.BlinkFade = val); + AddMapping( + CommandBarComponentTags.NoHiddenCollision, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.NoHiddenCollision = val); + AddMapping( + CommandBarComponentTags.ManualOffset, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.ManualOffset = val); + AddMapping( + CommandBarComponentTags.SelectedAlpha, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.SelectedAlpha = val); + AddMapping( + CommandBarComponentTags.PixelAlign, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.PixelAlign = val); + AddMapping( + CommandBarComponentTags.CanDragStack, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.CanDragStack = val); + AddMapping( + CommandBarComponentTags.CanAnimate, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.CanAnimate = val); + AddMapping( + CommandBarComponentTags.AnimFps, + PetroglyphXmlFloatParser.Instance.Parse, + (obj, val) => obj.AnimFps = val); + AddMapping( + CommandBarComponentTags.LoopAnim, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.LoopAnim = val); + AddMapping( + CommandBarComponentTags.SmoothBar, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.SmoothBar = val); + AddMapping( + CommandBarComponentTags.OutlinedBar, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.OutlinedBar = val); + AddMapping( + CommandBarComponentTags.DragBack, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.DragBack = val); + AddMapping( + CommandBarComponentTags.LowerEffectAdditive, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.LowerEffectAdditive = val); + AddMapping( + CommandBarComponentTags.UpperEffectAdditive, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.UpperEffectAdditive = val); + AddMapping( + CommandBarComponentTags.ClickShift, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.ClickShift = val); + AddMapping( + CommandBarComponentTags.TutorialScene, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.TutorialScene = val); + AddMapping( + CommandBarComponentTags.DialogScene, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.DialogScene = val); + AddMapping( + CommandBarComponentTags.ShouldRenderAtDragPos, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.ShouldRenderAtDragPos = val); + AddMapping( + CommandBarComponentTags.DisableDarken, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.DisableDarken = val); + AddMapping( + CommandBarComponentTags.AnimateBack, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.AnimateBack = val); + AddMapping( + CommandBarComponentTags.AnimateUpperEffect, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.AnimateUpperEffect = val); + } + } + + internal static class CommandBarComponentTags + { + public const string SelectedTextureName = "Selected_Texture_Name"; + public const string BlankTextureName = "Blank_Texture_Name"; + public const string IconTextureName = "Icon_Texture_Name"; + public const string IconAlternateTextureName = "Icon_Alternate_Texture_Name"; + public const string MouseOverTextureName = "Mouse_Over_Texture_Name"; + public const string DisabledTextureName = "Disabled_Texture_Name"; + public const string FlashTextureName = "Flash_Texture_Name"; + public const string BarTextureName = "Bar_Texture_Name"; + public const string BarOverlayName = "Bar_Overlay_Name"; + public const string BuildTextureName = "Build_Texture_Name"; + public const string ModelName = "Model_Name"; + public const string BoneName = "Bone_Name"; + public const string CursorTextureName = "Cursor_Texture_Name"; + public const string FontName = "Font_Name"; + public const string AlternateFontName = "Alternate_Font_Name"; + public const string TooltipText = "Tooltip_Text"; + public const string ClickSfx = "Click_SFX"; + public const string MouseOverSfx = "Mouse_Over_SFX"; + public const string LowerEffectTextureName = "Lower_Effect_Texture_Name"; + public const string UpperEffectTextureName = "Upper_Effect_Texture_Name"; + public const string OverlayTextureName = "Overlay_Texture_Name"; + public const string Overlay2TextureName = "Overlay2_Texture_Name"; + public const string RightClickSfx = "Right_Click_SFX"; + public const string Type = "Type"; + public const string Group = "Group"; + public const string DragAndDrop = "Drag_And_Drop"; + public const string DragSelect = "Drag_Select"; + public const string Receptor = "Receptor"; + public const string Toggle = "Toggle"; + public const string Tab = "Tab"; + public const string AssociatedText = "Associated_Text"; + public const string Hidden = "Hidden"; + public const string Scale = "Scale"; + public const string Color = "Color"; + public const string TextColor = "Text_Color"; + public const string TextColor2 = "Text_Color2"; + public const string Size = "Size"; + public const string ClearColor = "Clear_Color"; + public const string Disabled = "Disabled"; + public const string SwapTexture = "Swap_Texture"; + public const string BaseLayer = "Base_Layer"; + public const string DrawAdditive = "Draw_Additive"; + public const string TextOffset = "Text_Offset"; + public const string TextOffset2 = "Text_Offset2"; + public const string Offset = "Offset"; + public const string DefaultOffset = "Default_Offset"; + public const string DefaultOffsetWidescreen = "Default_Offset_Widescreen"; + public const string IconOffset = "Icon_Offset"; + public const string MouseOverOffset = "Mouse_Over_Offset"; + public const string DisabledOffset = "Disabled_Offset"; + public const string BuildDialOffset = "Build_Dial_Offset"; + public const string BuildDial2Offset = "Build_Dial2_Offset"; + public const string LowerEffectOffset = "Lower_Effect_Offset"; + public const string UpperEffectOffset = "Upper_Effect_Offset"; + public const string OverlayOffset = "Overlay_Offset"; + public const string Overlay2Offset = "Overlay2_Offset"; + public const string Editable = "Editable"; + public const string MaxTextLength = "Max_Text_Length"; + public const string BlinkRate = "Blink_Rate"; + public const string FontPointSize = "Font_Point_Size"; + public const string TextOutline = "Text_Outline"; + public const string MaxTextWidth = "Max_Text_Width"; + public const string Stackable = "Stackable"; + public const string ModelOffsetX = "Model_Offset_X"; + public const string ModelOffsetY = "Model_Offset_Y"; + public const string ScaleModelX = "Scale_Model_X"; + public const string ScaleModelY = "Scale_Model_Y"; + public const string Collideable = "Collideable"; + public const string TextEmboss = "Text_Emboss"; + public const string ShouldGhost = "Should_Ghost"; + public const string GhostBaseOnly = "Ghost_Base_Only"; + public const string MaxBarLevel = "Max_Bar_Level"; + public const string MaxBarColor = "Max_Bar_Color"; + public const string CrossFade = "Cross_Fade"; + public const string LeftJustified = "Left_Justified"; + public const string RightJustified = "Right_Justified"; + public const string NoShell = "No_Shell"; + public const string SnapDrag = "Snap_Drag"; + public const string SnapLocation = "Snap_Location"; + public const string BlinkDuration = "Blink_Duration"; + public const string ScaleDuration = "Scale_Duration"; + public const string OffsetRender = "Offset_Render"; + public const string BlinkFade = "Blink_Fade"; + public const string NoHiddenCollision = "No_Hidden_Collision"; + public const string ManualOffset = "Manual_Offset"; + public const string SelectedAlpha = "Selected_Alpha"; + public const string PixelAlign = "Pixel_Align"; + public const string CanDragStack = "Can_Drag_Stack"; + public const string CanAnimate = "Can_Animate"; + public const string AnimFps = "Anim_FPS"; + public const string LoopAnim = "Loop_Anim"; + public const string SmoothBar = "Smooth_Bar"; + public const string OutlinedBar = "Outlined_Bar"; + public const string DragBack = "Drag_Back"; + public const string LowerEffectAdditive = "Lower_Effect_Additive"; + public const string UpperEffectAdditive = "Upper_Effect_Additive"; + public const string ClickShift = "Click_Shift"; + public const string TutorialScene = "Tutorial_Scene"; + public const string DialogScene = "Dialog_Scene"; + public const string ShouldRenderAtDragPos = "Should_Render_At_Drag_Pos"; + public const string DisableDarken = "Disable_Darken"; + public const string AnimateBack = "Animate_Back"; + public const string AnimateUpperEffect = "Animate_Upper_Effect"; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/NamedObjects/GameObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/NamedObjects/GameObjectParser.cs new file mode 100644 index 0000000..f68f0f2 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/NamedObjects/GameObjectParser.cs @@ -0,0 +1,233 @@ +using AnakinRaW.CommonUtilities.Collections; +using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine.GameObjects; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.Parsers; +using System; +using System.Diagnostics; +using System.Xml.Linq; +using Crc32 = PG.Commons.Hashing.Crc32; + +namespace PG.StarWarsGame.Engine.Xml.Parsers; + +internal partial class GameObjectParser( + GameEngineType engine, + IServiceProvider serviceProvider, + IXmlParserErrorReporter? errorReporter = null) + : NamedXmlObjectParser(engine, new GameObjectXmlTagMapper(serviceProvider), errorReporter, serviceProvider) +{ + internal bool OverlayLoad { get; set; } + + protected override bool UpperCaseNameForCrc => true; + + protected override bool UpperCaseNameForObject => true; + + protected override GameObject CreateXmlObject( + string name, + Crc32 nameCrc, + XElement element, + IReadOnlyFrugalValueListDictionary parsedEntries, + XmlLocationInfo location) + { + if (OverlayLoad) + { + parsedEntries.TryGetFirstValue(nameCrc, out var type); + Debug.Assert(type is not null); + OverlayType(type, element, parsedEntries); + return type; + } + + // The engine actually manages a CRC table of the classification names, + // but since we uppercase the name and this feature is nowhere used, + // except for "MultiplayerStructureMarker", + // we can just use the name as the classification. + var classificationName = GetTagName(element).ToUpperInvariant(); + var gameObjectType = new GameObject(name, classificationName, nameCrc, parsedEntries.ValueCount, location); + if (Logger != null) + LogCreatingNewGameObjectType(Logger, gameObjectType.Name); + return gameObjectType; + } + + protected override void ParseObject( + GameObject xmlObject, + XElement element, + bool replace, + in IReadOnlyFrugalValueListDictionary parsedEntries) + { + if (element.HasElements && element.Attribute("SubObjectList")?.Value == "Yes") + { + // TODO + return; + } + + if (OverlayLoad) + { + OverlayType(xmlObject, element, parsedEntries); + } + else + { + base.ParseObject(xmlObject, element, replace, in parsedEntries); + } + } + + protected override void ValidateAndFixupValues(GameObject gameObject, XElement element, in IReadOnlyFrugalValueListDictionary parsedEntries) + { + if (!OverlayLoad) + { + // TODO + //BehaviorClass.AddImpliedBehaviors(this, BehaviorNames); + //InitBehaviorMap(); + + PostLoadFixup(gameObject); + if (string.IsNullOrEmpty(gameObject.VariantOfExistingTypeName)) + gameObject.IsLoadingComplete = true; + else + OverlayType(gameObject, element, parsedEntries); + } + } + + private void OverlayType(GameObject gameObject, XElement element, IReadOnlyFrugalValueListDictionary parsedEntries) + { + var baseType = gameObject.VariantOfExistingType; + if (baseType is null) + { + var baseTypeName = gameObject.VariantOfExistingTypeName; + if (string.IsNullOrEmpty(baseTypeName)) + return; + + var nameCrc = CreateNameCrc(baseTypeName); + + parsedEntries.TryGetFirstValue(nameCrc, out baseType); + if (baseType is null) + return; + } + OverlayType(baseType, gameObject, element); + } + + private void OverlayType(GameObject baseType, GameObject derivedType, XElement element) + { + if (!baseType.IsLoadingComplete) + return; + + derivedType.ApplyBaseType(baseType); + + ParseTags(derivedType, element, true, ReadOnlyFrugalValueListDictionary.Empty); + + PostLoadFixup(derivedType); + derivedType.IsLoadingComplete = true; + } + + protected override bool ParseTag( + XElement tag, + GameObject xmlObject, + bool replace, + in IReadOnlyFrugalValueListDictionary parseState) + { + // The engine ignores the return value, but we do not, so we can report unknown tags. + base.ParseTag(tag, xmlObject, replace, in parseState); + + // TODO: Once parsing is complete, return original parse result. + return true; + } + + private void PostLoadFixup(GameObject gameObject) + { + // TODO: + // MaxSpeed *= 1.0; + // MaxThrust *= 1.0; + // Asserts and some coercions + + // The engine loads references for scripts, images, hardpoints, etc., + // but we don't do that here. + } + + private sealed class GameObjectXmlTagMapper(IServiceProvider serviceProvider) : XmlTagMapper(serviceProvider) + { + protected override void BuildMappings() + { + AddMapping( + GameObjectXmlTags.GalacticModelName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.GalacticModel = val); + AddMapping( + GameObjectXmlTags.GalacticFleetOverrideModelName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.GalacticFleetOverrideModel = val); + AddMapping( + GameObjectXmlTags.DestroyedGalacticModelName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.DestroyedGalacticModel = val); + AddMapping( + GameObjectXmlTags.LandModelName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.LandModel = val); + AddMapping( + GameObjectXmlTags.LandTerrainModelMapping, + CommaSeparatedStringKeyValueListParser.Instance.Parse, + (obj, val, replace) => + SetOrReplaceList(obj.InternalLandTerrainModelMapping, val, replace)); + AddMapping( + GameObjectXmlTags.SpaceModelName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.SpaceModel = val); + AddMapping( + GameObjectXmlTags.LandModelAnimOverrideName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.LandAnimOverrideModel = val); + AddMapping( + GameObjectXmlTags.SpaceModelAnimOverrideName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.SpaceAnimOverrideModel = val); + + AddMapping( + GameObjectXmlTags.CompanyUnits, + PetroglyphXmlLooseStringListParser.Instance.Parse, + // The MULTI_OBJECT_REFERENCE parser never replaces (this is done by the MultiReferenceList itself) + (obj, val) => obj.GroundCompanyUnits.AddRange(val)); + + AddMapping( + GameObjectXmlTags.DamagedSmokeAssetName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.DamagedSmokeAssetModel = val); + + + AddMapping( + GameObjectXmlTags.GuiModelName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.GuiModel = val); + + AddMapping( + GameObjectXmlTags.IconName, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.IconName = val); + + AddMapping( + GameObjectXmlTags.VariantOfExistingType, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.VariantOfExistingTypeName = val); + } + } + + [LoggerMessage(LogLevel.Debug, "--- Creating new GameObjectTypeClass for key '{objectName}'")] + static partial void LogCreatingNewGameObjectType(ILogger logger, string objectName); + + internal static class GameObjectXmlTags + { + public const string LandTerrainModelMapping = "Land_Terrain_Model_Mapping"; + public const string GalacticModelName = "Galactic_Model_Name"; + public const string DestroyedGalacticModelName = "Destroyed_Galactic_Model_Name"; + public const string LandModelName = "Land_Model_Name"; + public const string SpaceModelName = "Space_Model_Name"; + public const string GalacticFleetOverrideModelName = "Galactic_Fleet_Override_Model_Name"; + public const string GuiModelName = "GUI_Model_Name"; + public const string LandModelAnimOverrideName = "Land_Model_Anim_Override_Name"; + public const string SpaceModelAnimOverrideName = "Space_Model_Anim_Override_Name"; + public const string DamagedSmokeAssetName = "Damaged_Smoke_Asset_Name"; + + public const string VariantOfExistingType = "Variant_Of_Existing_Type"; + + public const string IconName = "Icon_Name"; + public const string CompanyUnits = "Company_Units"; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/NamedObjects/SfxEventParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/NamedObjects/SfxEventParser.cs new file mode 100644 index 0000000..b20f217 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/NamedObjects/SfxEventParser.cs @@ -0,0 +1,321 @@ +using System; +using System.Diagnostics; +using System.Xml.Linq; +using AnakinRaW.CommonUtilities.Collections; +using PG.Commons.Hashing; +using PG.StarWarsGame.Engine.Audio.Sfx; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.Parsers; + +namespace PG.StarWarsGame.Engine.Xml.Parsers; + +internal class SfxEventParser(GameEngineType engine, IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) + : NamedXmlObjectParser(engine, new SfxEventXmlTagMapper(serviceProvider), errorReporter, serviceProvider) +{ + // This is a slight derivation from the engine: + // The engine does not upper case the name neither for the name itself and the CRC. + // However, the SFXEventManager maps all SFXEvent by their upper-cased name CRC values. + // We create and store upper-cased name CRC to the SFXEvent too. + protected override bool UpperCaseNameForCrc => true; + protected override bool UpperCaseNameForObject => false; + + protected override SfxEvent CreateXmlObject( + string name, + Crc32 nameCrc, + XElement element, + IReadOnlyFrugalValueListDictionary parsedEntries, + XmlLocationInfo location) + { + return new SfxEvent(name, nameCrc, location); + } + + protected override void ValidateAndFixupValues(SfxEvent sfxEvent, XElement element, in IReadOnlyFrugalValueListDictionary parsedEntries) + { + if (sfxEvent.Name.Length > PGConstants.MaxSFXEventName) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.TooLongData, + Message = $"SFXEvent name '{sfxEvent.Name}' is too long." + }); + } + + if (sfxEvent.Is2D == sfxEvent.Is3D) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"SFXEvent '{sfxEvent.Name}' has the same value for is2D and is3D." + }); + } + + if (sfxEvent.MinVolume > sfxEvent.MaxVolume) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"{SfxEventXmlTags.MinVolume} should not be higher than {SfxEventXmlTags.MaxVolume} for SFXEvent '{sfxEvent.Name}'" + }); + } + + if (sfxEvent.MinPitch > sfxEvent.MaxPitch) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"{SfxEventXmlTags.MinPitch} should not be higher than {SfxEventXmlTags.MaxPitch} for SFXEvent '{sfxEvent.Name}'" + }); + } + + if (sfxEvent.MinPan2D > sfxEvent.MaxPan2D) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"{SfxEventXmlTags.MinPan2D} should not be higher than {SfxEventXmlTags.MaxPan2D} for SFXEvent '{sfxEvent.Name}'" + }); + } + + if (sfxEvent.MinPredelay > sfxEvent.MaxPredelay) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"{SfxEventXmlTags.MinPredelay} should not be higher than {SfxEventXmlTags.MaxPredelay} for SFXEvent '{sfxEvent.Name}'" + }); + } + + if (sfxEvent.MinPostdelay > sfxEvent.MaxPostdelay) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"{SfxEventXmlTags.MinPostdelay} should not be higher than {SfxEventXmlTags.MaxPostdelay} for SFXEvent '{sfxEvent.Name}'" + }); + } + + sfxEvent.FixupValues(); + } + + protected override bool ParseTag( + XElement tag, + SfxEvent sfxEvent, + bool replace, + in IReadOnlyFrugalValueListDictionary parsedEntries) + { + if (tag.Name.LocalName == SfxEventXmlTags.UsePreset) + { + // TODO: Needs is Valid Check? + + var presetName = PetroglyphXmlStringParser.Instance.Parse(tag); + + Debug.Assert(!string.IsNullOrEmpty(presetName)); + + var presetNameCrc = HashingService.GetCrc32Upper(presetName.AsSpan(), PGConstants.DefaultPGEncoding); + if (presetNameCrc != default && parsedEntries.TryGetFirstValue(presetNameCrc, out var preset)) + sfxEvent.ApplyPreset(preset); + else + { + ErrorReporter?.Report(new XmlError(this, tag) + { + Message = $"Cannot to find preset '{presetName}' for SFXEvent '{sfxEvent.Name}'", + ErrorKind = XmlParseErrorKind.MissingReference + }); + } + + // Set the preset value regardless whether we found the SFXEvent or not. + sfxEvent.UsePresetName = presetName; + + return true; + } + + return base.ParseTag(tag, sfxEvent, replace, parsedEntries); + } + + + internal sealed class SfxEventXmlTagMapper(IServiceProvider serviceProvider) + : XmlTagMapper(serviceProvider) + { + protected override void BuildMappings() + { + AddMapping( + SfxEventXmlTags.IsPreset, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.IsPreset = val); + AddMapping( + SfxEventXmlTags.UsePreset, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.UsePresetName = val); + AddMapping( + SfxEventXmlTags.Samples, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => SetOrReplaceList(obj.SamplesInternal, val, replace)); + AddMapping( + SfxEventXmlTags.PreSamples, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => SetOrReplaceList(obj.PreSamplesInternal, val, replace)); + AddMapping( + SfxEventXmlTags.PostSamples, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => SetOrReplaceList(obj.PostSamplesInternal, val, replace)); + AddMapping( + SfxEventXmlTags.TextID, + PetroglyphXmlLooseStringListParser.Instance.Parse, + (obj, val, replace) => SetOrReplaceList(obj.LocalizedTextIDsInternal, val, replace)); + AddMapping( + SfxEventXmlTags.PlaySequentially, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.PlaySequentially = val); + AddMapping( + SfxEventXmlTags.Priority, + x => PetroglyphXmlByteParser.Instance.ParseClamped(x, SfxEvent.MinPriorityValue, SfxEvent.MaxPriorityValue), + (obj, val) => obj.Priority = val); + AddMapping( + SfxEventXmlTags.Probability, + x => PetroglyphXmlBytePercentParser.Instance.ParseAtMost(x, SfxEvent.MaxProbabilityValue), + (obj, val) => obj.Probability = val); + AddMapping( + SfxEventXmlTags.PlayCount, + x => PetroglyphXmlSByteParser.Instance.ParseAtLeast(x, SfxEvent.InfinitivePlayCountValue), + (obj, val) => obj.PlayCount = val); + AddMapping( + SfxEventXmlTags.LoopFadeInSeconds, + x => PetroglyphXmlFloatParser.Instance.ParseAtLeast(x, SfxEvent.MinLoopSecondsValue), + (obj, val) => obj.LoopFadeInSeconds = val); + AddMapping( + SfxEventXmlTags.LoopFadeOutSeconds, + x => PetroglyphXmlFloatParser.Instance.ParseAtLeast(x, SfxEvent.MinLoopSecondsValue), + (obj, val) => obj.LoopFadeOutSeconds = val); + AddMapping( + SfxEventXmlTags.MaxInstances, + x => PetroglyphXmlSByteParser.Instance.ParseAtLeast(x, SfxEvent.MinMaxInstancesValue), + (obj, val) => obj.MaxInstances = val); + AddMapping( + SfxEventXmlTags.MinVolume, + x => PetroglyphXmlBytePercentParser.Instance.ParseAtMost(x, SfxEvent.MaxVolumeValue), + (obj, val) => obj.MinVolume = val); + AddMapping( + SfxEventXmlTags.MaxVolume, + x => PetroglyphXmlBytePercentParser.Instance.ParseAtMost(x, SfxEvent.MaxVolumeValue), + (obj, val) => obj.MaxVolume = val); + AddMapping( + SfxEventXmlTags.MinPitch, + x => PetroglyphXmlByteParser.Instance.ParseClamped(x, SfxEvent.MinPitchValue, SfxEvent.MaxPitchValue), + (obj, val) => obj.MinPitch = val); + AddMapping( + SfxEventXmlTags.MaxPitch, + x => PetroglyphXmlByteParser.Instance.ParseClamped(x, SfxEvent.MinPitchValue, SfxEvent.MaxPitchValue), + (obj, val) => obj.MaxPitch = val); + AddMapping( + SfxEventXmlTags.MinPan2D, + x => PetroglyphXmlByteParser.Instance.ParseAtMost(x, SfxEvent.MaxPan2dValue), + (obj, val) => obj.MinPan2D = val); + AddMapping( + SfxEventXmlTags.MaxPan2D, + x => PetroglyphXmlByteParser.Instance.ParseAtMost(x, SfxEvent.MaxPan2dValue), + (obj, val) => obj.MaxPan2D = val); + AddMapping( + SfxEventXmlTags.MinPredelay, + PetroglyphXmlUnsignedIntegerParser.Instance.Parse, + (obj, val) => obj.MinPredelay = val); + AddMapping( + SfxEventXmlTags.MaxPredelay, + PetroglyphXmlUnsignedIntegerParser.Instance.Parse, + (obj, val) => obj.MaxPredelay = val); + AddMapping( + SfxEventXmlTags.MinPostdelay, + PetroglyphXmlUnsignedIntegerParser.Instance.Parse, + (obj, val) => obj.MinPostdelay = val); + AddMapping( + SfxEventXmlTags.MaxPostdelay, + PetroglyphXmlUnsignedIntegerParser.Instance.Parse, + (obj, val) => obj.MaxPostdelay = val); + AddMapping( + SfxEventXmlTags.VolumeSaturationDistance, + // I think it was planned at some time to support -1.0 and >= 0.0, since you don't get a warning when -1.0 is coded + // but the Engine coerces anything < 0.0 to 0.0. + x => PetroglyphXmlFloatParser.Instance.ParseAtLeast(x, SfxEvent.MinVolumeSaturationValue), + (obj, val) => obj.VolumeSaturationDistance = val); + AddMapping( + SfxEventXmlTags.KillsPreviousObjectSFX, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.KillsPreviousObjectsSfx = val); + AddMapping( + SfxEventXmlTags.OverlapTest, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.OverlapTestName = val); + AddMapping( + SfxEventXmlTags.Localize, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.IsLocalized = val); + AddMapping( + SfxEventXmlTags.Is2D, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.Is2D = val); + AddMapping( + SfxEventXmlTags.Is3D, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.Is3D = val); + AddMapping( + SfxEventXmlTags.IsGui, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.IsGui = val); + AddMapping( + SfxEventXmlTags.IsHudVo, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.IsHudVo = val); + AddMapping( + SfxEventXmlTags.IsUnitResponseVo, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.IsUnitResponseVo = val); + AddMapping( + SfxEventXmlTags.IsAmbientVo, + PetroglyphXmlBooleanParser.Instance.Parse, + (obj, val) => obj.IsAmbientVo = val); + AddMapping( + SfxEventXmlTags.ChainedSfxEvent, + PetroglyphXmlStringParser.Instance.Parse, + (obj, val) => obj.ChainedSfxEventName = val); + } + } + + internal static class SfxEventXmlTags + { + internal const string PresetXRef = "XREF_PRESET"; + internal const string IsPreset = "Is_Preset"; + internal const string UsePreset = "Use_Preset"; + internal const string Samples = "Samples"; + internal const string PreSamples = "Pre_Samples"; + internal const string PostSamples = "Post_Samples"; + internal const string TextID = "Text_ID"; + internal const string PlaySequentially = "Play_Sequentially"; + internal const string Priority = "Priority"; + internal const string Probability = "Probability"; + internal const string PlayCount = "Play_Count"; + internal const string LoopFadeInSeconds = "Loop_Fade_In_Seconds"; + internal const string LoopFadeOutSeconds = "Loop_Fade_Out_Seconds"; + internal const string MaxInstances = "Max_Instances"; + internal const string MinVolume = "Min_Volume"; + internal const string MaxVolume = "Max_Volume"; + internal const string MinPitch = "Min_Pitch"; + internal const string MaxPitch = "Max_Pitch"; + internal const string MinPan2D = "Min_Pan2D"; + internal const string MaxPan2D = "Max_Pan2D"; + internal const string MinPredelay = "Min_Predelay"; + internal const string MaxPredelay = "Max_Predelay"; + internal const string MinPostdelay = "Min_Postdelay"; + internal const string MaxPostdelay = "Max_Postdelay"; + internal const string VolumeSaturationDistance = "Volume_Saturation_Distance"; + internal const string KillsPreviousObjectSFX = "Kills_Previous_Object_SFX"; + internal const string OverlapTest = "Overlap_Test"; + internal const string Localize = "Localize"; + internal const string Is2D = "Is_2D"; + internal const string Is3D = "Is_3D"; + internal const string IsGui = "Is_GUI"; + internal const string IsHudVo = "Is_HUD_VO"; + internal const string IsUnitResponseVo = "Is_Unit_Response_VO"; + internal const string IsAmbientVo = "Is_Ambient_VO"; + internal const string ChainedSfxEvent = "Chained_SFXEvent"; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs deleted file mode 100644 index 00f4f61..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerContentParser.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Linq; -using System.Xml; -using AnakinRaW.CommonUtilities.Collections; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using PG.Commons.Hashing; -using PG.Commons.Services; -using PG.StarWarsGame.Engine.IO; -using PG.StarWarsGame.Files.XML; -using PG.StarWarsGame.Files.XML.Data; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Engine.Xml.Parsers; - -internal sealed class XmlContainerContentParser : ServiceBase, IPetroglyphXmlParser -{ - public event EventHandler? XmlParseError; - - private readonly IXmlParserErrorReporter? _reporter; - private readonly IPetroglyphXmlFileParserFactory _fileParserFactory; - - public XmlContainerContentParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? reporter) : base(serviceProvider) - { - _reporter = reporter; - _fileParserFactory = serviceProvider.GetRequiredService(); - Name = GetType().FullName!; - } - - public string Name { get; } - - public void ParseEntriesFromFileListXml( - string xmlFile, - IGameRepository gameRepository, - string lookupPath, - FrugalValueListDictionary entries, - Action? onFileParseAction = null) where T : notnull - { - Logger.LogDebug("Parsing container data '{XmlFile}'", xmlFile); - - using var containerStream = gameRepository.TryOpenFile(xmlFile); - if (containerStream == null) - { - _reporter?.Report(this, XmlParseErrorEventArgs.FromMissingFile(xmlFile)); - Logger.LogWarning("Could not find XML file '{XmlFile}'", xmlFile); - - var args = new XmlContainerParserErrorEventArgs(xmlFile, null, true) - { - // No reason to continue - Continue = false - }; - XmlParseError?.Invoke(this, args); - return; - } - - XmlFileListContainer? container; - - try - { - var containerParser = new XmlFileListParser(Services, _reporter); - container = containerParser.ParseFile(containerStream); - if (container is null) - throw new XmlException($"Unable to parse XML container file '{xmlFile}'."); - } - catch (XmlException e) - { - var args = new XmlContainerParserErrorEventArgs(xmlFile, e, true) - { - // No reason to continue - Continue = false - }; - XmlParseError?.Invoke(this, args); - return; - } - - - var xmlFiles = container.Files.Select(x => FileSystem.Path.Combine(lookupPath, x)).ToList(); - - var parser = _fileParserFactory.CreateFileParser(_reporter); - - foreach (var file in xmlFiles) - { - if (onFileParseAction is not null) - onFileParseAction(file); - - using var fileStream = gameRepository.TryOpenFile(file); - - if (fileStream is null) - { - _reporter?.Report(parser, XmlParseErrorEventArgs.FromMissingFile(file)); - Logger.LogWarning("Could not find XML file '{File}'", file); - - var args = new XmlContainerParserErrorEventArgs(file); - XmlParseError?.Invoke(this, args); - - if (args.Continue) - continue; - return; - } - - Logger.LogDebug("Parsing File '{File}'", file); - - try - { - parser.ParseFile(fileStream, entries); - } - catch (XmlException e) - { - _reporter?.Report(parser, new XmlParseErrorEventArgs(new XmlLocationInfo(file, 0), XmlParseErrorKind.Unknown, e.Message)); - - var args = new XmlContainerParserErrorEventArgs(file, e); - XmlParseError?.Invoke(this, args); - - if (!args.Continue) - return; - } - } - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerParserErrorEventArgs.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerParserErrorEventArgs.cs deleted file mode 100644 index 87ccce1..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlContainerParserErrorEventArgs.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Xml; - -namespace PG.StarWarsGame.Engine.Xml.Parsers; - -internal class XmlContainerParserErrorEventArgs(string file, XmlException? exception = null, bool isXmlFileList = false) -{ - public bool Continue - { - get; - // Once this is set to false, there is no way back. - set => field &= value; - } = true; - - public bool ErrorInXmlFileList { get; } = isXmlFileList; - - public string File { get; } = file; - - [MemberNotNullWhen(true, nameof(Exception))] - public bool HasException => Exception is not null; - - public bool IsFileNotFound => !HasException; - - public XmlException? Exception { get; } = exception; -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs deleted file mode 100644 index 1565e9e..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Xml.Linq; -using AnakinRaW.CommonUtilities.Collections; -using Microsoft.Extensions.DependencyInjection; -using PG.Commons.Hashing; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Engine.Xml.Parsers; - -public abstract class XmlObjectParser( - IReadOnlyFrugalValueListDictionary parsedElements, - IServiceProvider serviceProvider, - IXmlParserErrorReporter? errorReporter = null) - : XmlObjectParser(parsedElements, serviceProvider, errorReporter) where TObject : XmlObject -{ - protected void Parse(TObject xmlObject, XElement element) - { - Parse(xmlObject, element, EmptyParseState.Instance); - } - - protected sealed override bool ParseTag(XElement tag, TObject xmlObject, in EmptyParseState parseState) - { - return ParseTag(tag, xmlObject); - } - - protected abstract bool ParseTag(XElement tag, TObject xmlObject); -} - -public readonly struct EmptyParseState -{ - public static readonly EmptyParseState Instance = new(); -} - - -public abstract class XmlObjectParser( - IReadOnlyFrugalValueListDictionary parsedElements, - IServiceProvider serviceProvider, - IXmlParserErrorReporter? errorReporter = null) - : PetroglyphXmlElementParser(errorReporter) where TObject : XmlObject -{ - protected IReadOnlyFrugalValueListDictionary ParsedElements { get; } = - parsedElements ?? throw new ArgumentNullException(nameof(parsedElements)); - - protected ICrc32HashingService HashingService { get; } = serviceProvider.GetRequiredService(); - - public abstract TObject Parse(XElement element, out Crc32 crc32); - - protected void Parse(TObject xmlObject, XElement element, in TParseState state) - { - foreach (var tag in element.Elements()) - { - if (!ParseTag(tag, xmlObject, state)) - { - OnParseError(new XmlParseErrorEventArgs(tag, XmlParseErrorKind.UnknownNode, - $"The node '{tag.Name}' is not supported.")); - break; - } - } - } - - protected abstract bool ParseTag(XElement tag, TObject xmlObject, in TParseState parseState); - - protected string GetXmlObjectName(XElement element, out Crc32 crc32, bool uppercaseName) - { - GetNameAttributeValue(element, out var name); - crc32 = uppercaseName - ? HashingService.GetCrc32Upper(name.AsSpan(), PGConstants.DefaultPGEncoding) - : HashingService.GetCrc32(name.AsSpan(), PGConstants.DefaultPGEncoding); - - if (crc32 == default) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"Name for XmlObject cannot be empty.")); - } - - return name; - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphStarWarsGameXmlParseSettings.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphStarWarsGameXmlParseSettings.cs new file mode 100644 index 0000000..3952f25 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphStarWarsGameXmlParseSettings.cs @@ -0,0 +1,10 @@ +namespace PG.StarWarsGame.Engine.Xml; + +public sealed record PetroglyphStarWarsGameXmlParseSettings +{ + public required string GameManager { get; init; } + + public bool InvalidFilesListXmlFailsInitialization { get; init; } = true; + + public bool InvalidObjectXmlFailsInitialization { get; init; } = false; +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphStarWarsGameXmlParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphStarWarsGameXmlParser.cs new file mode 100644 index 0000000..f849d05 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphStarWarsGameXmlParser.cs @@ -0,0 +1,230 @@ +using System; +using System.IO; +using System.Linq; +using System.Xml; +using AnakinRaW.CommonUtilities.Collections; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PG.Commons.Hashing; +using PG.Commons.Services; +using PG.StarWarsGame.Engine.ErrorReporting; +using PG.StarWarsGame.Engine.IO; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Data; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.Parsers; + +namespace PG.StarWarsGame.Engine.Xml; + +public sealed class PetroglyphStarWarsGameXmlParser : ServiceBase, IPetroglyphXmlParserInfo +{ + private readonly IGameRepository _gameRepository; + private readonly PetroglyphStarWarsGameXmlParseSettings _settings; + private readonly IGameEngineErrorReporter _reporter; + private readonly IPetroglyphXmlFileParserFactory _fileParserFactory; + + public string Name { get; } + + public PetroglyphStarWarsGameXmlParser( + IGameRepository gameRepository, + PetroglyphStarWarsGameXmlParseSettings settings, + IServiceProvider serviceProvider, + IGameEngineErrorReporter reporter) + : base(serviceProvider) + { + _gameRepository = gameRepository; + _settings = settings; + _reporter = reporter; + _fileParserFactory = serviceProvider.GetRequiredService(); + Name = GetType().FullName!; + } + + public T? ParseFile(string xmlFile, XmlFileParser parser) where T : XmlObject + { + return ParseCore(xmlFile, parser.ParseFile, () => null); + } + + public XmlFileList ParseFileList(string xmlFile) + { + return ParseCore(xmlFile, + stream => new XmlFileListParser(Services, _reporter).ParseFile(stream), + () => XmlFileList.Empty(new XmlLocationInfo(xmlFile, null))); + } + + public void ParseEntriesFromFileListXml( + string xmlFile, + string lookupPath, + FrugalValueListDictionary entries, + Action? onParseContainerAction = null) where T : NamedXmlObject + { + var container = ParseFileList(xmlFile); + + var xmlFiles = container.Files.Select(x => FileSystem.Path.Combine(lookupPath, x)).ToList(); + + var parser = new XmlContainerFileParser(Services, + _fileParserFactory.CreateNamedXmlObjectParser(_gameRepository.EngineType, _reporter), _reporter); + + foreach (var file in xmlFiles) + { + onParseContainerAction?.Invoke(file); + if (!ParseObjectsFromContainerFile(file, parser, entries)) + return; + } + } + + public bool ParseObjectsFromContainerFile( + string xmlFile, + IXmlContainerFileParser parser, + IFrugalValueListDictionary entries) where T : NamedXmlObject + { + return ParseCore(xmlFile, stream => + { + parser.ParseFile(stream, entries); + return true; + }, () => _settings.InvalidObjectXmlFailsInitialization); + } + + private T ParseCore(string xmlFile, Func parseAction, Func invalidFileAction) + { + Logger.LogDebug("Parsing file '{XmlFile}'", xmlFile); + + using var fileStream = _gameRepository.TryOpenFile(xmlFile); + + if (fileStream is null) + { + var message = $"Could not find XML file '{xmlFile}'"; + Logger.LogWarning(message); + + _reporter.Report(new XmlError(this, locationInfo: new XmlLocationInfo(xmlFile, null)) + { + Message = message, + ErrorKind = XmlParseErrorKind.MissingFile + }); + + if (_settings.InvalidObjectXmlFailsInitialization) + { + _reporter.Report(new InitializationError + { + GameManager = _settings.GameManager, + Message = message, + }); + } + + return invalidFileAction(); + } + + try + { + return parseAction(fileStream); + } + catch (XmlException e) + { + _reporter.Report(new XmlError(this, locationInfo: new XmlLocationInfo(xmlFile, e.LineNumber)) + { + ErrorKind = XmlParseErrorKind.Unknown, + Message = e.Message, + }); + if (_settings.InvalidObjectXmlFailsInitialization) + { + _reporter.Report(new InitializationError + { + GameManager = _settings.GameManager, + Message = e.Message, + }); + } + + return invalidFileAction(); + } + } +} + +enum XmlDataType +{ + DB_DATA_TYPE_BOOL = 0x0, + DB_DATA_TYPE_DWORD = 0x1, + DB_DATA_TYPE_UNSIGNED_CHAR = 0x2, + DB_DATA_TYPE_SIGNED_CHAR = 0x3, + DB_DATA_TYPE_UNSIGNED_CHAR_PERCENT = 0x4, + DB_DATA_TYPE_UNSIGNED_INT = 0x5, + DB_DATA_TYPE_SIGNED_INT = 0x6, + DB_DATA_TYPE_UNSIGNED_INT_PERCENT = 0x7, + DB_DATA_TYPE_FLOAT = 0x8, + DB_DATA_TYPE_UNIT_FLOAT = 0x9, + DB_DATA_TYPE_DOUBLE = 0xa, + DB_DATA_TYPE_UNIT_DOUBLE = 0xb, + DB_DATA_TYPE_SIGNED_INT_HEX = 0xc, + DB_DATA_TYPE_DWORD_HEX = 0xd, + DB_DATA_TYPE_CONVERSION = 0xe, + DB_DATA_TYPE_VECTOR2 = 0xf, + DB_DATA_TYPE_VECTOR3 = 0x10, + DB_DATA_TYPE_VECTOR4 = 0x11, + DB_DATA_TYPE_DYN_VECTOR_INT = 0x12, + DB_DATA_TYPE_DYN_VECTOR_FLOAT = 0x13, + DB_DATA_TYPE_DYN_VECTOR_VECTOR3 = 0x14, + DB_DATA_TYPE_DYN_VECTOR_VECTOR2 = 0x15, + DB_DATA_TYPE_RGBA = 0x16, + DB_DATA_TYPE_STL_STRING = 0x17, + DB_DATA_TYPE_STL_LIST_STL_STRINGS = 0x18, + DB_DATA_TYPE_STL_VECTOR_DWORDS = 0x19, + DB_DATA_TYPE_STL_VECTOR_DWORDS_HEX = 0x1a, + DB_DATA_TYPE_STL_VECTOR_STL_STRINGS = 0x1b, + DB_DATA_TYPE_STL_STRING_UPPER = 0x1c, + DB_DATA_TYPE_OBJECT_REFERENCE = 0x1d, + DB_DATA_TYPE_MULTI_OBJECT_REFERENCE = 0x1e, + DB_DATA_TYPE_SFX_EVENT = 0x1f, + DB_DATA_TYPE_SPEECH_EVENT = 0x20, + DB_DATA_TYPE_MUSIC_EVENT = 0x21, + DB_DATA_TYPE_SFXEVENT_OVERRIDE_LIST_ENTRY = 0x22, + DB_DATA_TYPE_WEATHER_SFXEVENT_LOOP_LIST_ENTRY = 0x23, + DB_DATA_TYPE_WEATHER_SFXEVENT_INTERMITTENT_LIST_ENTRY = 0x24, + DB_DATA_TYPE_WEATHER_SFX_EVENT_PAIR_ARRAY_ENTRY = 0x25, + DB_DATA_TYPE_AMBIENT_SFXEVENT_INTERMITTENT_LIST_ENTRY = 0x26, + DB_DATA_TYPE_DYN_VECTOR_SFX_EVENT_ENTRY = 0x27, + DB_DATA_TYPE_TRIPLE_OBJ_TYPE_AND_SPEECH_EVENT_ENTRY = 0x28, + DB_DATA_TYPE_DYN_VECTOR_FACTION_AND_MUSIC_EVENT_PAIR_ENTRY = 0x29, + DB_DATA_TYPE_DYN_VECTOR_STL_STRINGS = 0x2a, + DB_DATA_TYPE_FACTION_DATA_OVERRIDE_UINT = 0x2b, + DB_DATA_TYPE_FACTION_DATA_OVERRIDE_FLOAT = 0x2c, + DB_DATA_TYPE_FACTION_DATA_OVERRIDE_NAMEREF = 0x2d, + DB_DATA_TYPE_QUADRATIC = 0x2e, + DB_DATA_TYPE_SPLINE = 0x2f, + DB_DATA_TYPE_LINEAR = 0x30, + DB_DATA_TYPE_PARABOLIC = 0x31, + DB_DATA_TYPE_SHIPCLASS = 0x32, + DB_DATA_TYPE_SCRIPT_VARIABLE = 0x33, + DB_DATA_TYPE_CONVERSION_STRING_PAIR_VECTOR = 0x34, + DB_DATA_TYPE_CONVERSION_OBJECT_REF_VECTOR = 0x35, + DB_DATA_TYPE_STL_VECTOR_OBJECT_REFERENCE_STRING_PAIR = 0x36, + DB_DATA_TYPE_STARTING_FORCE_DEFINITION = 0x37, + DB_DATA_TYPE_STL_VECTOR_ABILITIES = 0x38, + DB_DATA_TYPE_STL_VECTOR_ABILITIES_DATA = 0x39, + DB_DATA_TYPE_STL_VECTOR_ACTIONS = 0x3a, + DB_DATA_TYPE_STL_HASHMAP_DAMAGE_TO_DEATH_CLONE = 0x3b, + DB_DATA_TYPE_STL_VECTOR_STRING_INT_PAIR = 0x3c, + DB_DATA_TYPE_LIST_GAME_OBJECT_CATEGORY_FLOAT_PAIR = 0x3d, + DB_DATA_TYPE_DAMAGE_TO_ARMOR_MOD_ENTRY = 0x3e, + DB_DATA_QUOTED_STRING_DYN_VECTOR_ENTRY = 0x3f, + DB_DATA_TYPE_LANGUAGE_STL_STRING_ARRAY_ENTRY = 0x40, + DB_DATA_TYPE_HARD_POINT_TYPE_ARRAY_OF_DYN_VECTOR_STL_STRINGS = 0x41, + DB_DATA_TYPE_HARD_POINT_TYPE_ARRAY_OF_SFXEVENTS = 0x42, + DB_DATA_TYPE_FACTION = 0x43, + DB_DATA_TYPE_WEIGHTED_TYPE_LIST = 0x44, + DB_DATA_TYPE_STL_VECTOR_CONVERSION = 0x45, + DB_DATA_TYPE_STL_VECTOR_OBJECT_REFERENCE_INT_PAIR = 0x46, + DB_DATA_TYPE_DISCRETE_DISTRIBUTION_NAME_REFERENCE = 0x47, + DB_DATA_TYPE_STL_HASHMAP_CONVERSION_TO_FLOAT = 0x48, + DB_DATA_TYPE_UNIT_ABILITY = 0x49, + DB_TYPE_PROJECTILE_CATEGORY = 0x4a, + DB_DATA_TYPE_RENDER_MODE = 0x4b, + DB_DATA_TYPE_NON_HERO_ABILITIES_SFX_EVENT_PAIR = 0x4c, + DB_DATA_TYPE_UNIT_MODE_MULTIPLIER_MOD = 0x4d, + DB_DATA_TYPE_UNIT_MODE_FLAG_MOD = 0x4e, + DB_DATA_TYPE_STRING_INT_PAIR = 0x4f, + DB_DATA_TYPE_BIT_BOOL = 0x50, + DB_DATA_TYPE_STL_VECTOR_PROJECTILE_CATEGORIES = 0x51, + DB_DATA_TYPE_COMBAT_MOD = 0x52, + DB_DATA_TYPE_MULTI_OBJECT_REFERENCE_WITH_OR = 0x53, + DB_DATA_TYPE_CONVERSION_PTR = 0x54, + DB_DATA_TYPE_CONVERSION_OBJECT_REF_VECTOR_PTR = 0x55, + DB_DATA_TYPE_CONVERSION_64 = 0x56 +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs deleted file mode 100644 index 550f459..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using PG.StarWarsGame.Engine.Audio.Sfx; -using PG.StarWarsGame.Engine.CommandBar.Xml; -using PG.StarWarsGame.Engine.GameObjects; -using PG.StarWarsGame.Engine.Xml.Parsers.File; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Engine.Xml; - -internal sealed class PetroglyphXmlFileParserFactory(IServiceProvider serviceProvider) : IPetroglyphXmlFileParserFactory -{ - public IPetroglyphXmlFileContainerParser CreateFileParser(IXmlParserErrorReporter? errorReporter) where T : notnull - { - if (typeof(T) == typeof(SfxEvent)) - return (IPetroglyphXmlFileContainerParser) new SfxEventFileParser(serviceProvider, errorReporter); - - if (typeof(T) == typeof(CommandBarComponentData)) - return (IPetroglyphXmlFileContainerParser)new CommandBarComponentFileParser(serviceProvider, errorReporter); - - if (typeof(T) == typeof(GameObject)) - return (IPetroglyphXmlFileContainerParser)new GameObjectFileParser(serviceProvider, errorReporter); - - - throw new NotImplementedException($"Unable to get parser for type {typeof(T)}"); - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/CommandBarComponentTags.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/CommandBarComponentTags.cs deleted file mode 100644 index bc5498b..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/CommandBarComponentTags.cs +++ /dev/null @@ -1,108 +0,0 @@ -namespace PG.StarWarsGame.Engine.Xml.Tags; - -public static class CommandBarComponentTags -{ - public const string SelectedTextureName = "Selected_Texture_Name"; - public const string BlankTextureName = "Blank_Texture_Name"; - public const string IconTextureName = "Icon_Texture_Name"; - public const string IconAlternateTextureName = "Icon_Alternate_Texture_Name"; - public const string MouseOverTextureName = "Mouse_Over_Texture_Name"; - public const string DisabledTextureName = "Disabled_Texture_Name"; - public const string FlashTextureName = "Flash_Texture_Name"; - public const string BarTextureName = "Bar_Texture_Name"; - public const string BarOverlayName = "Bar_Overlay_Name"; - public const string BuildTextureName = "Build_Texture_Name"; - public const string ModelName = "Model_Name"; - public const string BoneName = "Bone_Name"; - public const string CursorTextureName = "Cursor_Texture_Name"; - public const string FontName = "Font_Name"; - public const string AlternateFontName = "Alternate_Font_Name"; - public const string TooltipText = "Tooltip_Text"; - public const string ClickSfx = "Click_SFX"; - public const string MouseOverSfx = "Mouse_Over_SFX"; - public const string LowerEffectTextureName = "Lower_Effect_Texture_Name"; - public const string UpperEffectTextureName = "Upper_Effect_Texture_Name"; - public const string OverlayTextureName = "Overlay_Texture_Name"; - public const string Overlay2TextureName = "Overlay2_Texture_Name"; - public const string RightClickSfx = "Right_Click_SFX"; - public const string Type = "Type"; - public const string Group = "Group"; - public const string DragAndDrop = "Drag_And_Drop"; - public const string DragSelect = "Drag_Select"; - public const string Receptor = "Receptor"; - public const string Toggle = "Toggle"; - public const string Tab = "Tab"; - public const string AssociatedText = "Associated_Text"; - public const string Hidden = "Hidden"; - public const string Scale = "Scale"; - public const string Color = "Color"; - public const string TextColor = "Text_Color"; - public const string TextColor2 = "Text_Color2"; - public const string Size = "Size"; - public const string ClearColor = "Clear_Color"; - public const string Disabled = "Disabled"; - public const string SwapTexture = "Swap_Texture"; - public const string BaseLayer = "Base_Layer"; - public const string DrawAdditive = "Draw_Additive"; - public const string TextOffset = "Text_Offset"; - public const string TextOffset2 = "Text_Offset2"; - public const string Offset = "Offset"; - public const string DefaultOffset = "Default_Offset"; - public const string DefaultOffsetWidescreen = "Default_Offset_Widescreen"; - public const string IconOffset = "Icon_Offset"; - public const string MouseOverOffset = "Mouse_Over_Offset"; - public const string DisabledOffset = "Disabled_Offset"; - public const string BuildDialOffset = "Build_Dial_Offset"; - public const string BuildDial2Offset = "Build_Dial2_Offset"; - public const string LowerEffectOffset = "Lower_Effect_Offset"; - public const string UpperEffectOffset = "Upper_Effect_Offset"; - public const string OverlayOffset = "Overlay_Offset"; - public const string Overlay2Offset = "Overlay2_Offset"; - public const string Editable = "Editable"; - public const string MaxTextLength = "Max_Text_Length"; - public const string BlinkRate = "Blink_Rate"; - public const string FontPointSize = "Font_Point_Size"; - public const string TextOutline = "Text_Outline"; - public const string MaxTextWidth = "Max_Text_Width"; - public const string Stackable = "Stackable"; - public const string ModelOffsetX = "Model_Offset_X"; - public const string ModelOffsetY = "Model_Offset_Y"; - public const string ScaleModelX = "Scale_Model_X"; - public const string ScaleModelY = "Scale_Model_Y"; - public const string Collideable = "Collideable"; - public const string TextEmboss = "Text_Emboss"; - public const string ShouldGhost = "Should_Ghost"; - public const string GhostBaseOnly = "Ghost_Base_Only"; - public const string MaxBarLevel = "Max_Bar_Level"; - public const string MaxBarColor = "Max_Bar_Color"; - public const string CrossFade = "Cross_Fade"; - public const string LeftJustified = "Left_Justified"; - public const string RightJustified = "Right_Justified"; - public const string NoShell = "No_Shell"; - public const string SnapDrag = "Snap_Drag"; - public const string SnapLocation = "Snap_Location"; - public const string BlinkDuration = "Blink_Duration"; - public const string ScaleDuration = "Scale_Duration"; - public const string OffsetRender = "Offset_Render"; - public const string BlinkFade = "Blink_Fade"; - public const string NoHiddenCollision = "No_Hidden_Collision"; - public const string ManualOffset = "Manual_Offset"; - public const string SelectedAlpha = "Selected_Alpha"; - public const string PixelAlign = "Pixel_Align"; - public const string CanDragStack = "Can_Drag_Stack"; - public const string CanAnimate = "Can_Animate"; - public const string AnimFps = "Anim_FPS"; - public const string LoopAnim = "Loop_Anim"; - public const string SmoothBar = "Smooth_Bar"; - public const string OutlinedBar = "Outlined_Bar"; - public const string DragBack = "Drag_Back"; - public const string LowerEffectAdditive = "Lower_Effect_Additive"; - public const string UpperEffectAdditive = "Upper_Effect_Additive"; - public const string ClickShift = "Click_Shift"; - public const string TutorialScene = "Tutorial_Scene"; - public const string DialogScene = "Dialog_Scene"; - public const string ShouldRenderAtDragPos = "Should_Render_At_Drag_Pos"; - public const string DisableDarken = "Disable_Darken"; - public const string AnimateBack = "Animate_Back"; - public const string AnimateUpperEffect = "Animate_Upper_Effect"; -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/ComponentTextureKeyExtensions.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/ComponentTextureKeyExtensions.cs deleted file mode 100644 index c072c90..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/ComponentTextureKeyExtensions.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using PG.StarWarsGame.Engine.GuiDialog; - -namespace PG.StarWarsGame.Engine.Xml.Tags; - -internal static class ComponentTextureKeyExtensions -{ - public static bool TryConvertToKey(ReadOnlySpan keyValue, out GuiComponentType key) - { - key = keyValue switch - { - "Button_Left" => GuiComponentType.ButtonLeft, - "Button_Middle" => GuiComponentType.ButtonMiddle, - "Button_Right" => GuiComponentType.ButtonRight, - "Button_Left_Mouse_Over" => GuiComponentType.ButtonLeftMouseOver, - "Button_Middle_Mouse_Over" => GuiComponentType.ButtonMiddleMouseOver, - "Button_Right_Mouse_Over" => GuiComponentType.ButtonRightMouseOver, - "Button_Left_Pressed" => GuiComponentType.ButtonLeftPressed, - "Button_Middle_Pressed" => GuiComponentType.ButtonMiddlePressed, - "Button_Right_Pressed" => GuiComponentType.ButtonRightPressed, - "Button_Left_Disabled" => GuiComponentType.ButtonLeftDisabled, - "Button_Middle_Disabled" => GuiComponentType.ButtonMiddleDisabled, - "Button_Right_Disabled" => GuiComponentType.ButtonRightDisabled, - - "Check_Off" => GuiComponentType.CheckOff, - "Check_On" => GuiComponentType.CheckOn, - - "Dial_Left" => GuiComponentType.DialLeft, - "Dial_Middle" => GuiComponentType.DialMiddle, - "Dial_Right" => GuiComponentType.DialRight, - "Dial_Plus" => GuiComponentType.DialPlus, - "Dial_Plus_Mouse_Over" => GuiComponentType.DialPlusMouseOver, - "Dial_Plus_Pressed" => GuiComponentType.DialPlusPressed, - "Dial_Minus" => GuiComponentType.DialMinus, - "Dial_Minus_Mouse_Over" => GuiComponentType.DialMinusMouseOver, - "Dial_Minus_Pressed" => GuiComponentType.DialMinusPressed, - "Dial_Tab" => GuiComponentType.DialTab, - - "Frame_Bottom" => GuiComponentType.FrameBottom, - "Frame_Bottom_Left" => GuiComponentType.FrameBottomLeft, - "Frame_Bottom_Right" => GuiComponentType.FrameBottomRight, - "Frame_Background" => GuiComponentType.FrameBackground, - "Frame_Left" => GuiComponentType.FrameLeft, - "Frame_Right" => GuiComponentType.FrameRight, - "Frame_Top" => GuiComponentType.FrameTop, - "Frame_Top_Left" => GuiComponentType.FrameTopLeft, - "Frame_Top_Right" => GuiComponentType.FrameTopRight, - "Frame_Top_Transition_Left" => GuiComponentType.FrameTopTransitionLeft, - "Frame_Top_Transition_Right" => GuiComponentType.FrameTopTransitionRight, - "Frame_Bottom_Transition_Left" => GuiComponentType.FrameBottomTransitionLeft, - "Frame_Bottom_Transition_Right" => GuiComponentType.FrameBottomTransitionRight, - "Frame_Left_Transition_Top" => GuiComponentType.FrameLeftTransitionTop, - "Frame_Left_Transition_Bottom" => GuiComponentType.FrameLeftTransitionBottom, - "Frame_Right_Transition_Top" => GuiComponentType.FrameRightTransitionTop, - "Frame_Right_Transition_Bottom" => GuiComponentType.FrameRightTransitionBottom, - - "Radio_Off" => GuiComponentType.RadioOff, - "Radio_On" => GuiComponentType.RadioOn, - "Radio_Disabled" => GuiComponentType.RadioDisabled, - "Radio_Mouse_Over" => GuiComponentType.RadioMouseOver, - - "Scroll_Down_Button" => GuiComponentType.ScrollDownButton, - "Scroll_Down_Button_Pressed" => GuiComponentType.ScrollDownButtonPressed, - "Scroll_Down_Button_Mouse_Over" => GuiComponentType.ScrollDownButtonMouseOver, - "Scroll_Down_Button_Disabled" => GuiComponentType.ScrollDownButtonDisabled, - "Scroll_Middle" => GuiComponentType.ScrollMiddle, - "Scroll_Middle_Disabled" => GuiComponentType.ScrollMiddleDisabled, - "Scroll_Tab" => GuiComponentType.ScrollTab, - "Scroll_Tab_Disabled" => GuiComponentType.ScrollTabDisabled, - "Scroll_Up_Button" => GuiComponentType.ScrollUpButton, - "Scroll_Up_Button_Pressed" => GuiComponentType.ScrollUpButtonPressed, - "Scroll_Up_Button_Mouse_Over" => GuiComponentType.ScrollUpButtonMouseOver, - "Scroll_Up_Button_Disabled" => GuiComponentType.ScrollUpButtonDisabled, - - "Trackbar_Scroll_Down_Button" => GuiComponentType.TrackbarScrollDownButton, - "Trackbar_Scroll_Down_Button_Pressed" => GuiComponentType.TrackbarScrollDownButtonPressed, - "Trackbar_Scroll_Down_Button_Mouse_Over" => GuiComponentType.TrackbarScrollDownButtonMouseOver, - "Trackbar_Scroll_Down_Button_Disabled" => GuiComponentType.TrackbarScrollDownButtonDisabled, - "Trackbar_Scroll_Middle" => GuiComponentType.TrackbarScrollMiddle, - "Trackbar_Scroll_Middle_Disabled" => GuiComponentType.TrackbarScrollMiddleDisabled, - "Trackbar_Scroll_Tab" => GuiComponentType.TrackbarScrollTab, - "Trackbar_Scroll_Tab_Disabled" => GuiComponentType.TrackbarScrollTabDisabled, - "Trackbar_Scroll_Up_Button" => GuiComponentType.TrackbarScrollUpButton, - "Trackbar_Scroll_Up_Button_Pressed" => GuiComponentType.TrackbarScrollUpButtonPressed, - "Trackbar_Scroll_Up_Button_Mouse_Over" => GuiComponentType.TrackbarScrollUpButtonMouseOver, - "Trackbar_Scroll_Up_Button_Disabled" => GuiComponentType.TrackbarScrollUpButtonDisabled, - - "Small_Frame_Bottom" => GuiComponentType.SmallFrameBottom, - "Small_Frame_Bottom_Left" => GuiComponentType.SmallFrameBottomLeft, - "Small_Frame_Bottom_Right" => GuiComponentType.SmallFrameBottomRight, - "Small_Frame_Left" => GuiComponentType.SmallFrameMiddleLeft, - "Small_Frame_Right" => GuiComponentType.SmallFrameMiddleRight, - "Small_Frame_Top" => GuiComponentType.SmallFrameTop, - "Small_Frame_Top_Left" => GuiComponentType.SmallFrameTopLeft, - "Small_Frame_Top_Right" => GuiComponentType.SmallFrameTopRight, - "Small_Frame_Background" => GuiComponentType.SmallFrameBackground, - - "Combo_Box_Popdown_Button" => GuiComponentType.ComboboxPopdown, - "Combo_Box_Popdown_Button_Pressed" => GuiComponentType.ComboboxPopdownPressed, - "Combo_Box_Popdown_Button_Mouse_Over" => GuiComponentType.ComboboxPopdownMouseOver, - "Combo_Box_Text_Box" => GuiComponentType.ComboboxTextBox, - "Combo_Box_Left_Cap" => GuiComponentType.ComboboxLeftCap, - - "Progress_Bar_Left" => GuiComponentType.ProgressLeft, - "Progress_Bar_Middle_Off" => GuiComponentType.ProgressMiddleOff, - "Progress_Bar_Middle_On" => GuiComponentType.ProgressMiddleOn, - "Progress_Bar_Right" => GuiComponentType.ProgressRight, - - "Scanlines" => GuiComponentType.Scanlines, - _ => (GuiComponentType)int.MaxValue - }; - return (int)key != int.MaxValue; - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/SfxEventXmlTags.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/SfxEventXmlTags.cs deleted file mode 100644 index 959da6b..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/SfxEventXmlTags.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace PG.StarWarsGame.Engine.Xml.Tags; - -public static class SfxEventXmlTags -{ - internal const string PresetXRef = "XREF_PRESET"; - - public const string IsPreset = "Is_Preset"; - public const string UsePreset = "Use_Preset"; - public const string Samples = "Samples"; - public const string PreSamples = "Pre_Samples"; - public const string PostSamples = "Post_Samples"; - public const string TextID = "Text_ID"; - public const string PlaySequentially = "Play_Sequentially"; - public const string Priority = "Priority"; - public const string Probability = "Probability"; - public const string PlayCount = "Play_Count"; - public const string LoopFadeInSeconds = "Loop_Fade_In_Seconds"; - public const string LoopFadeOutSeconds = "Loop_Fade_Out_Seconds"; - public const string MaxInstances = "Max_Instances"; - public const string MinVolume = "Min_Volume"; - public const string MaxVolume = "Max_Volume"; - public const string MinPitch = "Min_Pitch"; - public const string MaxPitch = "Max_Pitch"; - public const string MinPan2D = "Min_Pan2D"; - public const string MaxPan2D = "Max_Pan2D"; - public const string MinPredelay = "Min_Predelay"; - public const string MaxPredelay = "Max_Predelay"; - public const string MinPostdelay = "Min_Postdelay"; - public const string MaxPostdelay = "Max_Postdelay"; - public const string VolumeSaturationDistance = "Volume_Saturation_Distance"; - public const string KillsPreviousObjectSFX = "Kills_Previous_Object_SFX"; - public const string OverlapTest = "Overlap_Test"; - public const string Localize = "Localize"; - public const string Is2D = "Is_2D"; - public const string Is3D = "Is_3D"; - public const string IsGui = "Is_GUI"; - public const string IsHudVo = "Is_HUD_VO"; - public const string IsUnitResponseVo = "Is_Unit_Response_VO"; - public const string IsAmbientVo = "Is_Ambient_VO"; - public const string ChainedSfxEvent = "Chained_SFXEvent"; -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObject.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObject.cs deleted file mode 100644 index 71840fe..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObject.cs +++ /dev/null @@ -1,12 +0,0 @@ -using PG.StarWarsGame.Files.XML; - -namespace PG.StarWarsGame.Engine.Xml; - -public abstract class XmlObject(XmlLocationInfo location) -{ - public XmlLocationInfo Location { get; } = location; - - internal virtual void CoerceValues() - { - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObjectParser.cs new file mode 100644 index 0000000..923e6b1 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObjectParser.cs @@ -0,0 +1,48 @@ +using System.Xml.Linq; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Data; +using PG.StarWarsGame.Files.XML.ErrorHandling; + +namespace PG.StarWarsGame.Engine.Xml; + +public abstract class XmlObjectParser( + GameEngineType engine, + XmlTagMapper tagMapper, + IXmlParserErrorReporter? errorReporter = null) + : XmlObjectParserBase.EmptyParseState>(engine, tagMapper, errorReporter) + where TObject : XmlObject +{ + public TObject Parse(XElement element) + { + var xmlObject = CreateXmlObject(XmlLocationInfo.FromElement(element)); + ParseObject(xmlObject, element, false, EmptyParseState.Instance); + ValidateAndFixupValues(xmlObject, element, EmptyParseState.Instance); + return xmlObject; + } + + protected abstract TObject CreateXmlObject(XmlLocationInfo location); + + protected sealed override bool ParseTag(XElement tag, TObject xmlObject, bool replace, in EmptyParseState parseState) + { + return ParseTag(tag, xmlObject, replace); + } + + protected sealed override void ValidateAndFixupValues(TObject xmlObject, XElement element, in EmptyParseState parseState) + { + ValidateAndFixupValues(xmlObject, element); + } + + protected virtual bool ParseTag(XElement tag, TObject xmlObject, bool replace) + { + return XmlTagMapper.TryParseEntry(tag, xmlObject, replace, Engine); + } + + protected virtual void ValidateAndFixupValues(TObject xmlObject, XElement element) + { + } + + public readonly struct EmptyParseState + { + public static readonly EmptyParseState Instance = new(); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObjectParserBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObjectParserBase.cs new file mode 100644 index 0000000..9a23ddd --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObjectParserBase.cs @@ -0,0 +1,59 @@ +using System; +using System.Xml.Linq; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Data; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.Parsers; + +namespace PG.StarWarsGame.Engine.Xml; + +public abstract class XmlObjectParserBase( + GameEngineType engine, + XmlTagMapper tagMapper, + IXmlParserErrorReporter? errorReporter) + : PetroglyphXmlParserBase(errorReporter) where TObject : XmlObject +{ + protected readonly XmlTagMapper XmlTagMapper = tagMapper ?? throw new ArgumentNullException(nameof(tagMapper)); + protected readonly GameEngineType Engine = engine; + + protected virtual bool IgnoreEmptyValue => true; + + protected virtual void ValidateAndFixupValues(TObject namedXmlObject, XElement element, in TParseState state) + { + } + + protected virtual void ParseObject(TObject xmlObject, XElement element, bool replace, in TParseState state) + { + ParseTags(xmlObject, element, replace, in state); + } + + protected virtual bool ParseTag(XElement tag, TObject xmlObject, bool replace, in TParseState parseState) + { + return IsTagValid(tag) && XmlTagMapper.TryParseEntry(tag, xmlObject, replace, Engine); + } + + protected void ParseTags(TObject xmlObject, XElement element, bool replace, in TParseState state) + { + foreach (var tag in element.Elements()) + { + if (!tag.HasElements) + { + if (string.IsNullOrEmpty(tag.PGValue) && IgnoreEmptyValue) + continue; + + if (!ParseTag(tag, xmlObject, replace, state)) + { + ErrorReporter?.Report(new XmlError(this, tag) + { + Message = $"The node '{tag.Name}' is not supported.", + ErrorKind = XmlParseErrorKind.UnknownNode + }); + } + } + else + { + ParseObject(xmlObject, tag, replace, in state); + } + } + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObjectParserFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObjectParserFactory.cs new file mode 100644 index 0000000..eeee6e2 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlObjectParserFactory.cs @@ -0,0 +1,30 @@ +using PG.StarWarsGame.Engine.Audio.Sfx; +using PG.StarWarsGame.Engine.CommandBar.Xml; +using PG.StarWarsGame.Engine.GameObjects; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using System; +using PG.StarWarsGame.Files.XML.Data; +using PG.StarWarsGame.Engine.Xml.Parsers; + +namespace PG.StarWarsGame.Engine.Xml; + +internal sealed class XmlObjectParserFactory(IServiceProvider serviceProvider) : IPetroglyphXmlFileParserFactory +{ + public NamedXmlObjectParser CreateNamedXmlObjectParser(GameEngineType engine, IXmlParserErrorReporter? errorReporter) where T : NamedXmlObject + { + if (typeof(T) == typeof(SfxEvent)) + return ChangeType(new SfxEventParser(engine, serviceProvider, errorReporter)); + if (typeof(T) == typeof(CommandBarComponentData)) + return ChangeType(new CommandBarComponentParser(engine, serviceProvider, errorReporter)); + if (typeof(T) == typeof(GameObject)) + return ChangeType(new GameObjectParser(engine, serviceProvider, errorReporter)); + + + throw new ParserNotFoundException(typeof(T)); + } + + private static NamedXmlObjectParser ChangeType(object obj) where T : NamedXmlObject + { + return (NamedXmlObjectParser) obj; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlTagMapper.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlTagMapper.cs new file mode 100644 index 0000000..32fc2fb --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/XmlTagMapper.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Xml.Linq; +using AnakinRaW.CommonUtilities; +using Microsoft.Extensions.DependencyInjection; +using PG.Commons.Hashing; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Data; + +namespace PG.StarWarsGame.Engine.Xml; + +public abstract class XmlTagMapper where TObject : XmlObject +{ + private delegate void ParserValueAction(TObject target, XElement element, bool replace); + + private readonly struct MappingEntry(SupportedEngines supportedEngines, ParserValueAction action) + { + public readonly SupportedEngines SupportedEngines = supportedEngines; + public readonly ParserValueAction Action = action; + } + + [Flags] + public enum SupportedEngines + { + Eaw = 1, + Foc = 2, + All = Eaw | Foc + } + + private readonly Dictionary _tagMappings = new(); + private readonly ICrc32HashingService _crcService; + + protected XmlTagMapper(IServiceProvider serviceProvider) + { + if (serviceProvider == null) + throw new ArgumentNullException(nameof(serviceProvider)); + _crcService = serviceProvider.GetRequiredService(); + + // ReSharper disable once VirtualMemberCallInConstructor + BuildMappings(); + } + + protected abstract void BuildMappings(); + + protected static void SetOrReplaceList(IList destinationList, IEnumerable values, bool replace) + { + if (replace) + destinationList.Clear(); + foreach (var value in values) + destinationList.Add(value); + } + + protected void AddMapping( + string tagName, + Func parser, + Action setter, + SupportedEngines supportedEngines = SupportedEngines.All) + { + AddMapping(tagName, parser, (target, value, _) => setter(target, value), supportedEngines); + } + + protected void AddMapping( + string tagName, + Func parser, + Action setter, + SupportedEngines supportedEngines = SupportedEngines.All) + { + ThrowHelper.ThrowIfNullOrEmpty(tagName); + if (tagName.Length >= XmlFileConstants.MaxTagNameLength) + throw new ArgumentOutOfRangeException( + $"Tag name '{tagName}' exceeds maximum length of {XmlFileConstants.MaxTagNameLength} characters", nameof(tagName)); + + if (parser == null) + throw new ArgumentNullException(nameof(parser)); + if (setter == null) + throw new ArgumentNullException(nameof(setter)); + + var crc = GetCrc32(tagName); + + _tagMappings[crc] = new MappingEntry(supportedEngines, (target, element, replace) => + { + var value = parser(element); + setter(target, value, replace); + }); + } + + public bool TryParseEntry(XElement element, TObject target, bool replace, GameEngineType engine) + { + var tagName = element.Name.LocalName; + if (tagName.Length >= XmlFileConstants.MaxTagNameLength) + return false; + + var crc = GetCrc32(tagName); + + if (!_tagMappings.TryGetValue(crc, out var mapping)) + return false; + + if (!IsEngineSupported(mapping.SupportedEngines, engine)) + return false; + + mapping.Action(target, element, replace); + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsEngineSupported(SupportedEngines supportedEngines, GameEngineType requestedEngine) + { + // Convert enum value to its corresponding flag by shifting bit 1 left + // Eaw (0) -> 1 << 0 = 1, Foc (1) -> 1 << 1 = 2 + var engineFlag = (SupportedEngines)(1 << (int)requestedEngine); + // Use bitwise AND to check if the flag is set in supportedEngines + // Returns true if the bit is present, false otherwise + return (supportedEngines & engineFlag) != 0; + } + + private Crc32 GetCrc32(string tagName) + { + return _crcService.GetCrc32Upper(tagName, XmlFileConstants.XmlEncoding); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/PG.StarWarsGame.Files.ALO.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/PG.StarWarsGame.Files.ALO.csproj index 0d071ca..a92efb1 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/PG.StarWarsGame.Files.ALO.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/PG.StarWarsGame.Files.ALO.csproj @@ -1,6 +1,6 @@  - netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1;net10.0 PG.StarWarsGame.Files.ALO PG.StarWarsGame.Files.ALO AlamoEngineTools.PG.StarWarsGame.Files.ALO @@ -24,7 +24,4 @@ - - - \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj index 5df0123..000b3a6 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj @@ -1,6 +1,6 @@  - netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1;net10.0 PG.StarWarsGame.Files.ChunkFiles PG.StarWarsGame.Files.ChunkFiles AlamoEngineTools.PG.StarWarsGame.Files.ChunkFiles @@ -17,9 +17,6 @@ preview - - - - + \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/NamedXmlObject.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/NamedXmlObject.cs similarity index 61% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/NamedXmlObject.cs rename to src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/NamedXmlObject.cs index 7cf9c4e..2222633 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/NamedXmlObject.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/NamedXmlObject.cs @@ -1,11 +1,9 @@ using System; -using PG.Commons.Data; using PG.Commons.Hashing; -using PG.StarWarsGame.Files.XML; -namespace PG.StarWarsGame.Engine.Xml; +namespace PG.StarWarsGame.Files.XML.Data; -public abstract class NamedXmlObject(string name, Crc32 nameCrc, XmlLocationInfo location) : XmlObject(location), IHasCrc32 +public abstract class NamedXmlObject(string name, Crc32 nameCrc, XmlLocationInfo location) : XmlObject(location) { public Crc32 Crc32 { get; } = nameCrc; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/XmlFileList.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/XmlFileList.cs new file mode 100644 index 0000000..aef7f43 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/XmlFileList.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace PG.StarWarsGame.Files.XML.Data; + +public class XmlFileList(IReadOnlyList files, XmlLocationInfo location) : XmlObject(location) +{ + public static XmlFileList Empty(XmlLocationInfo location) + { + return new XmlFileList([], location); + } + + public IReadOnlyList Files { get; } = files; +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/XmlFileListContainer.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/XmlFileListContainer.cs deleted file mode 100644 index 31a16cc..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/XmlFileListContainer.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Collections.Generic; - -namespace PG.StarWarsGame.Files.XML.Data; - -public class XmlFileListContainer(IList files) -{ - public IList Files { get; } = files; -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/XmlObject.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/XmlObject.cs new file mode 100644 index 0000000..41cde2c --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Data/XmlObject.cs @@ -0,0 +1,6 @@ +namespace PG.StarWarsGame.Files.XML.Data; + +public abstract class XmlObject(XmlLocationInfo location) +{ + public XmlLocationInfo Location { get; } = location; +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IXmlParserErrorProvider.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IXmlParserErrorProvider.cs deleted file mode 100644 index 2470187..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IXmlParserErrorProvider.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace PG.StarWarsGame.Files.XML.ErrorHandling; - -public interface IXmlParserErrorProvider -{ - event XmlErrorEventHandler XmlParseError; -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IXmlParserErrorReporter.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IXmlParserErrorReporter.cs index 6bb14c1..bddb691 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IXmlParserErrorReporter.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IXmlParserErrorReporter.cs @@ -1,8 +1,6 @@ -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Files.XML.ErrorHandling; +namespace PG.StarWarsGame.Files.XML.ErrorHandling; public interface IXmlParserErrorReporter { - void Report(IPetroglyphXmlParser parser, XmlParseErrorEventArgs error); + void Report(XmlError error); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/PrimitiveXmlErrorReporter.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/PrimitiveXmlErrorReporter.cs index 39de0e6..4d4bce2 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/PrimitiveXmlErrorReporter.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/PrimitiveXmlErrorReporter.cs @@ -1,13 +1,14 @@ using System; -using PG.StarWarsGame.Files.XML.Parsers; namespace PG.StarWarsGame.Files.XML.ErrorHandling; -internal sealed class PrimitiveXmlErrorReporter : IXmlParserErrorReporter, IXmlParserErrorProvider +internal sealed class PrimitiveXmlErrorReporter : IXmlParserErrorReporter { - public event XmlErrorEventHandler? XmlParseError; + internal delegate void XmlErrorEventHandler(XmlError error); - private static readonly Lazy LazyInstance = new(() => new PrimitiveXmlErrorReporter()); + public event XmlErrorEventHandler? PrimitiveParseError; + + private static readonly Lazy LazyInstance = new(() => new PrimitiveXmlErrorReporter(), true); public static PrimitiveXmlErrorReporter Instance => LazyInstance.Value; @@ -15,8 +16,8 @@ private PrimitiveXmlErrorReporter() { } - public void Report(IPetroglyphXmlParser parser, XmlParseErrorEventArgs error) + public void Report(XmlError error) { - XmlParseError?.Invoke(parser, error); + PrimitiveParseError?.Invoke(error); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlError.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlError.cs new file mode 100644 index 0000000..bcdac81 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlError.cs @@ -0,0 +1,21 @@ +using System; +using System.Xml.Linq; +using PG.StarWarsGame.Files.XML.Parsers; + +namespace PG.StarWarsGame.Files.XML.ErrorHandling; + +public sealed class XmlError( + IPetroglyphXmlParserInfo parser, + XElement? element = null, + XmlLocationInfo? locationInfo = null) +{ + public XmlLocationInfo FileLocation { get; } = locationInfo ?? (element is not null ? XmlLocationInfo.FromElement(element) : default); + + public IPetroglyphXmlParserInfo Parser { get; } = parser ?? throw new ArgumentNullException(nameof(parser)); + + public XElement? Element { get; } = element; + + public required XmlParseErrorKind ErrorKind { get; init; } + + public required string Message { get; init; } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlErrorEventHandler.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlErrorEventHandler.cs deleted file mode 100644 index 69962e3..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlErrorEventHandler.cs +++ /dev/null @@ -1,5 +0,0 @@ -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Files.XML.ErrorHandling; - -public delegate void XmlErrorEventHandler(IPetroglyphXmlParser parser, XmlParseErrorEventArgs error); \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlErrorReporter.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlErrorReporter.cs index 8234729..dd8734b 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlErrorReporter.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlErrorReporter.cs @@ -1,29 +1,25 @@ using AnakinRaW.CommonUtilities; -using PG.StarWarsGame.Files.XML.Parsers; namespace PG.StarWarsGame.Files.XML.ErrorHandling; -public class XmlErrorReporter : DisposableObject, IXmlParserErrorReporter, IXmlParserErrorProvider +public class XmlErrorReporter : DisposableObject, IXmlParserErrorReporter { - public event XmlErrorEventHandler? XmlParseError; - public XmlErrorReporter() { - PrimitiveXmlErrorReporter.Instance.XmlParseError += OnPrimitiveError; + PrimitiveXmlErrorReporter.Instance.PrimitiveParseError += OnPrimitiveError; } - public virtual void Report(IPetroglyphXmlParser parser, XmlParseErrorEventArgs error) + public virtual void Report(XmlError error) { - XmlParseError?.Invoke(parser, error); } protected override void DisposeResources() { - PrimitiveXmlErrorReporter.Instance.XmlParseError -= OnPrimitiveError; + PrimitiveXmlErrorReporter.Instance.PrimitiveParseError -= OnPrimitiveError; } - private void OnPrimitiveError(IPetroglyphXmlParser parser, XmlParseErrorEventArgs error) + private void OnPrimitiveError(XmlError error) { - Report(parser, error); + Report(error); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlParseErrorEventArgs.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlParseErrorEventArgs.cs deleted file mode 100644 index afaa2ce..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlParseErrorEventArgs.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Xml.Linq; -using AnakinRaW.CommonUtilities; - -namespace PG.StarWarsGame.Files.XML.ErrorHandling; - -public class XmlParseErrorEventArgs : EventArgs -{ - public XmlLocationInfo Location { get; } - - public XElement? Element { get; } - - public XmlParseErrorKind ErrorKind { get; } - - public string Message { get; } - - public XmlParseErrorEventArgs(XElement element, XmlParseErrorKind errorKind, string message) - { - Element = element ?? throw new ArgumentNullException(nameof(element)); - Location = XmlLocationInfo.FromElement(element); - ErrorKind = errorKind; - Message = message; - } - - public XmlParseErrorEventArgs(XmlLocationInfo location, XmlParseErrorKind errorKind, string message) - { - Location = location; - Message = message; - ErrorKind = errorKind; - } - - public static XmlParseErrorEventArgs FromMissingFile(string file) - { - ThrowHelper.ThrowIfNullOrEmpty(file); - return new XmlParseErrorEventArgs(new XmlLocationInfo(file, null), XmlParseErrorKind.MissingFile, "XML file not found."); - } - - public static XmlParseErrorEventArgs FromEmptyRoot(XElement element) - { - return new XmlParseErrorEventArgs(element, XmlParseErrorKind.EmptyRoot, "XML file has an empty root node."); - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlParseErrorKind.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlParseErrorKind.cs index aa3f8d2..6308a74 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlParseErrorKind.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlParseErrorKind.cs @@ -50,5 +50,13 @@ public enum XmlParseErrorKind /// /// The XML tag name is null or empty. /// - EmptyNodeName + EmptyNodeName = 11, + /// + /// The XML tag has child elements. + /// + TagHasElements = 12, + /// + /// The name of the XML Element (not the value of the attribute "Name") has an unexpected name. + /// + UnexceptedElementName = 13, } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/PG.StarWarsGame.Files.XML.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/PG.StarWarsGame.Files.XML.csproj index be85ee8..4a8bced 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/PG.StarWarsGame.Files.XML.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/PG.StarWarsGame.Files.XML.csproj @@ -1,6 +1,6 @@  - netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1;net10.0 PG.StarWarsGame.Files.XML PG.StarWarsGame.Files.XML AlamoEngineTools.PG.StarWarsGame.Files.XML @@ -18,7 +18,7 @@ preview - + @@ -26,7 +26,4 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - - \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlElementParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlElementParser.cs deleted file mode 100644 index 14a8aa1..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlElementParser.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Xml.Linq; - -namespace PG.StarWarsGame.Files.XML.Parsers; - -public interface IPetroglyphXmlElementParser : IPetroglyphXmlParser where T : notnull -{ - T Parse(XElement element); -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlFileParser.cs deleted file mode 100644 index 5f9c4b4..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlFileParser.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.IO; - -namespace PG.StarWarsGame.Files.XML.Parsers; - -public interface IPetroglyphXmlFileParser : IPetroglyphXmlParser where T : notnull -{ - T? ParseFile(Stream xmlStream); -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlParserInfo.cs similarity index 64% rename from src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlParser.cs rename to src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlParserInfo.cs index fdf5fd5..6ba0ebf 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlParserInfo.cs @@ -1,6 +1,6 @@ namespace PG.StarWarsGame.Files.XML.Parsers; -public interface IPetroglyphXmlParser +public interface IPetroglyphXmlParserInfo { string Name { get; } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/PetroglyphXmlFileParserBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/PetroglyphXmlFileParserBase.cs index 6ffba48..2ac7f6c 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/PetroglyphXmlFileParserBase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/PetroglyphXmlFileParserBase.cs @@ -1,7 +1,6 @@ using System; using System.IO; using System.IO.Abstractions; -using System.Text; using System.Xml; using System.Xml.Linq; using Microsoft.Extensions.DependencyInjection; @@ -23,20 +22,22 @@ public abstract class PetroglyphXmlFileParserBase(IServiceProvider serviceProvid protected virtual bool LoadLineInfo => true; - protected XElement? GetRootElement(Stream xmlStream, out string fileName) + protected XElement GetRootElement(Stream xmlStream, out string fileName) { fileName = GetStrippedFileName(xmlStream.GetFilePath()); if (string.IsNullOrEmpty(fileName)) throw new InvalidOperationException("Unable to parse XML from unnamed stream. Either parse from a file or MEG stream."); - SkipLeadingWhiteSpace(fileName, xmlStream); + SkipCharactersUntilXmlHeader(fileName, xmlStream); - var xmlReader = XmlReader.Create(xmlStream, new XmlReaderSettings + var asciiStreamReader = new StreamReader(xmlStream, XmlFileConstants.XmlEncoding, false, 1024, leaveOpen: true); + using var xmlReader = XmlReader.Create(asciiStreamReader, new XmlReaderSettings { IgnoreWhitespace = true, IgnoreComments = true, - IgnoreProcessingInstructions = true + IgnoreProcessingInstructions = true, + CloseInput = true }, fileName); var options = LoadOptions.SetBaseUri; @@ -44,7 +45,22 @@ public abstract class PetroglyphXmlFileParserBase(IServiceProvider serviceProvid options |= LoadOptions.SetLineInfo; var doc = XDocument.Load(xmlReader, options); - return doc.Root; + + var root = doc.Root; + + if (root is null) + throw new XmlException("No root node found."); + + if (!root.HasElements) + { + ErrorReporter?.Report(new XmlError(this, root) + { + ErrorKind = XmlParseErrorKind.EmptyRoot, + Message = "XML file has an empty root node.", + }); + } + + return root; } private string GetStrippedFileName(string filePath) @@ -61,23 +77,25 @@ private string GetStrippedFileName(string filePath) } - private void SkipLeadingWhiteSpace(string fileName, Stream stream) + private void SkipCharactersUntilXmlHeader(string fileName, Stream stream) { - using var r = new StreamReader(stream, Encoding.ASCII, false, 10, true); + using var r = new StreamReader(stream, XmlFileConstants.XmlEncoding, false, 10, true); var count = 0; - while (true) - { - var c = (char)r.Read(); - if (!char.IsWhiteSpace(c)) - break; + // It might be possible, that a XML file starts with leading spaces or even a encoding BOM. + // The engine skips everything until the first '<' character, so we have to do the same to avoid parsing errors. + + while ((char)r.Read() != '<') count++; - } if (count != 0) { - OnParseError(new XmlParseErrorEventArgs(new XmlLocationInfo(fileName, 0), - XmlParseErrorKind.DataBeforeHeader, $"XML header is not the first entry of the XML file."));} + ErrorReporter?.Report(new XmlError(this, locationInfo: new XmlLocationInfo(fileName, 0)) + { + ErrorKind = XmlParseErrorKind.DataBeforeHeader, + Message = "XML header is not the first entry of the XML file.", + }); + } stream.Position = count; } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/PetroglyphXmlParserBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/PetroglyphXmlParserBase.cs index 7395eb6..ef2380e 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/PetroglyphXmlParserBase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/PetroglyphXmlParserBase.cs @@ -1,10 +1,12 @@ -using System.Linq; +using System; +using System.Linq; +using System.Runtime.Serialization; using PG.StarWarsGame.Files.XML.ErrorHandling; using System.Xml.Linq; namespace PG.StarWarsGame.Files.XML.Parsers; -public abstract class PetroglyphXmlParserBase : IPetroglyphXmlParser +public abstract class PetroglyphXmlParserBase : IPetroglyphXmlParserInfo { protected readonly IXmlParserErrorReporter? ErrorReporter; @@ -15,6 +17,41 @@ public override string ToString() return Name; } + protected bool IsTagValid(XElement element) + { + if (element.HasElements) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.TagHasElements, + Message = "A tag cannot have elements.", + }); + return false; + } + var tagName = element.Name.LocalName; + if (string.IsNullOrEmpty(tagName)) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.EmptyNodeName, + Message = "A tag name cannot be null or empty.", + }); + return false; + } + + if (tagName.Length > XmlFileConstants.MaxTagNameLength) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.TooLongData, + Message = $"A tag name can be only {XmlFileConstants.MaxTagNameLength} chars long.", + }); + return false; + } + + return true; + } + protected PetroglyphXmlParserBase(IXmlParserErrorReporter? errorReporter) { Name = GetType().FullName!; @@ -33,29 +70,38 @@ protected string GetNameAttributeValue(XElement element) return nameAttribute is null ? string.Empty : nameAttribute.Value; } - protected bool GetNameAttributeValue(XElement element, out string value) + protected bool GetNameAttributeValue(XElement element, out string value, bool uppercase) { return GetAttributeValue(element, "Name", out value!, string.Empty); } - protected bool GetAttributeValue(XElement element, string attribute, out string? value, string? defaultValue = null) + protected bool GetAttributeValue( + XElement element, + string attribute, + out string? value, + string defaultValue = "", + bool uppercase = false) { + // In this engine, this is actually case-sensitive var nameAttribute = element.Attributes() .FirstOrDefault(a => a.Name.LocalName == attribute); - + if (nameAttribute is null) { value = defaultValue; - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.MissingAttribute, $"Missing attribute '{attribute}'")); + if (uppercase) + value = value.ToUpperInvariant(); + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.MissingAttribute, + Message = $"Missing attribute '{attribute}'", + }); return false; } value = nameAttribute.Value; + if (uppercase) + value = value.ToUpperInvariant(); return true; } - - protected virtual void OnParseError(XmlParseErrorEventArgs error) - { - ErrorReporter?.Report(this, error); - } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/INamedXmlObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/INamedXmlObjectParser.cs new file mode 100644 index 0000000..ec7f855 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/INamedXmlObjectParser.cs @@ -0,0 +1,11 @@ +using System.Xml.Linq; +using AnakinRaW.CommonUtilities.Collections; +using PG.Commons.Hashing; +using PG.StarWarsGame.Files.XML.Data; + +namespace PG.StarWarsGame.Files.XML.Parsers; + +public interface INamedXmlObjectParser : IPetroglyphXmlParserInfo where T : NamedXmlObject +{ + T Parse(XElement element, IReadOnlyFrugalValueListDictionary parsedEntries, out Crc32 nameCrc); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlFileContainerParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IXmlContainerFileParser.cs similarity index 55% rename from src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlFileContainerParser.cs rename to src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IXmlContainerFileParser.cs index cd21e00..f9e8349 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Base/IPetroglyphXmlFileContainerParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IXmlContainerFileParser.cs @@ -1,10 +1,13 @@ using System.IO; using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; +using PG.StarWarsGame.Files.XML.Data; namespace PG.StarWarsGame.Files.XML.Parsers; -public interface IPetroglyphXmlFileContainerParser : IPetroglyphXmlParser where T : notnull +public interface IXmlContainerFileParser : IPetroglyphXmlParserInfo where T : NamedXmlObject { + INamedXmlObjectParser ElementParser { get; } + void ParseFile(Stream xmlStream, IFrugalValueListDictionary parsedEntries); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlElementParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlElementParser.cs deleted file mode 100644 index 5fa2c6d..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlElementParser.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Xml.Linq; -using PG.StarWarsGame.Files.XML.ErrorHandling; - -namespace PG.StarWarsGame.Files.XML.Parsers; - -public abstract class PetroglyphXmlElementParser(IXmlParserErrorReporter? errorReporter = null) - : PetroglyphXmlParserBase(errorReporter), IPetroglyphXmlElementParser where T : notnull -{ - public abstract T Parse(XElement element); -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileContainerParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileContainerParser.cs deleted file mode 100644 index d2b862a..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileContainerParser.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.IO; -using System.Xml.Linq; -using AnakinRaW.CommonUtilities.Collections; -using PG.Commons.Hashing; -using PG.StarWarsGame.Files.XML.ErrorHandling; - -namespace PG.StarWarsGame.Files.XML.Parsers; - -public abstract class PetroglyphXmlFileContainerParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? listener = null) - : PetroglyphXmlFileParserBase(serviceProvider, listener), IPetroglyphXmlFileContainerParser where T : notnull -{ - public void ParseFile(Stream xmlStream, IFrugalValueListDictionary parsedEntries) - { - var root = GetRootElement(xmlStream, out var fileName); - if (root is not null) - Parse(root, parsedEntries, fileName); - } - - protected abstract void Parse(XElement element, IFrugalValueListDictionary parsedElements, string fileName); -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs deleted file mode 100644 index 986cfef..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.IO; -using System.Xml.Linq; -using PG.StarWarsGame.Files.XML.ErrorHandling; - -namespace PG.StarWarsGame.Files.XML.Parsers; - -public abstract class PetroglyphXmlFileParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) - : PetroglyphXmlFileParserBase(serviceProvider, errorReporter), IPetroglyphXmlFileParser where T : notnull -{ - public T? ParseFile(Stream xmlStream) - { - var root = GetRootElement(xmlStream, out var fileName); - if (root is null) - { - var location = new XmlLocationInfo(fileName, 0); - OnParseError(new XmlParseErrorEventArgs(location, XmlParseErrorKind.EmptyRoot, - "Unable to get root node from XML file.")); - } - return root is null ? default : Parse(root, fileName); - } - - protected abstract T Parse(XElement element, string fileName); -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/CommaSeparatedStringKeyValueListParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/CommaSeparatedStringKeyValueListParser.cs index 1d2a5b3..8316fd5 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/CommaSeparatedStringKeyValueListParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/CommaSeparatedStringKeyValueListParser.cs @@ -1,12 +1,14 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Xml.Linq; +using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Files.XML.Parsers; // Used e.g, by // Format: Key, Value, Key, Value // There might be arbitrary spaces, tabs and newlines -// TODO: This class is not yet implemented, compliant to the engine + public sealed class CommaSeparatedStringKeyValueListParser : PetroglyphPrimitiveXmlParser> { public static readonly CommaSeparatedStringKeyValueListParser Instance = new(); @@ -17,13 +19,30 @@ private CommaSeparatedStringKeyValueListParser() private protected override IList<(string key, string value)> DefaultValue => []; - protected internal override IList<(string key, string value)> ParseCore(string trimmedValue, XElement element) + internal override int EngineDataTypeId => 0x34; + + protected internal override IList<(string key, string value)> ParseCore(ReadOnlySpan trimmedValue, XElement element) { - var values = element.Value.Split(','); + var valueText = element.PGValue; + + if (string.IsNullOrEmpty(valueText)) + return DefaultValue; + + if (valueText.Length >= 0x10000) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.TooLongData, + Message = "Input string exceeds maximum size." + }); + return DefaultValue; + } + + var values = valueText.Split(','); // Cases: Empty tag or invalid value (e.g, terrain only, wrong separator, etc.) - if (values.Length <= 1) - return new List<(string key, string value)>(0); + if (values.Length < 2) + return DefaultValue; var keyValueList = new List<(string key, string value)>(values.Length + 1 / 2); @@ -31,7 +50,14 @@ private CommaSeparatedStringKeyValueListParser() { // Case: Incomplete key-value pair if (values.Length - 1 < i + 1) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.MalformedValue, + Message = "Unexpected end of string. Missing string for conversion/string pair!" + }); break; + } var key = values[i].Trim(); var value = values[i + 1].Trim(); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphNumberParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphNumberParser.cs new file mode 100644 index 0000000..e93b0a6 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphNumberParser.cs @@ -0,0 +1,88 @@ +using System; +using System.Xml.Linq; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.Utilities; +#if NET7_0_OR_GREATER +using System.Numerics; +#endif + +namespace PG.StarWarsGame.Files.XML.Parsers; + +public abstract class PetroglyphNumberParser : PetroglyphPrimitiveXmlParser where T : struct, IEquatable, IComparable +#if NET10_0_OR_GREATER + , INumber, IMinMaxValue +#endif +{ + +#if NET7_0_OR_GREATER + protected virtual T MaxValue => T.MaxValue; + + protected virtual T MinValue => T.MinValue; +#else + protected abstract T MaxValue { get; } + + protected abstract T MinValue { get; } + +#endif + + public T ParseAtLeast(XElement element, T minValue) + { + if (minValue.CompareTo(MinValue) < 0 || minValue.CompareTo(MaxValue) > 0) + throw new ArgumentOutOfRangeException(nameof(minValue), "minValue is out of range."); + + var value = Parse(element); + var corrected = PGMath.Max(value, minValue); + if (!corrected.Equals(value)) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"Expected value to be at least {minValue} but got value '{value}'.", + }); + } + + return corrected; + } + + public T ParseAtMost(XElement element, T maxValue) + { + if (maxValue.CompareTo(MinValue) < 0 || maxValue.CompareTo(MaxValue) > 0) + throw new ArgumentOutOfRangeException(nameof(maxValue), "maxValue is out of range."); + + var value = Parse(element); + var corrected = PGMath.Min(value, maxValue); + if (!corrected.Equals(value)) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"Expected value to be at least {maxValue} but got value '{value}'.", + }); + } + + return corrected; + } + + + public T ParseClamped(XElement element, T minValue, T maxValue) + { + if (minValue.CompareTo(MinValue) < 0 || minValue.CompareTo(MaxValue) > 0) + throw new ArgumentOutOfRangeException(nameof(minValue), "minValue is out of range."); + if (maxValue.CompareTo(MinValue) < 0 || maxValue.CompareTo(MaxValue) > 0) + throw new ArgumentOutOfRangeException(nameof(maxValue), "maxValue is out of range."); + if (minValue.CompareTo(maxValue) > 0) + throw new ArgumentException("minValue must be less than or equal to maxValue."); + + var value = Parse(element); + var clamped = PGMath.Clamp(value, minValue, maxValue); + if (!value.Equals(clamped)) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"Expected integer between {minValue} and {maxValue} but got value '{value}'.", + }); + } + return clamped; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphPrimitiveXmlParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphPrimitiveXmlParser.cs index 0fd6fc1..48c53c3 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphPrimitiveXmlParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphPrimitiveXmlParser.cs @@ -1,30 +1,26 @@ -using System.Xml.Linq; -using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using System; +using System.Xml.Linq; namespace PG.StarWarsGame.Files.XML.Parsers; -public abstract class PetroglyphPrimitiveXmlParser : PetroglyphXmlElementParser where T : notnull +public abstract class PetroglyphPrimitiveXmlParser : PetroglyphXmlParserBase where T : notnull { private protected abstract T DefaultValue { get; } + internal abstract int EngineDataTypeId { get; } + private protected PetroglyphPrimitiveXmlParser() : base(PrimitiveXmlErrorReporter.Instance) { } - public sealed override T Parse(XElement element) + public T Parse(XElement element) { - var tagName = element.Name.LocalName; - if (string.IsNullOrEmpty(tagName)) - { - ErrorReporter?.Report(this, new XmlParseErrorEventArgs(element, XmlParseErrorKind.EmptyNodeName, "A tag name cannot be null or empty.")); + if (!IsTagValid(element)) return DefaultValue; - } - if (tagName.Length >= 256) - ErrorReporter?.Report(this, new XmlParseErrorEventArgs(element, XmlParseErrorKind.TooLongData, "A tag name cannot be null or empty.")); - - var value = element.Value.Trim(); + var value = element.PGValue.AsSpan().Trim(); return value.Length == 0 ? DefaultValue : ParseCore(value, element); } - protected internal abstract T ParseCore(string trimmedValue, XElement element); + protected internal abstract T ParseCore(ReadOnlySpan trimmedValue, XElement element); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlBooleanParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlBooleanParser.cs index 8672d1b..c609d0f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlBooleanParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlBooleanParser.cs @@ -1,4 +1,5 @@ -using System.Xml.Linq; +using System; +using System.Xml.Linq; namespace PG.StarWarsGame.Files.XML.Parsers; @@ -6,13 +7,15 @@ public sealed class PetroglyphXmlBooleanParser : PetroglyphPrimitiveXmlParser false; + + internal override int EngineDataTypeId => 0x0 & 0x50; + private PetroglyphXmlBooleanParser() { } - private protected override bool DefaultValue => false; - - protected internal override bool ParseCore(string trimmedValue, XElement element) + protected internal override bool ParseCore(ReadOnlySpan trimmedValue, XElement element) { // Yes! The engine only checks if the values is exact 1 or starts with Tt or Yy // At least it's efficient, I guess... diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlByteParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlByteParser.cs index aa7ba4b..56bcb5c 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlByteParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlByteParser.cs @@ -1,27 +1,39 @@ -using System.Xml.Linq; -using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using System; +using System.Xml.Linq; namespace PG.StarWarsGame.Files.XML.Parsers; -public sealed class PetroglyphXmlByteParser : PetroglyphPrimitiveXmlParser +public sealed class PetroglyphXmlByteParser : PetroglyphNumberParser { public static readonly PetroglyphXmlByteParser Instance = new(); + private protected override byte DefaultValue => 0; + + internal override int EngineDataTypeId => 0x2; + +#if !NET7_0_OR_GREATER + protected override byte MaxValue => byte.MaxValue; + + protected override byte MinValue => byte.MinValue; +#endif + private PetroglyphXmlByteParser() { } - private protected override byte DefaultValue => 0; - - protected internal override byte ParseCore(string trimmedValue, XElement element) + protected internal override byte ParseCore(ReadOnlySpan trimmedValue, XElement element) { var intValue = PetroglyphXmlIntegerParser.Instance.ParseCore(trimmedValue, element); var asByte = (byte)intValue; if (intValue != asByte) { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"Expected a byte value (0 - 255) but got value '{intValue}'.")); + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"Expected a byte value (0 - 255) but got value '{intValue}'.", + }); } return asByte; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlBytePercentParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlBytePercentParser.cs new file mode 100644 index 0000000..e419da4 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlBytePercentParser.cs @@ -0,0 +1,45 @@ +using PG.StarWarsGame.Files.XML.ErrorHandling; +using System; +using System.Xml.Linq; + +namespace PG.StarWarsGame.Files.XML.Parsers; + +public sealed class PetroglyphXmlBytePercentParser : PetroglyphNumberParser +{ + public static readonly PetroglyphXmlBytePercentParser Instance = new(); + + private protected override byte DefaultValue => 0; + + internal override int EngineDataTypeId => 0x4; + + protected override byte MaxValue => 100; + +#if !NET7_0_OR_GREATER + protected override byte MinValue => byte.MinValue; +#endif + + private PetroglyphXmlBytePercentParser() + { + } + + protected internal override byte ParseCore(ReadOnlySpan trimmedValue, XElement element) + { + var intValue = PetroglyphXmlIntegerParser.Instance.ParseCore(trimmedValue, element); + + if (intValue > MaxValue) + intValue = MaxValue; + + var asByte = (byte)intValue; + // Add additional check (> 100), cause the PG implementation is broken, but we need to stay "bug-compatible". + if (intValue != asByte) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"Expected a byte value (0 - 100) but got value '{asByte}'.", + }); + } + + return asByte; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlFloatParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlFloatParser.cs index d3b4e85..c8d0335 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlFloatParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlFloatParser.cs @@ -1,40 +1,42 @@ -using System; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using System; using System.Globalization; using System.Xml.Linq; -using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Files.XML.Parsers; -public sealed class PetroglyphXmlFloatParser : PetroglyphPrimitiveXmlParser +public sealed class PetroglyphXmlFloatParser : PetroglyphNumberParser { public static readonly PetroglyphXmlFloatParser Instance = new(); private protected override float DefaultValue => 0.0f; - private PetroglyphXmlFloatParser() - { - } + internal override int EngineDataTypeId => 0x8; - public float ParseAtLeast(XElement element, float minValue) - { - var value = Parse(element); - var corrected = Math.Max(value, minValue); - if (corrected != value) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"Expected float to be at least {minValue} but got value '{value}'.")); - } +#if !NET7_0_OR_GREATER + protected override float MaxValue => float.MaxValue; - return corrected; - } + protected override float MinValue => float.MinValue; +#endif - protected internal override float ParseCore(string trimmedValue, XElement element) + private PetroglyphXmlFloatParser() + { + } + + protected internal override float ParseCore(ReadOnlySpan trimmedValue, XElement element) { // The engine always loads FP numbers a long double and then converts that result to float - if (!double.TryParse(trimmedValue, NumberStyles.Any, CultureInfo.InvariantCulture, out var doubleValue)) + if (!double.TryParse(trimmedValue +#if NETSTANDARD2_0 + .ToString() +#endif + , NumberStyles.Any, CultureInfo.InvariantCulture, out var doubleValue)) { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.MalformedValue, - $"Expected double but got value '{trimmedValue}'.")); + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.MalformedValue, + Message = $"Expected double but got value '{trimmedValue.ToString()}'.", + }); return 0.0f; } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlIntegerParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlIntegerParser.cs index fd0c0ec..485c89f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlIntegerParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlIntegerParser.cs @@ -1,43 +1,47 @@ -using System.Xml.Linq; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Utilities; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using System; +using System.Xml.Linq; namespace PG.StarWarsGame.Files.XML.Parsers; -public sealed class PetroglyphXmlIntegerParser : PetroglyphPrimitiveXmlParser +public sealed class PetroglyphXmlIntegerParser : PetroglyphNumberParser { public static readonly PetroglyphXmlIntegerParser Instance = new(); private protected override int DefaultValue => 0; + internal override int EngineDataTypeId => 0x6; + +#if !NET7_0_OR_GREATER + protected override int MaxValue => int.MaxValue; + + protected override int MinValue => int.MinValue; +#endif + private PetroglyphXmlIntegerParser() { } - protected internal override int ParseCore(string trimmedValue, XElement element) + protected internal override int ParseCore(ReadOnlySpan trimmedValue, XElement element) { // The engines uses the C++ function std::atoi which is a little more loose. // For example the value '123d' get parsed to 123, // whereas in C# int.TryParse returns (false, 0) - if (!int.TryParse(trimmedValue, out var i)) + if (!int.TryParse(trimmedValue +#if NETSTANDARD2_0 + .ToString() +#endif + + , out var i)) { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.MalformedValue, - $"Expected integer but got '{trimmedValue}'.")); - return 0; + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.MalformedValue, + Message = $"Expected integer but got '{trimmedValue.ToString()}'.", + }); + return DefaultValue; } return i; } - - public int ParseWithRange(XElement element, int minValue, int maxValue) - { - var value = Parse(element); - var clamped = PGMath.Clamp(value, minValue, maxValue); - if (value != clamped) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"Expected integer between {minValue} and {maxValue} but got value '{value}'.")); - } - return clamped; - } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlLooseStringListParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlLooseStringListParser.cs index d06f660..3859a3a 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlLooseStringListParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlLooseStringListParser.cs @@ -20,21 +20,25 @@ public sealed class PetroglyphXmlLooseStringListParser : PetroglyphPrimitiveXmlP public static readonly PetroglyphXmlLooseStringListParser Instance = new(); private protected override IList DefaultValue => []; + internal override int EngineDataTypeId => 0x18 & 0x1B; private PetroglyphXmlLooseStringListParser() { } - protected internal override IList ParseCore(string trimmedValue, XElement element) + protected internal override IList ParseCore(ReadOnlySpan trimmedValue, XElement element) { if (trimmedValue.Length > 0x2000) { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.TooLongData, - $"Input value is too long '{trimmedValue.Length}' at {XmlLocationInfo.FromElement(element)}")); + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.TooLongData, + Message = $"Input value is too long '{trimmedValue.Length}' at {XmlLocationInfo.FromElement(element)}", + }); return DefaultValue; } - var entries = trimmedValue.Split(Separators, StringSplitOptions.RemoveEmptyEntries); + var entries = trimmedValue.ToString().Split(Separators, StringSplitOptions.RemoveEmptyEntries); return entries; } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlMax100ByteParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlMax100ByteParser.cs deleted file mode 100644 index ede64a3..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlMax100ByteParser.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Xml.Linq; -using PG.StarWarsGame.Files.XML.ErrorHandling; -using PG.StarWarsGame.Files.XML.Utilities; - -namespace PG.StarWarsGame.Files.XML.Parsers; - -public sealed class PetroglyphXmlMax100ByteParser : PetroglyphPrimitiveXmlParser -{ - public static readonly PetroglyphXmlMax100ByteParser Instance = new(); - - private protected override byte DefaultValue => 0; - - private PetroglyphXmlMax100ByteParser() - { - } - - protected internal override byte ParseCore(string trimmedValue, XElement element) - { - var intValue = PetroglyphXmlIntegerParser.Instance.ParseCore(trimmedValue, element); - - if (intValue > 100) - intValue = 100; - - var asByte = (byte)intValue; - if (intValue != asByte) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"Expected a byte value (0 - 255) but got value '{intValue}'.")); - } - - // Add additional check, cause the PG implementation is broken, but we need to stay "bug-compatible". - if (asByte > 100) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"Expected a byte value (0 - 100) but got value '{asByte}'.")); - } - - return asByte; - } - - public byte ParseWithRange(XElement element, byte minValue, byte maxValue) - { - if (maxValue > 100) - { - OnParseError(new XmlParseErrorEventArgs( - element, XmlParseErrorKind.InvalidValue, - $"The provided maxValue '{maxValue}' is above 100.")); - } - - // TODO: Do we need to coerce maxValue??? - - var value = Parse(element); - - var clamped = PGMath.Clamp(value, minValue, maxValue); - if (value != clamped) - { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"Expected byte between {minValue} and {maxValue} but got value '{value}'.")); - } - return clamped; - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlRgbaColorParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlRgbaColorParser.cs index 1ecf10a..e040aaf 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlRgbaColorParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlRgbaColorParser.cs @@ -9,12 +9,13 @@ public sealed class PetroglyphXmlRgbaColorParser : PetroglyphPrimitiveXmlParser< public static readonly PetroglyphXmlRgbaColorParser Instance = new(); private protected override Vector4Int DefaultValue => default; + internal override int EngineDataTypeId => 0x16; private PetroglyphXmlRgbaColorParser() { } - protected internal override Vector4Int ParseCore(string trimmedValue, XElement element) + protected internal override Vector4Int ParseCore(ReadOnlySpan trimmedValue, XElement element) { var values = PetroglyphXmlLooseStringListParser.Instance.ParseCore(trimmedValue, element); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlSByteParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlSByteParser.cs new file mode 100644 index 0000000..234b6ae --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlSByteParser.cs @@ -0,0 +1,41 @@ +using System; +using System.Xml.Linq; +using PG.StarWarsGame.Files.XML.ErrorHandling; + +namespace PG.StarWarsGame.Files.XML.Parsers; + +public sealed class PetroglyphXmlSByteParser : PetroglyphNumberParser +{ + public static readonly PetroglyphXmlSByteParser Instance = new(); + + private protected override sbyte DefaultValue => 0; + + internal override int EngineDataTypeId => 0x3; + +#if !NET7_0_OR_GREATER + protected override sbyte MaxValue => sbyte.MaxValue; + + protected override sbyte MinValue => sbyte.MinValue; +#endif + + private PetroglyphXmlSByteParser() + { + } + + protected internal override sbyte ParseCore(ReadOnlySpan trimmedValue, XElement element) + { + var intValue = PetroglyphXmlIntegerParser.Instance.ParseCore(trimmedValue, element); + + var asSByte = (sbyte)intValue; + if (intValue != asSByte) + { + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"Expected a byte value (0 - 255) but got value '{intValue}'.", + }); + } + + return asSByte; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlStringParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlStringParser.cs index dcca294..010a0f8 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlStringParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlStringParser.cs @@ -1,4 +1,5 @@ -using System.Xml.Linq; +using System; +using System.Xml.Linq; namespace PG.StarWarsGame.Files.XML.Parsers; @@ -6,14 +7,16 @@ public sealed class PetroglyphXmlStringParser : PetroglyphPrimitiveXmlParser string.Empty; + + internal override int EngineDataTypeId => 0x17 & 0x1D & 0x1F; + private PetroglyphXmlStringParser() { } - private protected override string DefaultValue => string.Empty; - - protected internal override string ParseCore(string trimmedValue, XElement element) + protected internal override string ParseCore(ReadOnlySpan trimmedValue, XElement element) { - return trimmedValue; + return trimmedValue.ToString(); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlUnsignedIntegerParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlUnsignedIntegerParser.cs index 7061cca..420f21d 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlUnsignedIntegerParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlUnsignedIntegerParser.cs @@ -1,27 +1,39 @@ -using System.Xml.Linq; -using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using System; +using System.Xml.Linq; namespace PG.StarWarsGame.Files.XML.Parsers; -public sealed class PetroglyphXmlUnsignedIntegerParser : PetroglyphPrimitiveXmlParser +public sealed class PetroglyphXmlUnsignedIntegerParser : PetroglyphNumberParser { public static readonly PetroglyphXmlUnsignedIntegerParser Instance = new(); private protected override uint DefaultValue => 0; + internal override int EngineDataTypeId => 0x5; + +#if !NET7_0_OR_GREATER + protected override uint MaxValue => uint.MaxValue; + + protected override uint MinValue => uint.MinValue; +#endif + private PetroglyphXmlUnsignedIntegerParser() { } - protected internal override uint ParseCore(string trimmedValue, XElement element) + protected internal override uint ParseCore(ReadOnlySpan trimmedValue, XElement element) { var intValue = PetroglyphXmlIntegerParser.Instance.ParseCore(trimmedValue, element); var asUint = (uint)intValue; if (intValue != asUint) { - OnParseError(new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, - $"Expected unsigned integer but got '{intValue}'.")); + ErrorReporter?.Report(new XmlError(this, element) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = $"Expected unsigned integer but got '{intValue}'.", + }); } return asUint; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlVector2FParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlVector2FParser.cs index 94900b7..488ee0a 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlVector2FParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlVector2FParser.cs @@ -1,4 +1,5 @@ -using System.Numerics; +using System; +using System.Numerics; using System.Xml.Linq; namespace PG.StarWarsGame.Files.XML.Parsers; @@ -11,13 +12,15 @@ public sealed class PetroglyphXmlVector2FParser : PetroglyphPrimitiveXmlParser default; + + internal override int EngineDataTypeId => 0x0F; + private PetroglyphXmlVector2FParser() { } - private protected override Vector2 DefaultValue => default; - - protected internal override Vector2 ParseCore(string trimmedValue, XElement element) + protected internal override Vector2 ParseCore(ReadOnlySpan trimmedValue, XElement element) { var listOfValues = LooseStringListParser.Parse(element); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/XmlContainerFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/XmlContainerFileParser.cs new file mode 100644 index 0000000..c75df69 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/XmlContainerFileParser.cs @@ -0,0 +1,33 @@ +using AnakinRaW.CommonUtilities.Collections; +using PG.Commons.Hashing; +using PG.StarWarsGame.Files.XML.Data; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using System; +using System.IO; +using System.Xml.Linq; + +namespace PG.StarWarsGame.Files.XML.Parsers; + +public sealed class XmlContainerFileParser( + IServiceProvider serviceProvider, + INamedXmlObjectParser elementParser, + IXmlParserErrorReporter? listener = null) + : PetroglyphXmlFileParserBase(serviceProvider, listener), IXmlContainerFileParser + where T : NamedXmlObject +{ + public INamedXmlObjectParser ElementParser { get; } = + elementParser ?? throw new ArgumentNullException(nameof(elementParser)); + + public void ParseFile(Stream xmlStream, IFrugalValueListDictionary parsedEntries) + { + var root = GetRootElement(xmlStream, out _); + foreach (var xElement in root.Elements()) + ParseElement(xElement, parsedEntries); + } + + private void ParseElement(XElement element, IFrugalValueListDictionary parsedEntries) + { + var parsedElement = ElementParser.Parse(element, parsedEntries, out var entryCrc); + parsedEntries.Add(entryCrc, parsedElement); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/XmlFileListParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/XmlFileListParser.cs index 380ea3f..0718ac4 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/XmlFileListParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/XmlFileListParser.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Xml.Linq; using PG.StarWarsGame.Files.XML.Data; using PG.StarWarsGame.Files.XML.ErrorHandling; @@ -7,32 +8,39 @@ namespace PG.StarWarsGame.Files.XML.Parsers; public sealed class XmlFileListParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) : - PetroglyphXmlFileParser(serviceProvider, errorReporter) -{ - protected override bool LoadLineInfo => false; - - protected override XmlFileListContainer Parse(XElement element, string fileName) + XmlFileParser(serviceProvider, errorReporter) +{ + protected override XmlFileList ParseRoot(XElement element, string fileName) { var files = new List(); foreach (var child in element.Elements()) { var tagName = GetTagName(child); - if (tagName == "File") + if (tagName != "File") { - var file = PetroglyphXmlStringParser.Instance.Parse(child); - if (file.Length == 0) + ErrorReporter?.Report(new XmlError(this, child) { - ErrorReporter?.Report(this, - new XmlParseErrorEventArgs(element, XmlParseErrorKind.InvalidValue, "Empty value in tag.")); - } - files.Add(file); + ErrorKind = XmlParseErrorKind.UnexceptedElementName, + Message = $"Tag '<{tagName}>' should not be used. Use '' only.", + }); } - else + + // NB: There intentionally is not else branch here, because that's how the engine behaves. + // It checks whether the tag is called "File" and reports an assert if not. + // However, it still consumes the value and treats it as file. + + var file = PetroglyphXmlStringParser.Instance.Parse(child); + if (file.Length == 0) { - ErrorReporter?.Report(this, new XmlParseErrorEventArgs(child, XmlParseErrorKind.UnknownNode, - $"Tag '<{tagName}>' is not supported. Only '' is supported.")); + ErrorReporter?.Report(new XmlError(this, child) + { + ErrorKind = XmlParseErrorKind.InvalidValue, + Message = "Empty value in tag", + }); } + + files.Add(file); } - return new XmlFileListContainer(files); + return new XmlFileList(new ReadOnlyCollection(files), new XmlLocationInfo(fileName, null)); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/XmlFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/XmlFileParser.cs new file mode 100644 index 0000000..8890a61 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/XmlFileParser.cs @@ -0,0 +1,19 @@ +using PG.StarWarsGame.Files.XML.Data; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using System; +using System.IO; +using System.Xml.Linq; + +namespace PG.StarWarsGame.Files.XML.Parsers; + +public abstract class XmlFileParser(IServiceProvider serviceProvider, IXmlParserErrorReporter? errorReporter = null) + : PetroglyphXmlFileParserBase(serviceProvider, errorReporter) where T : XmlObject +{ + public T ParseFile(Stream xmlStream) + { + var root = GetRootElement(xmlStream, out var fileName); + return ParseRoot(root, fileName); + } + + protected abstract T ParseRoot(XElement element, string fileName); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Utilities/PGMath.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Utilities/PGMath.cs index bc40317..4290b15 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Utilities/PGMath.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Utilities/PGMath.cs @@ -1,5 +1,8 @@ using System; using System.Runtime.CompilerServices; +#if NET7_0_OR_GREATER +using System.Numerics; +#endif namespace PG.StarWarsGame.Files.XML.Utilities; @@ -8,8 +11,11 @@ internal static class PGMath [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int Clamp(int value, int min, int max) { +#if NETSTANDARD2_1_OR_GREATER || NET + return Math.Clamp(value, min, max); +#endif if (min > max) - throw new ArgumentException("min cannot be larger than max."); + throw new ArgumentException($"'{max}' cannot be greater than {min}."); if (value < min) return min; return value > max ? max : value; @@ -18,10 +24,60 @@ public static int Clamp(int value, int min, int max) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static byte Clamp(byte value, byte min, byte max) { +#if NETSTANDARD2_1_OR_GREATER || NET + return Math.Clamp(value, min, max); +#endif if (min > max) - throw new ArgumentException("min cannot be larger than max."); + throw new ArgumentException($"'{max}' cannot be greater than {min}."); if (value < min) return min; return value > max ? max : value; } + +#if NET7_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Clamp(T value, T min, T max) where T : INumber + { + if (min > max) + throw new ArgumentException($"'{max}' cannot be greater than {min}."); + if (value < min) + return min; + return value > max ? max : value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Max(T val1, T val2) where T : INumber + { + return (val1 >= val2) ? val1 : val2; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Min(T val1, T val2) where T : INumber + { + return (val1 <= val2) ? val1 : val2; + } +# else + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Clamp(T value, T min, T max) where T : struct, IEquatable, IComparable + { + if (min.CompareTo(max) > 0) + throw new ArgumentException($"'{max}' cannot be greater than {min}."); + if (value.CompareTo(min) < 0) + return min; + return value.CompareTo(max) > 0 ? max : value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Max(T val1, T val2) where T : struct, IEquatable, IComparable + { + return val1.CompareTo(val2) >= 0 ? val1 : val2; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Min(T val1, T val2) where T : struct, IEquatable, IComparable + { + return val1.CompareTo(val2) <= 0 ? val1 : val2; + } +#endif } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XElementExtensions.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XElementExtensions.cs new file mode 100644 index 0000000..eb7a6c0 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XElementExtensions.cs @@ -0,0 +1,15 @@ +using System.Xml.Linq; + +namespace PG.StarWarsGame.Files.XML; + +public static class XElementExtensions +{ + extension(XElement element) + { + /// + /// Gets the value of the element as the Petroglyph engine would parse it. + /// That is, if the element has child elements, it returns an empty string, otherwise it returns the value of the element. + /// + public string PGValue => element.HasElements ? string.Empty : element.Value; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XmlFileConstants.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XmlFileConstants.cs new file mode 100644 index 0000000..fb0de39 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XmlFileConstants.cs @@ -0,0 +1,10 @@ +using System.Text; + +namespace PG.StarWarsGame.Files.XML; + +public static class XmlFileConstants +{ + public const int MaxTagNameLength = 255; + + public static readonly Encoding XmlEncoding = Encoding.ASCII; +} \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj b/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj index ef3c9ef..f28c11c 100644 --- a/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj +++ b/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj @@ -13,19 +13,19 @@ - - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -35,10 +35,6 @@ - - - - true true diff --git a/test/ModVerify.CliApp.Test/TestData/NoVerifierProvider.cs b/test/ModVerify.CliApp.Test/TestData/NoVerifierProvider.cs index ef7ad74..649aeb7 100644 --- a/test/ModVerify.CliApp.Test/TestData/NoVerifierProvider.cs +++ b/test/ModVerify.CliApp.Test/TestData/NoVerifierProvider.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using AET.ModVerify.Pipeline; +using AET.ModVerify; using AET.ModVerify.Settings; using AET.ModVerify.Verifiers; using PG.StarWarsGame.Engine;