From d8610036d92d795b6bf410401c0491af69a4a8e7 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sun, 2 Jun 2024 14:05:09 +0200 Subject: [PATCH 01/25] commit to switch devices --- src/ModVerify/ModVerify.csproj | 2 +- src/ModVerify/Steps/GameVerificationStep.cs | 14 ++--- .../Steps/VerifyReferencedModelsStep.cs | 7 +-- src/ModVerify/VerifyGamePipeline.cs | 63 ++++++++++++------- .../PG.StarWarsGame.Engine/GameDatabase.cs | 3 +- .../PG.StarWarsGame.Engine.csproj | 2 +- .../Pipeline/CreateGameDatabasePipeline.cs | 39 +++++++++--- .../Pipeline/CreateGameDatabaseStep.cs | 31 --------- 8 files changed, 80 insertions(+), 81 deletions(-) delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/CreateGameDatabaseStep.cs diff --git a/src/ModVerify/ModVerify.csproj b/src/ModVerify/ModVerify.csproj index 8febcd9..bc7995d 100644 --- a/src/ModVerify/ModVerify.csproj +++ b/src/ModVerify/ModVerify.csproj @@ -20,7 +20,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/ModVerify/Steps/GameVerificationStep.cs b/src/ModVerify/Steps/GameVerificationStep.cs index 295cd70..f759843 100644 --- a/src/ModVerify/Steps/GameVerificationStep.cs +++ b/src/ModVerify/Steps/GameVerificationStep.cs @@ -8,13 +8,11 @@ using Microsoft.Extensions.Logging; using PG.StarWarsGame.Engine; using PG.StarWarsGame.Engine.FileSystem; -using PG.StarWarsGame.Engine.Pipeline; namespace AET.ModVerify.Steps; public abstract class GameVerificationStep( - CreateGameDatabaseStep createDatabaseStep, - IGameRepository repository, + GameDatabase gameDatabase, VerificationSettings settings, IServiceProvider serviceProvider) : PipelineStep(serviceProvider) @@ -28,9 +26,9 @@ public abstract class GameVerificationStep( protected VerificationSettings Settings { get; } = settings; - protected GameDatabase Database { get; private set; } = null!; + protected GameDatabase Database { get; } = gameDatabase ?? throw new ArgumentNullException(nameof(gameDatabase)); - protected IGameRepository Repository => repository; + protected IGameRepository Repository => gameDatabase.GameRepository; protected abstract string LogFileName { get; } @@ -38,9 +36,6 @@ public abstract class GameVerificationStep( protected sealed override void RunCore(CancellationToken token) { - createDatabaseStep.Wait(); - Database = createDatabaseStep.GameDatabase; - Logger?.LogInformation($"Running verifier '{Name}'..."); try { @@ -57,12 +52,11 @@ protected sealed override void RunCore(CancellationToken token) protected abstract void RunVerification(CancellationToken token); - protected void AddError(VerificationError error) { if (!OnError(error)) { - Logger?.LogTrace($"Error suppressed: '{error}'"); + Logger?.LogTrace($"Error suppressed for verifier '{Name}': '{error}'"); return; } _verifyErrors.Add(error); diff --git a/src/ModVerify/Steps/VerifyReferencedModelsStep.cs b/src/ModVerify/Steps/VerifyReferencedModelsStep.cs index 5151e5b..1494181 100644 --- a/src/ModVerify/Steps/VerifyReferencedModelsStep.cs +++ b/src/ModVerify/Steps/VerifyReferencedModelsStep.cs @@ -8,8 +8,6 @@ using PG.Commons.Files; using PG.Commons.Utilities; using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Engine.FileSystem; -using PG.StarWarsGame.Engine.Pipeline; using PG.StarWarsGame.Files.ALO.Files.Models; using PG.StarWarsGame.Files.ALO.Files.Particles; using PG.StarWarsGame.Files.ALO.Services; @@ -18,11 +16,10 @@ namespace AET.ModVerify.Steps; internal sealed class VerifyReferencedModelsStep( - CreateGameDatabaseStep createDatabaseStep, - IGameRepository repository, + GameDatabase database, VerificationSettings settings, IServiceProvider serviceProvider) - : GameVerificationStep(createDatabaseStep, repository, settings, serviceProvider) + : GameVerificationStep(database, settings, serviceProvider) { public const string ModelNotFound = "ALO00"; public const string ModelBroken = "ALO01"; diff --git a/src/ModVerify/VerifyGamePipeline.cs b/src/ModVerify/VerifyGamePipeline.cs index 8b51723..e1a17ad 100644 --- a/src/ModVerify/VerifyGamePipeline.cs +++ b/src/ModVerify/VerifyGamePipeline.cs @@ -5,49 +5,42 @@ using System.Threading.Tasks; using AET.ModVerify.Steps; using AnakinRaW.CommonUtilities.SimplePipeline; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using PG.StarWarsGame.Engine.FileSystem; using PG.StarWarsGame.Engine.Pipeline; namespace AET.ModVerify; -public class VerifyGamePipeline : ParallelPipeline +public abstract class VerifyGamePipeline : Pipeline { - private IList _verificationSteps = null!; - private readonly IGameRepository _repository; + private IList _verificationSteps = new List(); + private readonly GameLocations _gameLocations; private readonly VerificationSettings _settings; - public VerifyGamePipeline(IGameRepository gameRepository, VerificationSettings settings, IServiceProvider serviceProvider) - : base(serviceProvider, 4, false) + protected VerifyGamePipeline(GameLocations gameLocations, VerificationSettings settings, IServiceProvider serviceProvider) + : base(serviceProvider) { - _repository = gameRepository; - _settings = settings; + _gameLocations = gameLocations ?? throw new ArgumentNullException(nameof(gameLocations)); + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); } - protected override Task> BuildSteps() - { - var buildIndexStep = new CreateGameDatabaseStep(_repository, ServiceProvider); - - _verificationSteps = new List - { - new VerifyReferencedModelsStep(buildIndexStep, _repository, _settings, ServiceProvider), - }; - - var allSteps = new List - { - buildIndexStep - }; - allSteps.AddRange(_verificationSteps); - return Task.FromResult>(allSteps); + protected override Task PrepareCoreAsync() + { + throw new NotImplementedException(); } - public override async Task RunAsync(CancellationToken token = default) + protected override async Task RunCoreAsync(CancellationToken token) { Logger?.LogInformation("Verifying game..."); try { - await base.RunAsync(token).ConfigureAwait(false); + var databaseService = ServiceProvider.GetRequiredService(); + + await databaseService.CreateDatabaseAsync() + + var stepsWithVerificationErrors = _verificationSteps.Where(x => x.VerifyErrors.Any()).ToList(); @@ -69,4 +62,26 @@ public override async Task RunAsync(CancellationToken token = default) Logger?.LogInformation("Finished game verification"); } } + + + //protected sealed override async Task> BuildSteps() + //{ + // var buildIndexStep = new CreateGameDatabaseStep(_repository, ServiceProvider); + + // _verificationSteps = new List + // { + // new VerifyReferencedModelsStep(buildIndexStep, _repository, _settings, ServiceProvider), + // }; + + // var allSteps = new List + // { + // buildIndexStep + // }; + // allSteps.AddRange(CreateVeificationSteps()); + + // return allSteps; + //} + + + protected abstract IEnumerable CreateVerificationSteps(); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameDatabase.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameDatabase.cs index 7abf073..152e811 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameDatabase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameDatabase.cs @@ -1,11 +1,12 @@ using System.Collections.Generic; using PG.StarWarsGame.Engine.DataTypes; +using PG.StarWarsGame.Engine.FileSystem; namespace PG.StarWarsGame.Engine; public class GameDatabase { - public required GameEngineType EngineType { get; init; } + public required IGameRepository GameRepository { get; init; } public required GameConstants GameConstants { get; init; } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj index 4487bb3..ba82246 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj @@ -17,7 +17,7 @@ true - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/CreateGameDatabasePipeline.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/CreateGameDatabasePipeline.cs index 53fb102..d1973b7 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/CreateGameDatabasePipeline.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/CreateGameDatabasePipeline.cs @@ -4,13 +4,28 @@ using System.Threading; using System.Threading.Tasks; using AnakinRaW.CommonUtilities.SimplePipeline; +using Microsoft.Extensions.Logging; using PG.StarWarsGame.Engine.DataTypes; using PG.StarWarsGame.Engine.FileSystem; namespace PG.StarWarsGame.Engine.Pipeline; -internal class CreateGameDatabasePipeline(IGameRepository repository, IServiceProvider serviceProvider) - : ParallelPipeline(serviceProvider) +public interface IGameDatabaseService +{ + Task CreateDatabaseAsync(GameEngineType targetEngineType, GameLocations locations, CancellationToken cancellationToken = default); +} + +internal class GameDatabaseService(IServiceProvider serviceProvider) : IGameDatabaseService +{ + public async Task CreateDatabaseAsync(GameEngineType targetEngineType, GameLocations locations, CancellationToken cancellationToken = default) + { + var pipeline = new GameDatabaseCreationPipeline(repository, serviceProvider, cancellationToken); + await pipeline.RunAsync(cancellationToken); + return pipeline.GameDatabase; + } +} + +internal class GameDatabaseCreationPipeline(GameRepository repository, IServiceProvider serviceProvider) : ParallelPipeline(serviceProvider) { private ParseSingletonXmlStep _parseGameConstants = null!; private ParseFromContainerStep _parseGameObjects = null!; @@ -71,14 +86,22 @@ protected override Task> BuildSteps() protected override async Task RunCoreAsync(CancellationToken token) { - await base.RunCoreAsync(token); + Logger?.LogInformation("Creating Game Database..."); - GameDatabase = new GameDatabase + try { - EngineType = repository.EngineType, - GameConstants = _parseGameConstants.Database, - GameObjects = _parseGameObjects.Database - }; + await base.RunCoreAsync(token); + + GameDatabase = new GameDatabase + { + GameConstants = _parseGameConstants.Database, + GameObjects = _parseGameObjects.Database + }; + } + finally + { + Logger?.LogInformation("Finished creating game database"); + } } private sealed class ParseSingletonXmlStep(string name, string xmlFile, IGameRepository repository, IServiceProvider serviceProvider) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/CreateGameDatabaseStep.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/CreateGameDatabaseStep.cs deleted file mode 100644 index 361e823..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/CreateGameDatabaseStep.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Threading; -using AnakinRaW.CommonUtilities.SimplePipeline.Steps; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using PG.StarWarsGame.Engine.FileSystem; - -namespace PG.StarWarsGame.Engine.Pipeline; - -public class CreateGameDatabaseStep : SynchronizedStep -{ - private readonly IGameRepository _gameRepository; - private readonly ILogger? _logger; - - public GameDatabase GameDatabase { get; private set; } = null!; - - public CreateGameDatabaseStep(IGameRepository gameRepository, IServiceProvider serviceProvider) : base(serviceProvider) - { - _logger = Services.GetService()?.CreateLogger(GetType()); - _gameRepository = gameRepository; - } - - protected override void RunSynchronized(CancellationToken token) - { - _logger?.LogInformation("Creating Game Database..."); - var indexGamesPipeline = new CreateGameDatabasePipeline(_gameRepository, Services); - indexGamesPipeline.RunAsync(token).Wait(); - GameDatabase = indexGamesPipeline.GameDatabase; - _logger?.LogInformation("Finished creating game database"); - } -} \ No newline at end of file From 003874e2258bd85b301a4a584ddb5497f4ad3f4a Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Mon, 3 Jun 2024 21:53:57 +0200 Subject: [PATCH 02/25] some refactorings --- src/ModVerify.CliApp/Program.cs | 24 +- src/ModVerify/IVerificationProvider.cs | 10 + src/ModVerify/ModVerifyServiceContribution.cs | 11 + src/ModVerify/Steps/GameVerificationStep.cs | 8 +- ...encedModelsStep.cs => VerifyModelsStep.cs} | 5 +- src/ModVerify/VerificationProvider.cs | 14 + src/ModVerify/VerificationSettings.cs | 2 + src/ModVerify/VerifyGamePipeline.cs | 70 ++-- .../{ => Database}/GameDatabase.cs | 6 +- .../Database/IGameDatabase.cs | 14 + .../FileSystem/AudioRepository.cs | 6 - .../FileSystem/GameRepository.cs | 304 ------------------ .../FileSystem/IGameRepositoryFactory.cs | 6 - .../{FileSystem => }/GameLocations.cs | 4 +- .../PetroglyphEngineServiceContribution.cs | 4 +- .../Pipeline/CreateDatabaseStep.cs | 2 +- ...ine.cs => GameDatabaseCreationPipeline.cs} | 21 +- .../Pipeline/GameDatabaseService.cs | 21 ++ .../Pipeline/IGameDatabaseService.cs | 10 + .../ParseXmlDatabaseFromContainerStep.cs | 2 +- .../Pipeline/ParseXmlDatabaseStep.cs | 2 +- .../Repositories/AudioRepository.cs | 6 + .../EffectsRepository.cs | 2 +- .../Repositories/FocGameRepository.cs | 98 ++++++ .../Repositories/GameRepository.cs | 302 +++++++++++++++++ .../GameRepositoryFactory.cs | 4 +- .../IGameRepository.cs | 6 +- .../Repositories/IGameRepositoryFactory.cs | 6 + .../IRepository.cs | 2 +- .../Utilities/DirectoryInfoGlobbingWrapper.cs | 106 ++++++ .../Utilities/FileInfoGlobbingWrapper.cs | 35 ++ .../Utilities/MatcherExtensions.cs | 78 +++++ .../Utilities/StringUtilities.cs | 24 -- .../Utilities/ValueStringBuilder.cs | 2 +- 34 files changed, 804 insertions(+), 413 deletions(-) create mode 100644 src/ModVerify/IVerificationProvider.cs create mode 100644 src/ModVerify/ModVerifyServiceContribution.cs rename src/ModVerify/Steps/{VerifyReferencedModelsStep.cs => VerifyModelsStep.cs} (98%) create mode 100644 src/ModVerify/VerificationProvider.cs rename src/PetroglyphTools/PG.StarWarsGame.Engine/{ => Database}/GameDatabase.cs (68%) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabase.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/AudioRepository.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/GameRepository.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/IGameRepositoryFactory.cs rename src/PetroglyphTools/PG.StarWarsGame.Engine/{FileSystem => }/GameLocations.cs (94%) rename src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/{CreateGameDatabasePipeline.cs => GameDatabaseCreationPipeline.cs} (84%) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/GameDatabaseService.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/IGameDatabaseService.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/AudioRepository.cs rename src/PetroglyphTools/PG.StarWarsGame.Engine/{FileSystem => Repositories}/EffectsRepository.cs (98%) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/FocGameRepository.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs rename src/PetroglyphTools/PG.StarWarsGame.Engine/{FileSystem => Repositories}/GameRepositoryFactory.cs (71%) rename src/PetroglyphTools/PG.StarWarsGame.Engine/{FileSystem => Repositories}/IGameRepository.cs (57%) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepositoryFactory.cs rename src/PetroglyphTools/PG.StarWarsGame.Engine/{FileSystem => Repositories}/IRepository.cs (84%) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/DirectoryInfoGlobbingWrapper.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/FileInfoGlobbingWrapper.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/MatcherExtensions.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/StringUtilities.cs diff --git a/src/ModVerify.CliApp/Program.cs b/src/ModVerify.CliApp/Program.cs index b4157d8..7800a90 100644 --- a/src/ModVerify.CliApp/Program.cs +++ b/src/ModVerify.CliApp/Program.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; using AET.ModVerify; +using AET.ModVerify.Steps; using AET.SteamAbstraction; using AnakinRaW.CommonUtilities.Hashing; using AnakinRaW.CommonUtilities.Registry; @@ -14,7 +15,7 @@ using Microsoft.Extensions.Logging; using PG.Commons.Extensibility; using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Engine.FileSystem; +using PG.StarWarsGame.Engine.Database; using PG.StarWarsGame.Files.ALO; using PG.StarWarsGame.Files.DAT.Services.Builder; using PG.StarWarsGame.Files.MEG.Data.Archives; @@ -97,9 +98,8 @@ private static VerifyGamePipeline BuildPipeline(IPlayableObject playableObject, playableObject.Game.Directory.FullName, fallbackGame.Directory.FullName); - var repo = _services.GetRequiredService().Create(GameEngineType.Foc, gameLocations); - return new VerifyGamePipeline(repo, VerificationSettings.Default, _services); + return new ModVerifyPipeline(GameEngineType.Foc, gameLocations, VerificationSettings.Default, _services); } private static IServiceProvider CreateAppServices() @@ -121,8 +121,9 @@ private static IServiceProvider CreateAppServices() RuntimeHelpers.RunClassConstructor(typeof(IMegArchive).TypeHandle); AloServiceContribution.ContributeServices(serviceCollection); serviceCollection.CollectPgServiceContributions(); - + PetroglyphEngineServiceContribution.ContributeServices(serviceCollection); + ModVerifyServiceContribution.ContributeServices(serviceCollection); return serviceCollection.BuildServiceProvider(); } @@ -139,5 +140,18 @@ private static void ConfigureLogging(ILoggingBuilder loggingBuilder) #endif loggingBuilder.AddConsole(); } - +} + +internal class ModVerifyPipeline( + GameEngineType targetType, + GameLocations gameLocations, + VerificationSettings settings, + IServiceProvider serviceProvider) + : VerifyGamePipeline(targetType, gameLocations, settings, serviceProvider) +{ + protected override IEnumerable CreateVerificationSteps(IGameDatabase database) + { + var verifyProvider = ServiceProvider.GetRequiredService(); + return verifyProvider.GetAllDefaultVerifiers(database, Settings); + } } \ No newline at end of file diff --git a/src/ModVerify/IVerificationProvider.cs b/src/ModVerify/IVerificationProvider.cs new file mode 100644 index 0000000..fb2779b --- /dev/null +++ b/src/ModVerify/IVerificationProvider.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using AET.ModVerify.Steps; +using PG.StarWarsGame.Engine.Database; + +namespace AET.ModVerify; + +public interface IVerificationProvider +{ + IEnumerable GetAllDefaultVerifiers(IGameDatabase database, VerificationSettings settings); +} \ No newline at end of file diff --git a/src/ModVerify/ModVerifyServiceContribution.cs b/src/ModVerify/ModVerifyServiceContribution.cs new file mode 100644 index 0000000..d223d2c --- /dev/null +++ b/src/ModVerify/ModVerifyServiceContribution.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace AET.ModVerify; + +public static class ModVerifyServiceContribution +{ + public static void ContributeServices(IServiceCollection serviceCollection) + { + serviceCollection.AddSingleton(sp => new VerificationProvider(sp)); + } +} \ No newline at end of file diff --git a/src/ModVerify/Steps/GameVerificationStep.cs b/src/ModVerify/Steps/GameVerificationStep.cs index f759843..065924a 100644 --- a/src/ModVerify/Steps/GameVerificationStep.cs +++ b/src/ModVerify/Steps/GameVerificationStep.cs @@ -6,13 +6,13 @@ using AnakinRaW.CommonUtilities.SimplePipeline.Steps; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Engine.FileSystem; +using PG.StarWarsGame.Engine.Database; +using PG.StarWarsGame.Engine.Repositories; namespace AET.ModVerify.Steps; public abstract class GameVerificationStep( - GameDatabase gameDatabase, + IGameDatabase gameDatabase, VerificationSettings settings, IServiceProvider serviceProvider) : PipelineStep(serviceProvider) @@ -26,7 +26,7 @@ public abstract class GameVerificationStep( protected VerificationSettings Settings { get; } = settings; - protected GameDatabase Database { get; } = gameDatabase ?? throw new ArgumentNullException(nameof(gameDatabase)); + protected IGameDatabase Database { get; } = gameDatabase ?? throw new ArgumentNullException(nameof(gameDatabase)); protected IGameRepository Repository => gameDatabase.GameRepository; diff --git a/src/ModVerify/Steps/VerifyReferencedModelsStep.cs b/src/ModVerify/Steps/VerifyModelsStep.cs similarity index 98% rename from src/ModVerify/Steps/VerifyReferencedModelsStep.cs rename to src/ModVerify/Steps/VerifyModelsStep.cs index 1494181..b33b3d1 100644 --- a/src/ModVerify/Steps/VerifyReferencedModelsStep.cs +++ b/src/ModVerify/Steps/VerifyModelsStep.cs @@ -8,6 +8,7 @@ using PG.Commons.Files; using PG.Commons.Utilities; using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Engine.Database; using PG.StarWarsGame.Files.ALO.Files.Models; using PG.StarWarsGame.Files.ALO.Files.Particles; using PG.StarWarsGame.Files.ALO.Services; @@ -16,7 +17,7 @@ namespace AET.ModVerify.Steps; internal sealed class VerifyReferencedModelsStep( - GameDatabase database, + IGameDatabase database, VerificationSettings settings, IServiceProvider serviceProvider) : GameVerificationStep(database, settings, serviceProvider) @@ -42,7 +43,7 @@ protected override void RunVerification(CancellationToken token) var aloQueue = new Queue(Database.GameObjects .SelectMany(x => x.Models) .Concat(FocHardcodedConstants.HardcodedModels)); - + var visitedAloFiles = new HashSet(StringComparer.OrdinalIgnoreCase); while (aloQueue.Count != 0) diff --git a/src/ModVerify/VerificationProvider.cs b/src/ModVerify/VerificationProvider.cs new file mode 100644 index 0000000..3a347c8 --- /dev/null +++ b/src/ModVerify/VerificationProvider.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using AET.ModVerify.Steps; +using PG.StarWarsGame.Engine.Database; + +namespace AET.ModVerify; + +internal class VerificationProvider(IServiceProvider serviceProvider) : IVerificationProvider +{ + public IEnumerable GetAllDefaultVerifiers(IGameDatabase database, VerificationSettings settings) + { + yield return new VerifyReferencedModelsStep(database, settings, serviceProvider); + } +} \ No newline at end of file diff --git a/src/ModVerify/VerificationSettings.cs b/src/ModVerify/VerificationSettings.cs index ab5e618..e98c5b4 100644 --- a/src/ModVerify/VerificationSettings.cs +++ b/src/ModVerify/VerificationSettings.cs @@ -2,6 +2,8 @@ public record VerificationSettings { + public int ParallelWorkers { get; init; } = 4; + public static readonly VerificationSettings Default = new() { ThrowBehavior = VerifyThrowBehavior.None diff --git a/src/ModVerify/VerifyGamePipeline.cs b/src/ModVerify/VerifyGamePipeline.cs index e1a17ad..cbf0bac 100644 --- a/src/ModVerify/VerifyGamePipeline.cs +++ b/src/ModVerify/VerifyGamePipeline.cs @@ -5,42 +5,70 @@ using System.Threading.Tasks; using AET.ModVerify.Steps; using AnakinRaW.CommonUtilities.SimplePipeline; +using AnakinRaW.CommonUtilities.SimplePipeline.Runners; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using PG.StarWarsGame.Engine.FileSystem; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Engine.Database; using PG.StarWarsGame.Engine.Pipeline; namespace AET.ModVerify; public abstract class VerifyGamePipeline : Pipeline { - private IList _verificationSteps = new List(); + private readonly List _verificationSteps = new(); + private readonly GameEngineType _targetType; private readonly GameLocations _gameLocations; - private readonly VerificationSettings _settings; + private readonly ParallelRunner _verifyRunner; - protected VerifyGamePipeline(GameLocations gameLocations, VerificationSettings settings, IServiceProvider serviceProvider) + protected VerificationSettings Settings { get; } + + protected VerifyGamePipeline(GameEngineType targetType, GameLocations gameLocations, VerificationSettings settings, IServiceProvider serviceProvider) : base(serviceProvider) { + _targetType = targetType; _gameLocations = gameLocations ?? throw new ArgumentNullException(nameof(gameLocations)); - _settings = settings ?? throw new ArgumentNullException(nameof(settings)); + Settings = settings ?? throw new ArgumentNullException(nameof(settings)); + + if (settings.ParallelWorkers is < 0 or > 64) + throw new ArgumentException("Settings has invalid parallel worker number.", nameof(settings)); + + _verifyRunner = new ParallelRunner(settings.ParallelWorkers, serviceProvider); } - protected override Task PrepareCoreAsync() + protected sealed override Task PrepareCoreAsync() { - throw new NotImplementedException(); + _verificationSteps.Clear(); + return Task.FromResult(true); } - protected override async Task RunCoreAsync(CancellationToken token) + protected sealed override async Task RunCoreAsync(CancellationToken token) { Logger?.LogInformation("Verifying game..."); try { var databaseService = ServiceProvider.GetRequiredService(); - await databaseService.CreateDatabaseAsync() + var database = await databaseService.CreateDatabaseAsync(_targetType, _gameLocations, token); + foreach (var gameVerificationStep in CreateVerificationSteps(database)) + { + _verifyRunner.AddStep(gameVerificationStep); + _verificationSteps.Add(gameVerificationStep); + } + try + { + Logger?.LogInformation("Verifying..."); + _verifyRunner.Error += OnError; + await _verifyRunner.RunAsync(token); + } + finally + { + _verifyRunner.Error -= OnError; + Logger?.LogInformation("Finished Verifying"); + } var stepsWithVerificationErrors = _verificationSteps.Where(x => x.VerifyErrors.Any()).ToList(); @@ -54,7 +82,7 @@ await databaseService.CreateDatabaseAsync() } } - if (_settings.ThrowBehavior == VerifyThrowBehavior.FinalThrow && failedSteps.Count > 0) + if (Settings.ThrowBehavior == VerifyThrowBehavior.FinalThrow && failedSteps.Count > 0) throw new GameVerificationException(stepsWithVerificationErrors); } finally @@ -63,25 +91,5 @@ await databaseService.CreateDatabaseAsync() } } - - //protected sealed override async Task> BuildSteps() - //{ - // var buildIndexStep = new CreateGameDatabaseStep(_repository, ServiceProvider); - - // _verificationSteps = new List - // { - // new VerifyReferencedModelsStep(buildIndexStep, _repository, _settings, ServiceProvider), - // }; - - // var allSteps = new List - // { - // buildIndexStep - // }; - // allSteps.AddRange(CreateVeificationSteps()); - - // return allSteps; - //} - - - protected abstract IEnumerable CreateVerificationSteps(); + protected abstract IEnumerable CreateVerificationSteps(IGameDatabase database); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameDatabase.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabase.cs similarity index 68% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/GameDatabase.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabase.cs index 152e811..cae38db 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/GameDatabase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabase.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; using PG.StarWarsGame.Engine.DataTypes; -using PG.StarWarsGame.Engine.FileSystem; +using PG.StarWarsGame.Engine.Repositories; -namespace PG.StarWarsGame.Engine; +namespace PG.StarWarsGame.Engine.Database; -public class GameDatabase +internal class GameDatabase : IGameDatabase { public required IGameRepository GameRepository { get; init; } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabase.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabase.cs new file mode 100644 index 0000000..d228af7 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabase.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using PG.StarWarsGame.Engine.DataTypes; +using PG.StarWarsGame.Engine.Repositories; + +namespace PG.StarWarsGame.Engine.Database; + +public interface IGameDatabase +{ + public IGameRepository GameRepository { get; } + + public GameConstants GameConstants { get; } + + public IList GameObjects { get; } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/AudioRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/AudioRepository.cs deleted file mode 100644 index 8ffc276..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/AudioRepository.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace PG.StarWarsGame.Engine.FileSystem; - -public class AudioRepository -{ - -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/GameRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/GameRepository.cs deleted file mode 100644 index f7b1c63..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/GameRepository.cs +++ /dev/null @@ -1,304 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions; -using System.Linq; -using AnakinRaW.CommonUtilities.FileSystem; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.Xml; -using PG.StarWarsGame.Files.MEG.Data.Archives; -using PG.StarWarsGame.Files.MEG.Files; -using PG.StarWarsGame.Files.MEG.Services; -using PG.StarWarsGame.Files.MEG.Services.Builder.Normalization; -using PG.StarWarsGame.Files.XML; - -namespace PG.StarWarsGame.Engine.FileSystem; - -// EaW file lookup works slightly different! -public class FocGameRepository : IGameRepository -{ - private readonly IServiceProvider _serviceProvider; - private readonly IFileSystem _fileSystem; - private readonly PetroglyphDataEntryPathNormalizer _megPathNormalizer; - private readonly ICrc32HashingService _crc32HashingService; - private readonly IMegFileExtractor _megExtractor; - private readonly IMegFileService _megFileService; - private readonly ILogger? _logger; - - private readonly string _gameDirectory; - - private readonly IList _modPaths = new List(); - private readonly IList _fallbackPaths = new List(); - - private readonly IVirtualMegArchive? _masterMegArchive; - - public GameEngineType EngineType => GameEngineType.Foc; - - public IRepository EffectsRepository { get; } - - public FocGameRepository(GameLocations gameLocations, IServiceProvider serviceProvider) - { - if (gameLocations == null) - throw new ArgumentNullException(nameof(gameLocations)); - - _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - _megPathNormalizer = serviceProvider.GetRequiredService(); - _crc32HashingService = serviceProvider.GetRequiredService(); - _megExtractor = serviceProvider.GetRequiredService(); - _megFileService = serviceProvider.GetRequiredService(); - _logger = serviceProvider.GetService()?.CreateLogger(GetType()); - - _fileSystem = serviceProvider.GetRequiredService(); - - foreach (var mod in gameLocations.ModPaths) - { - if (string.IsNullOrEmpty(mod)) - { - _logger?.LogTrace("Skipping null or empty mod path."); - continue; - } - _modPaths.Add(_fileSystem.Path.GetFullPath(mod)); - } - - _gameDirectory = _fileSystem.Path.GetFullPath(gameLocations.GamePath); - - - foreach (var fallbackPath in gameLocations.FallbackPaths) - { - if (string.IsNullOrEmpty(fallbackPath)) - { - _logger?.LogTrace("Skipping null or empty fallback path."); - continue; - } - _fallbackPaths.Add(_fileSystem.Path.GetFullPath(fallbackPath)); - } - - _masterMegArchive = CreateMasterMegArchive(); - - EffectsRepository = new EffectsRepository(this, serviceProvider); - } - - private IVirtualMegArchive CreateMasterMegArchive() - { - var builder = _serviceProvider.GetRequiredService(); - - var megsToConsider = new List(); - - // We assume that the first fallback path (if present at all) is always Empire at War. - var firstFallback = _fallbackPaths.FirstOrDefault(); - if (firstFallback is not null) - { - var eawMegs = LoadMegArchivesFromXml(firstFallback); - var eawPatch = LoadMegArchive(_fileSystem.Path.Combine(firstFallback, "Data\\Patch.meg")); - var eawPatch2 = LoadMegArchive(_fileSystem.Path.Combine(firstFallback, "Data\\Patch2.meg")); - var eaw64Patch = LoadMegArchive(_fileSystem.Path.Combine(firstFallback, "Data\\64Patch.meg")); - - megsToConsider.AddRange(eawMegs); - if (eawPatch is not null) - megsToConsider.Add(eawPatch); - if (eawPatch2 is not null) - megsToConsider.Add(eawPatch2); - if (eaw64Patch is not null) - megsToConsider.Add(eaw64Patch); - } - - var focOrModMegs = LoadMegArchivesFromXml("."); - var focPatch = LoadMegArchive("Data\\Patch.meg"); - var focPatch2 = LoadMegArchive("Data\\Patch2.meg"); - var foc64Patch = LoadMegArchive("Data\\64Patch.meg"); - - megsToConsider.AddRange(focOrModMegs); - if (focPatch is not null) - megsToConsider.Add(focPatch); - if (focPatch2 is not null) - megsToConsider.Add(focPatch2); - if (foc64Patch is not null) - megsToConsider.Add(foc64Patch); - - return builder.BuildFrom(megsToConsider, true); - } - - private IList LoadMegArchivesFromXml(string lookupPath) - { - var megFilesXmlPath = _fileSystem.Path.Combine(lookupPath, "Data\\MegaFiles.xml"); - - var fileParserFactory = _serviceProvider.GetRequiredService(); - - using var xmlStream = TryOpenFile(megFilesXmlPath); - - if (xmlStream is null) - { - _logger?.LogWarning($"Unable to find MegaFiles.xml at '{lookupPath}'"); - return Array.Empty(); - } - - var parser = fileParserFactory.GetFileParser(); - var megaFilesXml = parser.ParseFile(xmlStream); - - - var megs = new List(megaFilesXml.Files.Count); - - foreach (var file in megaFilesXml.Files.Select(x => x.Trim())) - { - var megPath = _fileSystem.Path.Combine(lookupPath, file); - var megFile = LoadMegArchive(megPath); - if (megFile is not null) - megs.Add(megFile); - } - - return megs; - } - - private IMegFile? LoadMegArchive(string megPath) - { - using var megFileStream = TryOpenFile(megPath); - if (megFileStream is not FileSystemStream fileSystemStream) - { - _logger?.LogWarning($"Unable to find MEG data at '{megPath}'"); - return null; - } - - var megFile = _megFileService.Load(fileSystemStream); - - if (megFile.FileInformation.FileVersion != MegFileVersion.V1) - throw new InvalidOperationException("MEG data version must be V1."); - - return megFile; - } - - public Stream OpenFile(string filePath, bool megFileOnly = false) - { - var stream = TryOpenFile(filePath, megFileOnly); - if (stream is null) - throw new FileNotFoundException($"Unable to find game data: {filePath}"); - return stream; - } - - public bool FileExists(string filePath, string[] extensions, bool megFileOnly = false) - { - foreach (var extension in extensions) - { - var newPath = _fileSystem.Path.ChangeExtension(filePath, extension); - if (FileExists(newPath, megFileOnly)) - return true; - } - return false; - } - - - public bool FileExists(string filePath, bool megFileOnly = false) - { - if (!megFileOnly) - { - // This is a custom rule - if (_fileSystem.Path.IsPathFullyQualified(filePath)) - return _fileSystem.File.Exists(filePath); - - foreach (var modPath in _modPaths) - { - var modFilePath = _fileSystem.Path.Combine(modPath, filePath); - if (_fileSystem.File.Exists(modFilePath)) - return true; - } - - var normalFilePath = _fileSystem.Path.Combine(_gameDirectory, filePath); - if (_fileSystem.File.Exists(normalFilePath)) - return true; - } - - if (_masterMegArchive is not null) - { - var normalizedPath = _megPathNormalizer.Normalize(filePath); - var crc = _crc32HashingService.GetCrc32(normalizedPath, PGConstants.PGCrc32Encoding); - - var entry = _masterMegArchive.FirstEntryWithCrc(crc); - if (entry is not null) - return true; - } - - if (!megFileOnly) - { - - foreach (var fallbackPath in _fallbackPaths) - { - var fallbackFilePath = _fileSystem.Path.Combine(fallbackPath, filePath); - if (_fileSystem.File.Exists(fallbackFilePath)) - return true; - } - } - - return false; - } - - public Stream? TryOpenFile(string filePath, bool megFileOnly = false) - { - if (!megFileOnly) - { - // This is a custom rule - if (_fileSystem.Path.IsPathFullyQualified(filePath)) - return !_fileSystem.File.Exists(filePath) ? null : OpenFileRead(filePath); - - foreach (var modPath in _modPaths) - { - var modFilePath = _fileSystem.Path.Combine(modPath, filePath); - if (_fileSystem.File.Exists(modFilePath)) - return OpenFileRead(modFilePath); - } - - - var normalFilePath = _fileSystem.Path.Combine(_gameDirectory, filePath); - if (_fileSystem.File.Exists(normalFilePath)) - return OpenFileRead(normalFilePath); - } - - if (_masterMegArchive is not null) - { - var normalizedPath = _megPathNormalizer.Normalize(filePath); - var crc = _crc32HashingService.GetCrc32(normalizedPath, PGConstants.PGCrc32Encoding); - - var entry = _masterMegArchive.FirstEntryWithCrc(crc); - if (entry is not null) - return _megExtractor.GetFileData(entry.Location); - } - - if (!megFileOnly) - { - foreach (var fallbackPath in _fallbackPaths) - { - var fallbackFilePath = _fileSystem.Path.Combine(fallbackPath, filePath); - if (_fileSystem.File.Exists(fallbackFilePath)) - return OpenFileRead(fallbackFilePath); - } - } - return null; - } - - private FileSystemStream OpenFileRead(string filePath) - { - if (!AllowOpenFile(filePath)) - throw new UnauthorizedAccessException("The data is not part of the Games!"); - return _fileSystem.FileStream.New(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); - } - - private bool AllowOpenFile(string filePath) - { - foreach (var modPath in _modPaths) - { - if (_fileSystem.Path.IsChildOf(modPath, filePath)) - return true; - } - - if (_fileSystem.Path.IsChildOf(_gameDirectory, filePath)) - return true; - - foreach (var fallbackPath in _fallbackPaths) - { - if (_fileSystem.Path.IsChildOf(fallbackPath, filePath)) - return true; - } - - return false; - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/IGameRepositoryFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/IGameRepositoryFactory.cs deleted file mode 100644 index ed6600f..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/IGameRepositoryFactory.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace PG.StarWarsGame.Engine.FileSystem; - -public interface IGameRepositoryFactory -{ - IGameRepository Create(GameEngineType engineType, GameLocations gameLocations); -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/GameLocations.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameLocations.cs similarity index 94% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/GameLocations.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/GameLocations.cs index c2939bf..18539a7 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/GameLocations.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/GameLocations.cs @@ -3,7 +3,7 @@ using System.Linq; using AnakinRaW.CommonUtilities; -namespace PG.StarWarsGame.Engine.FileSystem; +namespace PG.StarWarsGame.Engine; public sealed class GameLocations { @@ -30,7 +30,7 @@ public GameLocations(IList modPaths, string gamePath, string fallbackGam public GameLocations(IList modPaths, string gamePath, IList fallbackPaths) { - if (modPaths == null) + if (modPaths == null) throw new ArgumentNullException(nameof(modPaths)); if (fallbackPaths == null) throw new ArgumentNullException(nameof(fallbackPaths)); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs index f1ff96c..36f02b4 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; -using PG.StarWarsGame.Engine.FileSystem; using PG.StarWarsGame.Engine.Language; +using PG.StarWarsGame.Engine.Pipeline; +using PG.StarWarsGame.Engine.Repositories; using PG.StarWarsGame.Engine.Xml; namespace PG.StarWarsGame.Engine; @@ -12,5 +13,6 @@ public static void ContributeServices(IServiceCollection serviceCollection) serviceCollection.AddSingleton(sp => new GameRepositoryFactory(sp)); serviceCollection.AddSingleton(sp => new GameLanguageManager(sp)); serviceCollection.AddSingleton(sp => new PetroglyphXmlFileParserFactory(sp)); + serviceCollection.AddSingleton(sp => new GameDatabaseService(sp)); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/CreateDatabaseStep.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/CreateDatabaseStep.cs index 9905b93..bb94640 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/CreateDatabaseStep.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/CreateDatabaseStep.cs @@ -2,7 +2,7 @@ using System.Threading; using AnakinRaW.CommonUtilities.SimplePipeline.Steps; using Microsoft.Extensions.Logging; -using PG.StarWarsGame.Engine.FileSystem; +using PG.StarWarsGame.Engine.Repositories; namespace PG.StarWarsGame.Engine.Pipeline; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/CreateGameDatabasePipeline.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/GameDatabaseCreationPipeline.cs similarity index 84% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/CreateGameDatabasePipeline.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/GameDatabaseCreationPipeline.cs index d1973b7..4928e33 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/CreateGameDatabasePipeline.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/GameDatabaseCreationPipeline.cs @@ -5,26 +5,12 @@ using System.Threading.Tasks; using AnakinRaW.CommonUtilities.SimplePipeline; using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine.Database; using PG.StarWarsGame.Engine.DataTypes; -using PG.StarWarsGame.Engine.FileSystem; +using PG.StarWarsGame.Engine.Repositories; namespace PG.StarWarsGame.Engine.Pipeline; -public interface IGameDatabaseService -{ - Task CreateDatabaseAsync(GameEngineType targetEngineType, GameLocations locations, CancellationToken cancellationToken = default); -} - -internal class GameDatabaseService(IServiceProvider serviceProvider) : IGameDatabaseService -{ - public async Task CreateDatabaseAsync(GameEngineType targetEngineType, GameLocations locations, CancellationToken cancellationToken = default) - { - var pipeline = new GameDatabaseCreationPipeline(repository, serviceProvider, cancellationToken); - await pipeline.RunAsync(cancellationToken); - return pipeline.GameDatabase; - } -} - internal class GameDatabaseCreationPipeline(GameRepository repository, IServiceProvider serviceProvider) : ParallelPipeline(serviceProvider) { private ParseSingletonXmlStep _parseGameConstants = null!; @@ -92,8 +78,11 @@ protected override async Task RunCoreAsync(CancellationToken token) { await base.RunCoreAsync(token); + repository.Seal(); + GameDatabase = new GameDatabase { + GameRepository = repository, GameConstants = _parseGameConstants.Database, GameObjects = _parseGameObjects.Database }; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/GameDatabaseService.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/GameDatabaseService.cs new file mode 100644 index 0000000..6465a45 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/GameDatabaseService.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Engine.Database; +using PG.StarWarsGame.Engine.Repositories; + +namespace PG.StarWarsGame.Engine.Pipeline; + +internal class GameDatabaseService(IServiceProvider serviceProvider) : IGameDatabaseService +{ + public async Task CreateDatabaseAsync(GameEngineType targetEngineType, GameLocations locations, CancellationToken cancellationToken = default) + { + var repoFactory = serviceProvider.GetRequiredService(); + var repository = repoFactory.Create(targetEngineType, locations); + + var pipeline = new GameDatabaseCreationPipeline(repository, serviceProvider); + await pipeline.RunAsync(cancellationToken); + return pipeline.GameDatabase; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/IGameDatabaseService.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/IGameDatabaseService.cs new file mode 100644 index 0000000..9492017 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/IGameDatabaseService.cs @@ -0,0 +1,10 @@ +using System.Threading; +using System.Threading.Tasks; +using PG.StarWarsGame.Engine.Database; + +namespace PG.StarWarsGame.Engine.Pipeline; + +public interface IGameDatabaseService +{ + Task CreateDatabaseAsync(GameEngineType targetEngineType, GameLocations locations, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/ParseXmlDatabaseFromContainerStep.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/ParseXmlDatabaseFromContainerStep.cs index ee22402..00235e7 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/ParseXmlDatabaseFromContainerStep.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/ParseXmlDatabaseFromContainerStep.cs @@ -4,7 +4,7 @@ using System.Linq; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using PG.StarWarsGame.Engine.FileSystem; +using PG.StarWarsGame.Engine.Repositories; using PG.StarWarsGame.Engine.Xml; using PG.StarWarsGame.Files.XML; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/ParseXmlDatabaseStep.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/ParseXmlDatabaseStep.cs index 516d33c..2fb1c9d 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/ParseXmlDatabaseStep.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/ParseXmlDatabaseStep.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using PG.StarWarsGame.Engine.FileSystem; +using PG.StarWarsGame.Engine.Repositories; using PG.StarWarsGame.Engine.Xml; namespace PG.StarWarsGame.Engine.Pipeline; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/AudioRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/AudioRepository.cs new file mode 100644 index 0000000..134a732 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/AudioRepository.cs @@ -0,0 +1,6 @@ +namespace PG.StarWarsGame.Engine.Repositories; + +public class AudioRepository +{ + +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/EffectsRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/EffectsRepository.cs similarity index 98% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/EffectsRepository.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/EffectsRepository.cs index ea4f080..933c373 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/EffectsRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/EffectsRepository.cs @@ -4,7 +4,7 @@ using System.Linq; using Microsoft.Extensions.DependencyInjection; -namespace PG.StarWarsGame.Engine.FileSystem; +namespace PG.StarWarsGame.Engine.Repositories; public class EffectsRepository(IGameRepository baseRepository, IServiceProvider serviceProvider) : IRepository { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/FocGameRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/FocGameRepository.cs new file mode 100644 index 0000000..3b269f0 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/FocGameRepository.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using PG.StarWarsGame.Files.MEG.Files; + +namespace PG.StarWarsGame.Engine.Repositories; + +// EaW file lookup works slightly different! +internal class FocGameRepository : GameRepository +{ + public override GameEngineType EngineType => GameEngineType.Foc; + + public FocGameRepository(GameLocations gameLocations, IServiceProvider serviceProvider) : base(gameLocations, serviceProvider) + { + if (gameLocations == null) + throw new ArgumentNullException(nameof(gameLocations)); + + var megsToConsider = new List(); + + // We assume that the first fallback path (if present at all) is always Empire at War. + var firstFallback = FallbackPaths.FirstOrDefault(); + if (firstFallback is not null) + { + var eawMegs = LoadMegArchivesFromXml(firstFallback); + var eawPatch = LoadMegArchive(FileSystem.Path.Combine(firstFallback, "Data\\Patch.meg")); + var eawPatch2 = LoadMegArchive(FileSystem.Path.Combine(firstFallback, "Data\\Patch2.meg")); + var eaw64Patch = LoadMegArchive(FileSystem.Path.Combine(firstFallback, "Data\\64Patch.meg")); + + megsToConsider.AddRange(eawMegs); + if (eawPatch is not null) + megsToConsider.Add(eawPatch); + if (eawPatch2 is not null) + megsToConsider.Add(eawPatch2); + if (eaw64Patch is not null) + megsToConsider.Add(eaw64Patch); + } + + var focOrModMegs = LoadMegArchivesFromXml("."); + var focPatch = LoadMegArchive("Data\\Patch.meg"); + var focPatch2 = LoadMegArchive("Data\\Patch2.meg"); + var foc64Patch = LoadMegArchive("Data\\64Patch.meg"); + + megsToConsider.AddRange(focOrModMegs); + if (focPatch is not null) + megsToConsider.Add(focPatch); + if (focPatch2 is not null) + megsToConsider.Add(focPatch2); + if (foc64Patch is not null) + megsToConsider.Add(foc64Patch); + + AddMegFiles(megsToConsider); + } + + + protected override T? RepositoryFileLookup(string filePath, Func> pathAction, Func> megAction, bool megFileOnly, T? defaultValue = default) where T: default + { + if (!megFileOnly) + { + foreach (var modPath in ModPaths) + { + var modFilePath = FileSystem.Path.Combine(modPath, filePath); + + var result = pathAction(modFilePath); + if (result.ShallReturn) + return result.Result; + } + + { + var normalFilePath = FileSystem.Path.Combine(GameDirectory, filePath); + var result = pathAction(normalFilePath); + if (result.ShallReturn) + return result.Result; + } + + } + + if (MasterMegArchive is not null) + { + var result = megAction(filePath); + if (result.ShallReturn) + return result.Result; + } + + if (!megFileOnly) + { + foreach (var fallbackPath in FallbackPaths) + { + var fallbackFilePath = FileSystem.Path.Combine(fallbackPath, filePath); + + var result = pathAction(fallbackFilePath); + if (result.ShallReturn) + return result.Result; + } + } + + return defaultValue; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs new file mode 100644 index 0000000..32dcedd --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs @@ -0,0 +1,302 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using System.Runtime.InteropServices; +using AnakinRaW.CommonUtilities.FileSystem; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileSystemGlobbing; +using Microsoft.Extensions.Logging; +using PG.Commons.Hashing; +using PG.Commons.Services; +using PG.StarWarsGame.Engine.Utilities; +using PG.StarWarsGame.Engine.Xml; +using PG.StarWarsGame.Files.MEG.Data.Archives; +using PG.StarWarsGame.Files.MEG.Data.Entries; +using PG.StarWarsGame.Files.MEG.Data.EntryLocations; +using PG.StarWarsGame.Files.MEG.Files; +using PG.StarWarsGame.Files.MEG.Services; +using PG.StarWarsGame.Files.MEG.Services.Builder.Normalization; +using PG.StarWarsGame.Files.XML; + +namespace PG.StarWarsGame.Engine.Repositories; + +internal abstract class GameRepository : ServiceBase, IGameRepository +{ + private readonly IMegFileService _megFileService; + private readonly IMegFileExtractor _megExtractor; + private readonly PetroglyphDataEntryPathNormalizer _megPathNormalizer; + private readonly ICrc32HashingService _crc32HashingService; + private readonly IVirtualMegArchiveBuilder _virtualMegBuilder; + + protected readonly string GameDirectory; + + protected readonly IList ModPaths = new List(); + protected readonly IList FallbackPaths = new List(); + + private bool _sealed; + + public abstract GameEngineType EngineType { get; } + + public IRepository EffectsRepository { get; } + + protected IVirtualMegArchive? MasterMegArchive { get; private set; } + + protected GameRepository(GameLocations gameLocations, IServiceProvider serviceProvider) : base(serviceProvider) + { + if (gameLocations == null) + throw new ArgumentNullException(nameof(gameLocations)); + + _megExtractor = serviceProvider.GetRequiredService(); + _megFileService = serviceProvider.GetRequiredService(); + _virtualMegBuilder = serviceProvider.GetRequiredService(); + _crc32HashingService = serviceProvider.GetRequiredService(); + _megPathNormalizer = new PetroglyphDataEntryPathNormalizer(serviceProvider); + + foreach (var mod in gameLocations.ModPaths) + { + if (string.IsNullOrEmpty(mod)) + throw new InvalidOperationException("Mods with empty paths are not valid."); + + ModPaths.Add(FileSystem.Path.GetFullPath(mod)); + } + + GameDirectory = FileSystem.Path.GetFullPath(gameLocations.GamePath); + + + foreach (var fallbackPath in gameLocations.FallbackPaths) + { + if (string.IsNullOrEmpty(fallbackPath)) + { + Logger.LogTrace("Skipping null or empty fallback path."); + continue; + } + FallbackPaths.Add(FileSystem.Path.GetFullPath(fallbackPath)); + } + + EffectsRepository = new EffectsRepository(this, serviceProvider); + } + + + public void AddMegFiles(IList megFiles) + { + ThrowIfSealed(); + if (MasterMegArchive is null) + MasterMegArchive = _virtualMegBuilder.BuildFrom(megFiles, true); + else + { + var newLocations = new List(); + foreach (var megFile in megFiles) + newLocations.AddRange(megFile.Archive.Select(entry => + new MegDataEntryReference(new MegDataEntryLocationReference(megFile, entry)))); + MasterMegArchive = _virtualMegBuilder.BuildFrom(MasterMegArchive.ToList().Concat(newLocations), true); + } + } + + public void AddMegFile(string megFile) + { + var megArchive = LoadMegArchive(megFile); + if (megArchive is null) + { + Logger.LogWarning($"Unable to find MEG file at '{megFile}'"); + return; + } + + AddMegFiles([megArchive]); + } + + public bool FileExists(string filePath, bool megFileOnly = false) + { + return RepositoryFileLookup(filePath, fp => + { + if (FileSystem.File.Exists(fp)) + return new ActionResult(true, true); + return ActionResult.DoNotReturn; + }, + fp => + { + var entry = FindFileInMasterMeg(fp); + if (entry is not null) + return new ActionResult(true, true); + return ActionResult.DoNotReturn; + }, megFileOnly); + } + + public Stream? TryOpenFile(string filePath, bool megFileOnly = false) + { + return RepositoryFileLookup(filePath, fp => + { + if (FileSystem.File.Exists(fp)) + return new ActionResult(true, OpenFileRead(fp)); + return ActionResult.DoNotReturn; + }, + fp => + { + var entry = FindFileInMasterMeg(fp); + if (entry is not null) + return new ActionResult(true, _megExtractor.GetFileData(entry.Location)); + return ActionResult.DoNotReturn; + }, megFileOnly); + } + + + public Stream OpenFile(string filePath, bool megFileOnly = false) + { + var stream = TryOpenFile(filePath, megFileOnly); + if (stream is null) + throw new FileNotFoundException($"Unable to find game data: {filePath}"); + return stream; + } + + public bool FileExists(string filePath, string[] extensions, bool megFileOnly = false) + { + foreach (var extension in extensions) + { + var newPath = FileSystem.Path.ChangeExtension(filePath, extension); + if (FileExists(newPath, megFileOnly)) + return true; + } + return false; + } + + public IEnumerable FindFiles(string searchPattern, bool megFileOnly = false) + { + var files = new HashSet(); + + var matcher = new Matcher(); + matcher.AddInclude(searchPattern); + + RepositoryFileLookup(searchPattern, + pattern => + { + var path = pattern.AsSpan().TrimEnd(searchPattern.AsSpan()); + + var matcherResult = matcher.Execute(FileSystem, path.ToString()); + + foreach (var matchedFile in matcherResult.Files) + { + var normalizedFile = _megPathNormalizer.Normalize(matchedFile.Path); + files.Add(normalizedFile); + } + + return ActionResult.DoNotReturn; + }, + _ => + { + var foundFiles = MasterMegArchive!.FindAllEntries(searchPattern, true); + foreach (var x in foundFiles) + files.Add(x.FilePath); + + return ActionResult.DoNotReturn; + }, megFileOnly); + + return files; + } + + protected IList LoadMegArchivesFromXml(string lookupPath) + { + var megFilesXmlPath = FileSystem.Path.Combine(lookupPath, "Data\\MegaFiles.xml"); + + var fileParserFactory = Services.GetRequiredService(); + + using var xmlStream = TryOpenFile(megFilesXmlPath); + + if (xmlStream is null) + { + Logger.LogWarning($"Unable to find MegaFiles.xml at '{lookupPath}'"); + return Array.Empty(); + } + + var parser = fileParserFactory.GetFileParser(); + var megaFilesXml = parser.ParseFile(xmlStream); + + + var megs = new List(megaFilesXml.Files.Count); + + foreach (var file in megaFilesXml.Files.Select(x => x.Trim())) + { + var megPath = FileSystem.Path.Combine(lookupPath, file); + var megFile = LoadMegArchive(megPath); + if (megFile is not null) + megs.Add(megFile); + } + + return megs; + } + + internal void Seal() + { + _sealed = true; + } + + protected abstract T? RepositoryFileLookup(string filePath, Func> pathAction, + Func> megAction, bool megFileOnly, T? defaultValue = default); + + protected IMegFile? LoadMegArchive(string megPath) + { + using var megFileStream = TryOpenFile(megPath); + if (megFileStream is not FileSystemStream fileSystemStream) + return null; + + var megFile = _megFileService.Load(fileSystemStream); + + if (megFile.FileInformation.FileVersion != MegFileVersion.V1) + throw new InvalidOperationException("MEG data version must be V1."); + + return megFile; + } + + protected MegDataEntryReference? FindFileInMasterMeg(string filePath) + { + var normalizedPath = _megPathNormalizer.Normalize(filePath); + var crc = _crc32HashingService.GetCrc32(normalizedPath, PGConstants.PGCrc32Encoding); + + return MasterMegArchive?.FirstEntryWithCrc(crc); + } + + protected FileSystemStream OpenFileRead(string filePath) + { + if (!AllowOpenFile(filePath)) + throw new UnauthorizedAccessException("The data is not part of the Games!"); + return FileSystem.FileStream.New(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + } + + private bool AllowOpenFile(string filePath) + { + foreach (var modPath in ModPaths) + { + if (FileSystem.Path.IsChildOf(modPath, filePath)) + return true; + } + + if (FileSystem.Path.IsChildOf(GameDirectory, filePath)) + return true; + + foreach (var fallbackPath in FallbackPaths) + { + if (FileSystem.Path.IsChildOf(fallbackPath, filePath)) + return true; + } + + return false; + } + + private void ThrowIfSealed() + { + if (_sealed) + throw new InvalidOperationException("The object is sealed for modifications"); + } + + protected readonly struct ActionResult(bool shallReturn, T? result) + { + public T? Result { get; } = result; + + public bool ShallReturn { get; } = shallReturn; + + public static ActionResult DoNotReturn => default; + } + + [StructLayout(LayoutKind.Explicit)] + private readonly struct EmptyStruct; +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/GameRepositoryFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepositoryFactory.cs similarity index 71% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/GameRepositoryFactory.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepositoryFactory.cs index 02e24d2..8c3af4d 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/GameRepositoryFactory.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepositoryFactory.cs @@ -1,10 +1,10 @@ using System; -namespace PG.StarWarsGame.Engine.FileSystem; +namespace PG.StarWarsGame.Engine.Repositories; internal sealed class GameRepositoryFactory(IServiceProvider serviceProvider) : IGameRepositoryFactory { - public IGameRepository Create(GameEngineType engineType, GameLocations gameLocations) + public GameRepository Create(GameEngineType engineType, GameLocations gameLocations) { if (engineType == GameEngineType.Eaw) throw new NotImplementedException("Empire at War is currently not supported."); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/IGameRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepository.cs similarity index 57% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/IGameRepository.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepository.cs index 8965189..689b1e9 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/IGameRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepository.cs @@ -1,4 +1,6 @@ -namespace PG.StarWarsGame.Engine.FileSystem; +using System.Collections.Generic; + +namespace PG.StarWarsGame.Engine.Repositories; public interface IGameRepository : IRepository { @@ -7,4 +9,6 @@ public interface IGameRepository : IRepository IRepository EffectsRepository { get; } bool FileExists(string filePath, string[] extensions, bool megFileOnly = false); + + IEnumerable FindFiles(string searchPattern, bool megFileOnly = false); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepositoryFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepositoryFactory.cs new file mode 100644 index 0000000..c1461b0 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepositoryFactory.cs @@ -0,0 +1,6 @@ +namespace PG.StarWarsGame.Engine.Repositories; + +internal interface IGameRepositoryFactory +{ + GameRepository Create(GameEngineType engineType, GameLocations gameLocations); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/IRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IRepository.cs similarity index 84% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/IRepository.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IRepository.cs index 972c91f..fe5764f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/FileSystem/IRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IRepository.cs @@ -1,6 +1,6 @@ using System.IO; -namespace PG.StarWarsGame.Engine.FileSystem; +namespace PG.StarWarsGame.Engine.Repositories; public interface IRepository { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/DirectoryInfoGlobbingWrapper.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/DirectoryInfoGlobbingWrapper.cs new file mode 100644 index 0000000..2f2114a --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/DirectoryInfoGlobbingWrapper.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; + +namespace PG.StarWarsGame.Engine.Utilities; + +// Taken from https://github.com/vipentti/Vipentti.IO.Abstractions.FileSystemGlobbing + +/// +/// Wraps to be used with +/// +public sealed class DirectoryInfoGlobbingWrapper : Microsoft.Extensions.FileSystemGlobbing.Abstractions.DirectoryInfoBase +{ + private readonly IFileSystem _fileSystem; + private readonly IDirectoryInfo _directoryInfo; + private readonly bool _isParentPath; + + /// + public override string Name => _isParentPath ? ".." : _directoryInfo.Name; + + /// + public override string FullName => _directoryInfo.FullName; + + /// + public override Microsoft.Extensions.FileSystemGlobbing.Abstractions.DirectoryInfoBase? ParentDirectory => + _directoryInfo.Parent is null + ? null + : new DirectoryInfoGlobbingWrapper(_fileSystem, _directoryInfo.Parent); + + /// + /// Construct a new instance of + /// + /// The filesystem + /// The directory + public DirectoryInfoGlobbingWrapper(IFileSystem fileSystem, IDirectoryInfo directoryInfo) + : this(fileSystem, directoryInfo, isParentPath: false) + { + } + + private DirectoryInfoGlobbingWrapper(IFileSystem fileSystem, IDirectoryInfo directoryInfo, bool isParentPath) + { + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _directoryInfo = directoryInfo ?? throw new ArgumentNullException(nameof(directoryInfo)); + _isParentPath = isParentPath; + } + + /// + public override IEnumerable EnumerateFileSystemInfos() + { + if (_directoryInfo.Exists) + { + IEnumerable fileSystemInfos; + try + { + fileSystemInfos = _directoryInfo.EnumerateFileSystemInfos("*", SearchOption.TopDirectoryOnly); + } + catch (DirectoryNotFoundException) + { + yield break; + } + + foreach (var fileSystemInfo in fileSystemInfos) + { + yield return fileSystemInfo switch + { + IDirectoryInfo directoryInfo => new DirectoryInfoGlobbingWrapper(_fileSystem, directoryInfo), + IFileInfo fileInfo => new FileInfoGlobbingWrapper(_fileSystem, fileInfo), + _ => throw new NotSupportedException() + }; + } + } + } + + /// + public override Microsoft.Extensions.FileSystemGlobbing.Abstractions.DirectoryInfoBase? GetDirectory(string path) + { + var isParentPath = string.Equals(path, "..", StringComparison.Ordinal); + + if (isParentPath) + { + return new DirectoryInfoGlobbingWrapper(_fileSystem, + _fileSystem.DirectoryInfo.New(Path.Combine(_directoryInfo.FullName, path)), isParentPath); + } + + var dirs = _directoryInfo.GetDirectories(path); + + return dirs switch + { + { Length: 1 } + => new DirectoryInfoGlobbingWrapper(_fileSystem, dirs[0], isParentPath), + { Length: 0 } => null, + // This shouldn't happen. The parameter name isn't supposed to contain wild card. + _ + => throw new InvalidOperationException( + $"More than one sub directories are found under {_directoryInfo.FullName} with name {path}." + ), + }; + } + + /// + public override Microsoft.Extensions.FileSystemGlobbing.Abstractions.FileInfoBase GetFile(string path) + { + return new FileInfoGlobbingWrapper(_fileSystem, _fileSystem.FileInfo.New(Path.Combine(FullName, path))); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/FileInfoGlobbingWrapper.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/FileInfoGlobbingWrapper.cs new file mode 100644 index 0000000..744c377 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/FileInfoGlobbingWrapper.cs @@ -0,0 +1,35 @@ +using System.IO.Abstractions; + +namespace PG.StarWarsGame.Engine.Utilities; + +// Taken from https://github.com/vipentti/Vipentti.IO.Abstractions.FileSystemGlobbing + +internal sealed class FileInfoGlobbingWrapper + : Microsoft.Extensions.FileSystemGlobbing.Abstractions.FileInfoBase +{ + private readonly IFileSystem _fileSystem; + private readonly IFileInfo _fileInfo; + + /// + public override string Name => _fileInfo.Name; + + /// + public override string FullName => _fileInfo.FullName; + + /// + public override Microsoft.Extensions.FileSystemGlobbing.Abstractions.DirectoryInfoBase? ParentDirectory => + _fileInfo.Directory is null + ? null + : new DirectoryInfoGlobbingWrapper(_fileSystem, _fileInfo.Directory); + + /// + /// Initialize a new instance + /// + /// The filesystem + /// The file + public FileInfoGlobbingWrapper(IFileSystem fileSystem, IFileInfo fileInfo) + { + _fileSystem = fileSystem; + _fileInfo = fileInfo; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/MatcherExtensions.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/MatcherExtensions.cs new file mode 100644 index 0000000..e2bde1e --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/MatcherExtensions.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Linq; +using Microsoft.Extensions.FileSystemGlobbing; + +namespace PG.StarWarsGame.Engine.Utilities; + +// Taken from https://github.com/vipentti/Vipentti.IO.Abstractions.FileSystemGlobbing + +/// +/// Provides extensions for to support +/// +public static class MatcherExtensions +{ + /// + /// Searches the directory specified for all files matching patterns added to this instance of + /// + /// The matcher + /// The filesystem + /// The root directory for the search + /// Always returns instance of , even if no files were matched + public static PatternMatchingResult Execute(this Matcher matcher, IFileSystem fileSystem, string directoryPath) + { + if (matcher == null) + throw new ArgumentNullException(nameof(matcher)); + if (fileSystem == null) + throw new ArgumentNullException(nameof(fileSystem)); + return Execute(matcher, fileSystem, fileSystem.DirectoryInfo.New(directoryPath)); + } + + /// + public static PatternMatchingResult Execute(this Matcher matcher, IFileSystem fileSystem, IDirectoryInfo directoryInfo) + { + if (matcher == null) + throw new ArgumentNullException(nameof(matcher)); + if (fileSystem == null) + throw new ArgumentNullException(nameof(fileSystem)); + if (directoryInfo == null) + throw new ArgumentNullException(nameof(directoryInfo)); + return matcher.Execute(new DirectoryInfoGlobbingWrapper(fileSystem, directoryInfo)); + } + + /// + /// Searches the directory specified for all files matching patterns added to this instance of + /// + /// The matcher + /// The filesystem + /// The root directory for the search + /// Absolute file paths of all files matched. Empty enumerable if no files matched given patterns. + public static IEnumerable GetResultsInFullPath(this Matcher matcher, IFileSystem fileSystem, string directoryPath) + { + if (matcher == null) + throw new ArgumentNullException(nameof(matcher)); + if (fileSystem == null) + throw new ArgumentNullException(nameof(fileSystem)); + return GetResultsInFullPath(matcher, fileSystem, fileSystem.DirectoryInfo.New(directoryPath)); + } + + /// + public static IEnumerable GetResultsInFullPath(this Matcher matcher, IFileSystem fileSystem, IDirectoryInfo directoryInfo) + { + var matches = Execute(matcher, fileSystem, directoryInfo); + + if (!matches.HasMatches) + return Enumerable.Empty(); + + var fsPath = fileSystem.Path; + var directoryFullName = directoryInfo.FullName; + + return matches.Files.Select(GetFullPath); + + string GetFullPath(FilePatternMatch match) + { + return fsPath.GetFullPath(fsPath.Combine(directoryFullName, match.Path)); + } + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/StringUtilities.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/StringUtilities.cs deleted file mode 100644 index 6511f68..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/StringUtilities.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; - -namespace PG.StarWarsGame.Engine.Utilities; - -internal static class StringUtilities -{ - internal static unsafe string Concat(ReadOnlySpan str0, ReadOnlySpan str1, ReadOnlySpan str2) - { - var result = new string('\0', checked(str0.Length + str1.Length + str2.Length)); - fixed (char* resultPtr = result) - { - var resultSpan = new Span(resultPtr, result.Length); - - str0.CopyTo(resultSpan); - resultSpan = resultSpan.Slice(str0.Length); - - str1.CopyTo(resultSpan); - resultSpan = resultSpan.Slice(str1.Length); - - str2.CopyTo(resultSpan); - } - return result; - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/ValueStringBuilder.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/ValueStringBuilder.cs index 6145d3c..e210518 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/ValueStringBuilder.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Utilities/ValueStringBuilder.cs @@ -5,7 +5,7 @@ namespace PG.StarWarsGame.Engine.Utilities; // From https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/System/Text/ValueStringBuilder.cs internal ref struct ValueStringBuilder(Span initialBuffer) { - private Span _chars = initialBuffer; + private readonly Span _chars = initialBuffer; private int _pos = 0; public override string ToString() From b89074fc2d83a61985660137178d9c048be720ee Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sat, 8 Jun 2024 12:28:00 +0200 Subject: [PATCH 03/25] start parsing sfxevents and duplicate checker --- PetroglyphTools | 2 +- src/ModVerify/Steps/DuplicateFinderStep.cs | 49 +++++ ...sStep.cs => VerifyReferencedModelsStep.cs} | 8 +- src/ModVerify/VerificationProvider.cs | 1 + src/ModVerify/VerifyGamePipeline.cs | 1 - .../DataTypes/GameObject.cs | 61 +++--- .../DataTypes/SfxEvent.cs | 20 ++ .../DataTypes/XmlObject.cs | 19 ++ .../Database/GameDatabase.cs | 7 +- .../GameDatabaseService.cs | 9 +- .../Database/IGameDatabase.cs | 7 +- .../IGameDatabaseService.cs | 3 +- .../Database/IXmlDatabase.cs | 15 ++ .../Initialization}/CreateDatabaseStep.cs | 4 +- .../GameDatabaseCreationPipeline.cs | 80 +++++--- .../ParseXmlDatabaseFromContainerStep.cs | 26 +-- .../Initialization}/ParseXmlDatabaseStep.cs | 4 +- .../Database/XmlDatabase.cs | 27 +++ .../PetroglyphEngineServiceContribution.cs | 2 +- .../Repositories/GameRepository.cs | 1 + .../Xml/Parsers/Data/GameConstantsParser.cs | 21 ++ .../Parsers/{ => Data}/GameObjectParser.cs | 20 +- .../Xml/Parsers/Data/SfxEventParser.cs | 73 +++++++ .../Parsers/File/GameObjectFileFileParser.cs | 29 +++ .../Xml/Parsers/File/SfxEventFileParser.cs | 29 +++ .../Xml/Parsers/GameConstantsFileParser.cs | 14 -- .../Xml/Parsers/GameObjectFileFileParser.cs | 19 -- .../Xml/PetroglyphXmlParserFactory.cs | 10 +- .../Parsers/IPetroglyphXmlElementParser.cs | 7 +- .../Parsers/IPetroglyphXmlFileParser.cs | 5 +- .../Parsers/PetroglyphXmlElementParser.cs | 13 +- .../Parsers/PetroglyphXmlFileParser.cs | 30 ++- .../CommaSeparatedStringKeyValueListParser.cs | 6 + .../Primitives/PetroglyphXmlStringParser.cs | 6 + .../Primitives/XmlFileContainerParser.cs | 14 +- .../ValueListDictionary.cs | 189 +++++++++++++++--- 36 files changed, 647 insertions(+), 184 deletions(-) create mode 100644 src/ModVerify/Steps/DuplicateFinderStep.cs rename src/ModVerify/Steps/{VerifyModelsStep.cs => VerifyReferencedModelsStep.cs} (96%) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/SfxEvent.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/XmlObject.cs rename src/PetroglyphTools/PG.StarWarsGame.Engine/{Pipeline => Database}/GameDatabaseService.cs (68%) rename src/PetroglyphTools/PG.StarWarsGame.Engine/{Pipeline => Database}/IGameDatabaseService.cs (75%) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IXmlDatabase.cs rename src/PetroglyphTools/PG.StarWarsGame.Engine/{Pipeline => Database/Initialization}/CreateDatabaseStep.cs (78%) rename src/PetroglyphTools/PG.StarWarsGame.Engine/{Pipeline => Database/Initialization}/GameDatabaseCreationPipeline.cs (58%) rename src/PetroglyphTools/PG.StarWarsGame.Engine/{Pipeline => Database/Initialization}/ParseXmlDatabaseFromContainerStep.cs (68%) rename src/PetroglyphTools/PG.StarWarsGame.Engine/{Pipeline => Database/Initialization}/ParseXmlDatabaseStep.cs (92%) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Database/XmlDatabase.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameConstantsParser.cs rename src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/{ => Data}/GameObjectParser.cs (90%) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileFileParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/GameConstantsFileParser.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/GameObjectFileFileParser.cs diff --git a/PetroglyphTools b/PetroglyphTools index 16461a0..adf607f 160000 --- a/PetroglyphTools +++ b/PetroglyphTools @@ -1 +1 @@ -Subproject commit 16461a02bb8f57a584cc6e9fff0f7a4d1c3901b5 +Subproject commit adf607fbe479c315ea142564f08b637e839914b1 diff --git a/src/ModVerify/Steps/DuplicateFinderStep.cs b/src/ModVerify/Steps/DuplicateFinderStep.cs new file mode 100644 index 0000000..f74721e --- /dev/null +++ b/src/ModVerify/Steps/DuplicateFinderStep.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq; +using System.Threading; +using AnakinRaW.CommonUtilities.Collections; +using PG.StarWarsGame.Engine.Database; +using PG.StarWarsGame.Engine.DataTypes; + +namespace AET.ModVerify.Steps; + +public sealed class DuplicateFinderStep( + IGameDatabase gameDatabase, + VerificationSettings settings, + IServiceProvider serviceProvider) + : GameVerificationStep(gameDatabase, settings, serviceProvider) +{ + public const string DuplicateFound = "DUP00"; + + protected override string LogFileName => "Duplicates"; + + public override string Name => "Duplicate Definitions"; + + protected override void RunVerification(CancellationToken token) + { + CheckDatabaseForDuplicates(Database.GameObjects, "GameObject"); + CheckDatabaseForDuplicates(Database.SfxEvents, "SFXEvent"); + } + + private void CheckDatabaseForDuplicates(IXmlDatabase database, string context) where T : XmlObject + { + foreach (var key in database.EntryKeys) + { + var entries = database.GetEntries(key); + if (entries.Count > 1) + AddError(VerificationError.Create(DuplicateFound, CreateDuplicateErrorMessage(context, entries))); + } + } + + private string CreateDuplicateErrorMessage(string context, ReadOnlyFrugalList entries) where T : XmlObject + { + var firstEntry = entries.First(); + + var message = $"{context} '{firstEntry.Name}' ({firstEntry.Crc32}) has duplicate definitions: "; + + foreach (var entry in entries) + message += $"['{entry.Name}' in {entry.Location.XmlFile}] "; + + return message.TrimEnd(); + } +} \ No newline at end of file diff --git a/src/ModVerify/Steps/VerifyModelsStep.cs b/src/ModVerify/Steps/VerifyReferencedModelsStep.cs similarity index 96% rename from src/ModVerify/Steps/VerifyModelsStep.cs rename to src/ModVerify/Steps/VerifyReferencedModelsStep.cs index b33b3d1..a43d244 100644 --- a/src/ModVerify/Steps/VerifyModelsStep.cs +++ b/src/ModVerify/Steps/VerifyReferencedModelsStep.cs @@ -16,7 +16,7 @@ namespace AET.ModVerify.Steps; -internal sealed class VerifyReferencedModelsStep( +public sealed class VerifyReferencedModelsStep( IGameDatabase database, VerificationSettings settings, IServiceProvider serviceProvider) @@ -34,13 +34,13 @@ internal sealed class VerifyReferencedModelsStep( private readonly IAloFileService _modelFileService = serviceProvider.GetRequiredService(); - protected override string LogFileName => "Model"; + protected override string LogFileName => "ModelRefs"; - public override string Name => "Model"; + public override string Name => "Referenced Models"; protected override void RunVerification(CancellationToken token) { - var aloQueue = new Queue(Database.GameObjects + var aloQueue = new Queue(Database.GameObjects.Entries .SelectMany(x => x.Models) .Concat(FocHardcodedConstants.HardcodedModels)); diff --git a/src/ModVerify/VerificationProvider.cs b/src/ModVerify/VerificationProvider.cs index 3a347c8..2a5c9d8 100644 --- a/src/ModVerify/VerificationProvider.cs +++ b/src/ModVerify/VerificationProvider.cs @@ -10,5 +10,6 @@ internal class VerificationProvider(IServiceProvider serviceProvider) : IVerific public IEnumerable GetAllDefaultVerifiers(IGameDatabase database, VerificationSettings settings) { yield return new VerifyReferencedModelsStep(database, settings, serviceProvider); + yield return new DuplicateFinderStep(database, settings, serviceProvider); } } \ No newline at end of file diff --git a/src/ModVerify/VerifyGamePipeline.cs b/src/ModVerify/VerifyGamePipeline.cs index cbf0bac..4d1d60a 100644 --- a/src/ModVerify/VerifyGamePipeline.cs +++ b/src/ModVerify/VerifyGamePipeline.cs @@ -10,7 +10,6 @@ using Microsoft.Extensions.Logging; using PG.StarWarsGame.Engine; using PG.StarWarsGame.Engine.Database; -using PG.StarWarsGame.Engine.Pipeline; namespace AET.ModVerify; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/GameObject.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/GameObject.cs index 27bea09..320649f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/GameObject.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/GameObject.cs @@ -1,24 +1,33 @@ using System; using System.Collections.Generic; using System.Linq; -using PG.Commons.DataTypes; using PG.Commons.Hashing; using PG.StarWarsGame.Files.XML; namespace PG.StarWarsGame.Engine.DataTypes; -public sealed class GameObject(string type, string name, Crc32 nameCrc, GameObjectType estimatedType, ValueListDictionary properties, XmlLocationInfo location) - : IHasCrc32 +public sealed class GameObject : XmlObject { - public string Type { get; } = type ?? throw new ArgumentNullException(nameof(type)); + private readonly IReadOnlyValueListDictionary _properties; - public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); + internal GameObject( + string type, + string name, + Crc32 nameCrc, + GameObjectType estimatedType, + IReadOnlyValueListDictionary properties, + XmlLocationInfo location) + : base(name, nameCrc, location) + { + _properties = properties; + Type = type ?? throw new ArgumentNullException(nameof(type)); + EstimatedType = estimatedType; + } - public Crc32 Crc32 { get; } = nameCrc; + public string Type { get; } - public GameObjectType EstimatedType { get; } = estimatedType; - public XmlLocationInfo Location { get; } = location; + public GameObjectType EstimatedType { get; } /// /// Gets all model files (including particles) the game object references. @@ -27,22 +36,24 @@ public ISet Models { get { - var models = properties.AggregateValues(new HashSet - { - "Galactic_Model_Name", - "Destroyed_Galactic_Model_Name", - "Land_Model_Name", - "Space_Model_Name", - "Model_Name", - "Tactical_Model_Name", - "Galactic_Fleet_Override_Model_Name", - "GUI_Model_Name", - "GUI_Model", - // This can either be a model or a unit reference - "Land_Model_Anim_Override_Name", - "xxxSpace_Model_Name", - "Damaged_Smoke_Asset_Name" - }, v => v.EndsWith(".alo", StringComparison.OrdinalIgnoreCase)); + var models = _properties.AggregateValues + (new HashSet + { + "Galactic_Model_Name", + "Destroyed_Galactic_Model_Name", + "Land_Model_Name", + "Space_Model_Name", + "Model_Name", + "Tactical_Model_Name", + "Galactic_Fleet_Override_Model_Name", + "GUI_Model_Name", + "GUI_Model", + // This can either be a model or a unit reference + "Land_Model_Anim_Override_Name", + "xxxSpace_Model_Name", + "Damaged_Smoke_Asset_Name" + }, v => v.EndsWith(".alo", StringComparison.OrdinalIgnoreCase), + ValueListDictionaryExtensions.AggregateStrategy.LastValuePerKey); var terrainMappedModels = LandTerrainModelMapping?.Select(x => x.Model); if (terrainMappedModels is null) @@ -57,7 +68,7 @@ public ISet Models private T? GetLastPropertyOrDefault(string tagName, T? defaultValue = default) { - if (!properties.TryGetLastValue(tagName, out var value)) + if (!_properties.TryGetLastValue(tagName, out var value)) return defaultValue; return (T)value; } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/SfxEvent.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/SfxEvent.cs new file mode 100644 index 0000000..8414df3 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/SfxEvent.cs @@ -0,0 +1,20 @@ +using PG.Commons.Hashing; +using PG.StarWarsGame.Files.XML; + +namespace PG.StarWarsGame.Engine.DataTypes; + +public sealed class SfxEvent : XmlObject +{ + private int _volumeValue; + + public bool IsPreset { get; } + + public SfxEvent? Preset { get; } + + public int Volume => Preset?.Volume ?? _volumeValue; + + internal SfxEvent(string name, Crc32 nameCrc, XmlLocationInfo location) + : base(name, nameCrc, location) + { + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/XmlObject.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/XmlObject.cs new file mode 100644 index 0000000..29914cb --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/XmlObject.cs @@ -0,0 +1,19 @@ +using System; +using PG.Commons.DataTypes; +using PG.Commons.Hashing; +using PG.StarWarsGame.Files.XML; + +namespace PG.StarWarsGame.Engine.DataTypes; + +public abstract class XmlObject( + string name, + Crc32 nameCrc, + XmlLocationInfo location) + : IHasCrc32 +{ + public XmlLocationInfo Location { get; } = location; + + public Crc32 Crc32 { get; } = nameCrc; + + public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabase.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabase.cs index cae38db..c23903a 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabase.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using PG.StarWarsGame.Engine.DataTypes; +using PG.StarWarsGame.Engine.DataTypes; using PG.StarWarsGame.Engine.Repositories; namespace PG.StarWarsGame.Engine.Database; @@ -10,5 +9,7 @@ internal class GameDatabase : IGameDatabase public required GameConstants GameConstants { get; init; } - public required IList GameObjects { get; init; } + public required IXmlDatabase GameObjects { get; init; } + + public required IXmlDatabase SfxEvents { get; init; } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/GameDatabaseService.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabaseService.cs similarity index 68% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/GameDatabaseService.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabaseService.cs index 6465a45..9386d0c 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/GameDatabaseService.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabaseService.cs @@ -2,14 +2,17 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; -using PG.StarWarsGame.Engine.Database; +using PG.StarWarsGame.Engine.Database.Initialization; using PG.StarWarsGame.Engine.Repositories; -namespace PG.StarWarsGame.Engine.Pipeline; +namespace PG.StarWarsGame.Engine.Database; internal class GameDatabaseService(IServiceProvider serviceProvider) : IGameDatabaseService { - public async Task CreateDatabaseAsync(GameEngineType targetEngineType, GameLocations locations, CancellationToken cancellationToken = default) + public async Task CreateDatabaseAsync( + GameEngineType targetEngineType, + GameLocations locations, + CancellationToken cancellationToken = default) { var repoFactory = serviceProvider.GetRequiredService(); var repository = repoFactory.Create(targetEngineType, locations); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabase.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabase.cs index d228af7..7716027 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabase.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using PG.StarWarsGame.Engine.DataTypes; +using PG.StarWarsGame.Engine.DataTypes; using PG.StarWarsGame.Engine.Repositories; namespace PG.StarWarsGame.Engine.Database; @@ -10,5 +9,7 @@ public interface IGameDatabase public GameConstants GameConstants { get; } - public IList GameObjects { get; } + public IXmlDatabase GameObjects { get; } + + public IXmlDatabase SfxEvents { get; } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/IGameDatabaseService.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabaseService.cs similarity index 75% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/IGameDatabaseService.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabaseService.cs index 9492017..21b5971 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/IGameDatabaseService.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabaseService.cs @@ -1,8 +1,7 @@ using System.Threading; using System.Threading.Tasks; -using PG.StarWarsGame.Engine.Database; -namespace PG.StarWarsGame.Engine.Pipeline; +namespace PG.StarWarsGame.Engine.Database; public interface IGameDatabaseService { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IXmlDatabase.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IXmlDatabase.cs new file mode 100644 index 0000000..c2edb8e --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IXmlDatabase.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using AnakinRaW.CommonUtilities.Collections; +using PG.Commons.Hashing; +using PG.StarWarsGame.Engine.DataTypes; + +namespace PG.StarWarsGame.Engine.Database; + +public interface IXmlDatabase where T : XmlObject +{ + ICollection Entries { get; } + + ICollection EntryKeys { get; } + + ReadOnlyFrugalList GetEntries(Crc32 key); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/CreateDatabaseStep.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/CreateDatabaseStep.cs similarity index 78% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/CreateDatabaseStep.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/CreateDatabaseStep.cs index bb94640..2c1f20e 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/CreateDatabaseStep.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/CreateDatabaseStep.cs @@ -4,9 +4,9 @@ using Microsoft.Extensions.Logging; using PG.StarWarsGame.Engine.Repositories; -namespace PG.StarWarsGame.Engine.Pipeline; +namespace PG.StarWarsGame.Engine.Database.Initialization; -public abstract class CreateDatabaseStep(IGameRepository repository, IServiceProvider serviceProvider) +internal abstract class CreateDatabaseStep(IGameRepository repository, IServiceProvider serviceProvider) : PipelineStep(serviceProvider) where T : class { public T Database { get; private set; } = null!; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/GameDatabaseCreationPipeline.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs similarity index 58% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/GameDatabaseCreationPipeline.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs index 4928e33..642461e 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/GameDatabaseCreationPipeline.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs @@ -4,24 +4,45 @@ using System.Threading; using System.Threading.Tasks; using AnakinRaW.CommonUtilities.SimplePipeline; +using AnakinRaW.CommonUtilities.SimplePipeline.Runners; +using AnakinRaW.CommonUtilities.SimplePipeline.Steps; using Microsoft.Extensions.Logging; -using PG.StarWarsGame.Engine.Database; using PG.StarWarsGame.Engine.DataTypes; using PG.StarWarsGame.Engine.Repositories; -namespace PG.StarWarsGame.Engine.Pipeline; +namespace PG.StarWarsGame.Engine.Database.Initialization; -internal class GameDatabaseCreationPipeline(GameRepository repository, IServiceProvider serviceProvider) : ParallelPipeline(serviceProvider) +internal class GameDatabaseCreationPipeline(GameRepository repository, IServiceProvider serviceProvider) + : Pipeline(serviceProvider) { private ParseSingletonXmlStep _parseGameConstants = null!; - private ParseFromContainerStep _parseGameObjects = null!; + private ParseXmlDatabaseFromContainerStep _parseGameObjects = null!; + private ParseXmlDatabaseFromContainerStep _parseSfxEvents = null!; + + private ParallelRunner _parseXmlRunner = null!; public GameDatabase GameDatabase { get; private set; } = null!; - - protected override Task> BuildSteps() + + + protected override Task PrepareCoreAsync() + { + _parseXmlRunner = new ParallelRunner(4, ServiceProvider); + foreach (var xmlParserStep in CreateXmlParserSteps()) _parseXmlRunner.AddStep(xmlParserStep); + + + return Task.FromResult(true); + } + + private IEnumerable CreateXmlParserSteps() { - _parseGameConstants = new ParseSingletonXmlStep("GameConstants", "DATA\\XML\\GAMECONSTANTS.XML", repository, ServiceProvider); - _parseGameObjects = new ParseFromContainerStep("GameObjects", "DATA\\XML\\GAMEOBJECTFILES.XML", repository, ServiceProvider); + yield return _parseGameConstants = new ParseSingletonXmlStep("GameConstants", + "DATA\\XML\\GAMECONSTANTS.XML", repository, ServiceProvider); + + yield return _parseGameObjects = new ParseXmlDatabaseFromContainerStep("GameObjects", + "DATA\\XML\\GAMEOBJECTFILES.XML", repository, ServiceProvider); + + yield return _parseSfxEvents = new ParseXmlDatabaseFromContainerStep("SFXEvents", + "DATA\\XML\\SFXEventFiles.XML", repository, ServiceProvider); // GUIDialogs.xml // LensFlares.xml @@ -54,7 +75,6 @@ protected override Task> BuildSteps() // CONTAINER FILES: // GameObjectFiles.xml - // SFXEventFiles.xml // CommandBarComponentFiles.xml // TradeRouteFiles.xml // HardPointDataFiles.xml @@ -62,12 +82,6 @@ protected override Task> BuildSteps() // FactionFiles.xml // TargetingPrioritySetFiles.xml // MousePointerFiles.xml - - return Task.FromResult>(new List - { - _parseGameConstants, - _parseGameObjects - }); } protected override async Task RunCoreAsync(CancellationToken token) @@ -76,7 +90,22 @@ protected override async Task RunCoreAsync(CancellationToken token) try { - await base.RunCoreAsync(token); + try + { + Logger?.LogInformation("Parsing XML Files..."); + _parseXmlRunner.Error += OnError; + await _parseXmlRunner.RunAsync(token); + } + finally + { + Logger?.LogInformation("Finished parsing XML Files..."); + _parseXmlRunner.Error -= OnError; + } + + ThrowIfAnyStepsFailed(_parseXmlRunner.Steps); + + token.ThrowIfCancellationRequested(); + repository.Seal(); @@ -84,7 +113,8 @@ protected override async Task RunCoreAsync(CancellationToken token) { GameRepository = repository, GameConstants = _parseGameConstants.Database, - GameObjects = _parseGameObjects.Database + GameObjects = _parseGameObjects.Database, + SfxEvents = _parseSfxEvents.Database, }; } finally @@ -106,20 +136,4 @@ protected override T CreateDatabase(IList parsedDatabaseEntries) return parsedDatabaseEntries.First(); } } - - - private sealed class ParseFromContainerStep( - string name, - string xmlFile, - IGameRepository repository, - IServiceProvider serviceProvider) - : ParseXmlDatabaseFromContainerStep>(xmlFile, repository, serviceProvider) - { - protected override string Name => name; - - protected override IList CreateDatabase(IList> parsedDatabaseEntries) - { - return parsedDatabaseEntries.SelectMany(x => x).ToList(); - } - } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/ParseXmlDatabaseFromContainerStep.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/ParseXmlDatabaseFromContainerStep.cs similarity index 68% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/ParseXmlDatabaseFromContainerStep.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/ParseXmlDatabaseFromContainerStep.cs index 00235e7..a29c226 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/ParseXmlDatabaseFromContainerStep.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/ParseXmlDatabaseFromContainerStep.cs @@ -1,27 +1,31 @@ using System; -using System.Collections.Generic; using System.IO.Abstractions; using System.Linq; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using PG.Commons.Hashing; +using PG.StarWarsGame.Engine.DataTypes; using PG.StarWarsGame.Engine.Repositories; using PG.StarWarsGame.Engine.Xml; using PG.StarWarsGame.Files.XML; -namespace PG.StarWarsGame.Engine.Pipeline; +namespace PG.StarWarsGame.Engine.Database.Initialization; -public abstract class ParseXmlDatabaseFromContainerStep( +internal class ParseXmlDatabaseFromContainerStep( + string name, string xmlFile, IGameRepository repository, IServiceProvider serviceProvider) - : CreateDatabaseStep(repository, serviceProvider) - where T : class + : CreateDatabaseStep>(repository, serviceProvider) + where T : XmlObject { private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); protected readonly IPetroglyphXmlFileParserFactory FileParserFactory = serviceProvider.GetRequiredService(); - protected sealed override T CreateDatabase() + protected override string Name => name; + + protected sealed override IXmlDatabase CreateDatabase() { using var containerStream = GameRepository.OpenFile(xmlFile); var containerParser = FileParserFactory.GetFileParser(); @@ -30,19 +34,17 @@ protected sealed override T CreateDatabase() var xmlFiles = container.Files.Select(x => _fileSystem.Path.Combine("DATA\\XML", x)).ToList(); + var parsedEntries = new ValueListDictionary(); - var parsedDatabaseEntries = new List(); foreach (var file in xmlFiles) { using var fileStream = GameRepository.OpenFile(file); var parser = FileParserFactory.GetFileParser(); Logger?.LogDebug($"Parsing File '{file}'"); - var parsedData = parser.ParseFile(fileStream); - parsedDatabaseEntries.Add(parsedData); + parser.ParseFile(fileStream, parsedEntries); } - return CreateDatabase(parsedDatabaseEntries); - } - protected abstract T CreateDatabase(IList files); + return new XmlDatabase(parsedEntries, Services); + } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/ParseXmlDatabaseStep.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/ParseXmlDatabaseStep.cs similarity index 92% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/ParseXmlDatabaseStep.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/ParseXmlDatabaseStep.cs index 2fb1c9d..4bed127 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Pipeline/ParseXmlDatabaseStep.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/ParseXmlDatabaseStep.cs @@ -5,9 +5,9 @@ using PG.StarWarsGame.Engine.Repositories; using PG.StarWarsGame.Engine.Xml; -namespace PG.StarWarsGame.Engine.Pipeline; +namespace PG.StarWarsGame.Engine.Database.Initialization; -public abstract class ParseXmlDatabaseStep( +internal abstract class ParseXmlDatabaseStep( IList xmlFiles, IGameRepository repository, IServiceProvider serviceProvider) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/XmlDatabase.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/XmlDatabase.cs new file mode 100644 index 0000000..38d0a78 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/XmlDatabase.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using AnakinRaW.CommonUtilities.Collections; +using PG.Commons.Hashing; +using PG.StarWarsGame.Engine.DataTypes; +using PG.StarWarsGame.Files.XML; + +namespace PG.StarWarsGame.Engine.Database; + +internal class XmlDatabase(IReadOnlyValueListDictionary parsedObjects, IServiceProvider serviceProvider) : IXmlDatabase + where T : XmlObject +{ + + private readonly IServiceProvider _serviceProvider = + serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + + private readonly IReadOnlyValueListDictionary _parsedObjects = parsedObjects ?? throw new ArgumentNullException(nameof(parsedObjects)); + + public ICollection Entries => _parsedObjects.Values; + + public ICollection EntryKeys => _parsedObjects.Keys; + + public ReadOnlyFrugalList GetEntries(Crc32 key) + { + return _parsedObjects.GetValues(key); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs index 36f02b4..4b02a65 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Engine.Database; using PG.StarWarsGame.Engine.Language; -using PG.StarWarsGame.Engine.Pipeline; using PG.StarWarsGame.Engine.Repositories; using PG.StarWarsGame.Engine.Xml; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs index 32dcedd..74c21df 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs @@ -249,6 +249,7 @@ internal void Seal() protected MegDataEntryReference? FindFileInMasterMeg(string filePath) { + // TODO To Span, as we don't use the name elsewhere var normalizedPath = _megPathNormalizer.Normalize(filePath); var crc = _crc32HashingService.GetCrc32(normalizedPath, PGConstants.PGCrc32Encoding); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameConstantsParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameConstantsParser.cs new file mode 100644 index 0000000..23c5917 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameConstantsParser.cs @@ -0,0 +1,21 @@ +using System; +using System.Xml.Linq; +using PG.Commons.Hashing; +using PG.StarWarsGame.Engine.DataTypes; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Parsers; + +namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; + +internal class GameConstantsParser(IServiceProvider serviceProvider) : PetroglyphXmlFileParser(serviceProvider) +{ + public override GameConstants Parse(XElement element) + { + return new GameConstants(); + } + + protected override void Parse(XElement element, IValueListDictionary parsedElements) + { + throw new NotSupportedException(); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/GameObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs similarity index 90% rename from src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/GameObjectParser.cs rename to src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs index d3039f5..5e07b24 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/GameObjectParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Xml.Linq; using Microsoft.Extensions.DependencyInjection; using PG.Commons.Hashing; @@ -8,7 +7,7 @@ using PG.StarWarsGame.Files.XML.Parsers; using PG.StarWarsGame.Files.XML.Parsers.Primitives; -namespace PG.StarWarsGame.Engine.Xml.Parsers; +namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; public sealed class GameObjectParser(IServiceProvider serviceProvider) : PetroglyphXmlElementParser(serviceProvider) { @@ -39,14 +38,18 @@ public sealed class GameObjectParser(IServiceProvider serviceProvider) : Petrogl } public override GameObject Parse(XElement element) + { + throw new NotSupportedException(); + } + + public override GameObject Parse(XElement element, IReadOnlyValueListDictionary parsedElements, out Crc32 nameCrc) { var properties = ToKeyValuePairList(element); var name = GetNameAttributeValue(element); - var nameCrc = _crc32Hashing.GetCrc32(name, PGConstants.PGCrc32Encoding); + nameCrc = _crc32Hashing.GetCrc32Upper(name.AsSpan(), PGConstants.PGCrc32Encoding); var type = GetTagName(element); var objectType = EstimateType(type); - var location = XmlLocationInfo.FromElement(element); - var gameObject = new GameObject(type, name, nameCrc, objectType, properties, location); + var gameObject = new GameObject(type, name, nameCrc, objectType, properties, XmlLocationInfo.FromElement(element)); return gameObject; } @@ -101,13 +104,6 @@ private static GameObjectType EstimateType(string tagName) }; } - public string GetNameAttributeValue(XElement element) - { - var nameAttribute = element.Attributes() - .FirstOrDefault(a => a.Name.LocalName == "Name"); - return nameAttribute is null ? string.Empty : nameAttribute.Value; - } - public string GetTagName(XElement element) { return element.Name.LocalName; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs new file mode 100644 index 0000000..4ec2f0d --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs @@ -0,0 +1,73 @@ +using System; +using System.Xml.Linq; +using Microsoft.Extensions.DependencyInjection; +using PG.Commons.Hashing; +using PG.StarWarsGame.Engine.DataTypes; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Parsers; + +namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; + +public sealed class SfxEventParser(IServiceProvider serviceProvider) : PetroglyphXmlElementParser(serviceProvider) +{ + private readonly ICrc32HashingService _hashingService = serviceProvider.GetRequiredService(); + + protected override IPetroglyphXmlElementParser? GetParser(string tag) + { + return null; + //switch (tag) + //{ + // case "Land_Terrain_Model_Mapping": + // return new CommaSeparatedStringKeyValueListParser(ServiceProvider); + // case "Galactic_Model_Name": + // case "Damaged_Smoke_Asset_Name": + // return PetroglyphXmlStringParser.Instance; + // default: + // return null; + //} + } + + public override SfxEvent Parse( + XElement element, + IReadOnlyValueListDictionary parsedElements, + out Crc32 nameCrc) + { + var name = GetNameAttributeValue(element); + nameCrc = _hashingService.GetCrc32Upper(name.AsSpan(), PGConstants.PGCrc32Encoding); + + var valueList = new ValueListDictionary(); + + foreach (var child in element.Elements()) + { + var tagName = child.Name.LocalName; + var parser = GetParser(tagName); + if (parser is null) + continue; + + if (tagName.Equals("Use_Preset")) + { + var presetName = parser.Parse(child) as string; + var presetNameCrc = _hashingService.GetCrc32Upper(presetName.AsSpan(), PGConstants.PGCrc32Encoding); + if (presetNameCrc == default || !parsedElements.TryGetFirstValue(presetNameCrc, out var preset)) + { + // Unable to find Preset + continue; + } + } + else + { + valueList.Add(tagName, parser.Parse(child)); + } + } + + var sfxEvent = new SfxEvent(name, nameCrc, XmlLocationInfo.FromElement(element)); + + return sfxEvent; + } + + 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/GameObjectFileFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileFileParser.cs new file mode 100644 index 0000000..e02a86d --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileFileParser.cs @@ -0,0 +1,29 @@ +using System; +using System.Xml.Linq; +using PG.Commons.Hashing; +using PG.StarWarsGame.Engine.DataTypes; +using PG.StarWarsGame.Engine.Xml.Parsers.Data; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Parsers; + +namespace PG.StarWarsGame.Engine.Xml.Parsers.File; + +internal class GameObjectFileFileParser(IServiceProvider serviceProvider) + : PetroglyphXmlFileParser(serviceProvider) +{ + protected override void Parse(XElement element, IValueListDictionary parsedElements) + { + var parser = new GameObjectParser(ServiceProvider); + + foreach (var xElement in element.Elements()) + { + var sfxEvent = parser.Parse(xElement, parsedElements, out var nameCrc); + parsedElements.Add(nameCrc, sfxEvent); + } + } + + 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/File/SfxEventFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs new file mode 100644 index 0000000..c02b6bb --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs @@ -0,0 +1,29 @@ +using System; +using System.Xml.Linq; +using PG.Commons.Hashing; +using PG.StarWarsGame.Engine.DataTypes; +using PG.StarWarsGame.Engine.Xml.Parsers.Data; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Parsers; + +namespace PG.StarWarsGame.Engine.Xml.Parsers.File; + +internal class SfxEventFileParser(IServiceProvider serviceProvider) + : PetroglyphXmlFileParser(serviceProvider) +{ + protected override void Parse(XElement element, IValueListDictionary parsedElements) + { + var parser = new SfxEventParser(ServiceProvider); + var parsedObjects = new ValueListDictionary(); + foreach (var xElement in element.Elements()) + { + var sfxEvent = parser.Parse(xElement, parsedObjects, out var nameCrc); + parsedObjects.Add(nameCrc, sfxEvent); + } + } + + 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/GameConstantsFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/GameConstantsFileParser.cs deleted file mode 100644 index ac20c1c..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/GameConstantsFileParser.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Xml.Linq; -using PG.StarWarsGame.Engine.DataTypes; -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Engine.Xml.Parsers; - -public class GameConstantsFileParser(IServiceProvider serviceProvider) : PetroglyphXmlFileParser(serviceProvider) -{ - public override GameConstants Parse(XElement element) - { - return new GameConstants(); - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/GameObjectFileFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/GameObjectFileFileParser.cs deleted file mode 100644 index 3bb7e0a..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/GameObjectFileFileParser.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; -using PG.StarWarsGame.Engine.DataTypes; -using PG.StarWarsGame.Files.XML.Parsers; - -namespace PG.StarWarsGame.Engine.Xml.Parsers; - -public class GameObjectFileFileParser(IServiceProvider serviceProvider) : PetroglyphXmlFileParser>(serviceProvider) -{ - protected override bool LoadLineInfo => true; - - public override IList Parse(XElement element) - { - var parser = new GameObjectParser(ServiceProvider); - return element.Elements().Select(parser.Parse).ToList(); - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs index a29f79a..10af5a0 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs @@ -1,7 +1,8 @@ using System; using System.Collections.Generic; using PG.StarWarsGame.Engine.DataTypes; -using PG.StarWarsGame.Engine.Xml.Parsers; +using PG.StarWarsGame.Engine.Xml.Parsers.Data; +using PG.StarWarsGame.Engine.Xml.Parsers.File; using PG.StarWarsGame.Files.XML; using PG.StarWarsGame.Files.XML.Parsers; using PG.StarWarsGame.Files.XML.Parsers.Primitives; @@ -21,11 +22,14 @@ public IPetroglyphXmlFileParser GetFileParser(Type type) return new XmlFileContainerParser(serviceProvider); if (type == typeof(GameConstants)) - return new GameConstantsFileParser(serviceProvider); + return new GameConstantsParser(serviceProvider); - if (type == typeof(IList)) + if (type == typeof(GameObject)) return new GameObjectFileFileParser(serviceProvider); + if (type == typeof(SfxEvent)) + return new SfxEventFileParser(serviceProvider); + throw new NotImplementedException($"The parser for the type {type} is not yet implemented."); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlElementParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlElementParser.cs index a26a06b..8fb31c6 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlElementParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlElementParser.cs @@ -1,13 +1,16 @@ using System.Xml.Linq; +using PG.Commons.Hashing; namespace PG.StarWarsGame.Files.XML.Parsers; public interface IPetroglyphXmlElementParser { - public object? Parse(XElement element); + public object Parse(XElement element); } -public interface IPetroglyphXmlElementParser : IPetroglyphXmlElementParser +public interface IPetroglyphXmlElementParser : IPetroglyphXmlElementParser { public new T Parse(XElement element); + + public T Parse(XElement element, IReadOnlyValueListDictionary parsedElements, out Crc32 nameCrc); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlFileParser.cs index b958e1f..817a13c 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlFileParser.cs @@ -1,4 +1,5 @@ using System.IO; +using PG.Commons.Hashing; namespace PG.StarWarsGame.Files.XML.Parsers; @@ -7,7 +8,9 @@ public interface IPetroglyphXmlFileParser : IPetroglyphXmlElementParser public object? ParseFile(Stream stream); } -public interface IPetroglyphXmlFileParser : IPetroglyphXmlElementParser, IPetroglyphXmlFileParser +public interface IPetroglyphXmlFileParser : IPetroglyphXmlElementParser, IPetroglyphXmlFileParser { public new T ParseFile(Stream stream); + + public void ParseFile(Stream stream, IValueListDictionary parsedEntries); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlElementParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlElementParser.cs index 850bf26..b67fc88 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlElementParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlElementParser.cs @@ -1,5 +1,7 @@ using System; +using System.Linq; using System.Xml.Linq; +using PG.Commons.Hashing; using PG.StarWarsGame.Files.XML.Parsers.Primitives; namespace PG.StarWarsGame.Files.XML.Parsers; @@ -8,6 +10,7 @@ public abstract class PetroglyphXmlElementParser(IServiceProvider serviceProv { protected IServiceProvider ServiceProvider { get; } = serviceProvider; + protected virtual IPetroglyphXmlElementParser? GetParser(string tag) { return PetroglyphXmlStringParser.Instance; @@ -15,6 +18,8 @@ public abstract class PetroglyphXmlElementParser(IServiceProvider serviceProv public abstract T Parse(XElement element); + public abstract T Parse(XElement element, IReadOnlyValueListDictionary parsedElements, out Crc32 nameCrc); + public ValueListDictionary ToKeyValuePairList(XElement element) { var keyValuePairList = new ValueListDictionary(); @@ -30,10 +35,16 @@ public ValueListDictionary ToKeyValuePairList(XElement element) keyValuePairList.Add(tagName, value); } } - return keyValuePairList; } + protected string GetNameAttributeValue(XElement element) + { + var nameAttribute = element.Attributes() + .FirstOrDefault(a => a.Name.LocalName == "Name"); + return nameAttribute is null ? string.Empty : nameAttribute.Value; + } + object? IPetroglyphXmlElementParser.Parse(XElement element) { return Parse(element); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs index 16e5778..c721f5a 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs @@ -2,14 +2,37 @@ using System.IO; using System.Xml; using System.Xml.Linq; +using PG.Commons.Hashing; using PG.Commons.Utilities; namespace PG.StarWarsGame.Files.XML.Parsers; -public abstract class PetroglyphXmlFileParser(IServiceProvider serviceProvider) : PetroglyphXmlElementParser(serviceProvider), IPetroglyphXmlFileParser +public abstract class PetroglyphXmlFileParser(IServiceProvider serviceProvider) + : PetroglyphXmlElementParser(serviceProvider), IPetroglyphXmlFileParser { protected virtual bool LoadLineInfo => false; + public T ParseFile(Stream xmlStream) + { + var root = GetRootElement(xmlStream); + return root is null ? default! : Parse(root); + } + + public void ParseFile(Stream xmlStream, IValueListDictionary parsedEntries) + { + var root = GetRootElement(xmlStream); + if (root is not null) + Parse(root, parsedEntries); + } + + public sealed override T Parse(XElement element, IReadOnlyValueListDictionary parsedElements, out Crc32 nameCrc) + { + throw new NotSupportedException(); + } + + protected abstract void Parse(XElement element, IValueListDictionary parsedElements); + + private XElement? GetRootElement(Stream xmlStream) { var fileName = xmlStream.GetFilePath(); var xmlReader = XmlReader.Create(xmlStream, new XmlReaderSettings(), fileName); @@ -19,10 +42,7 @@ public T ParseFile(Stream xmlStream) options |= LoadOptions.SetLineInfo; var doc = XDocument.Load(xmlReader, options); - var root = doc.Root; - if (root is null) - return default!; - return Parse(root); + return doc.Root; } object? IPetroglyphXmlFileParser.ParseFile(Stream stream) 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 ee1929d..e0de592 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/CommaSeparatedStringKeyValueListParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/CommaSeparatedStringKeyValueListParser.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Xml.Linq; +using PG.Commons.Hashing; namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; @@ -34,4 +35,9 @@ public sealed class CommaSeparatedStringKeyValueListParser(IServiceProvider serv return keyValueList; } + + public override IList<(string key, string value)> Parse(XElement element, IReadOnlyValueListDictionary> parsedElements, out Crc32 nameCrc) + { + throw new NotSupportedException(); + } } \ 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 66253ae..5f53e49 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlStringParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlStringParser.cs @@ -1,5 +1,6 @@ using System; using System.Xml.Linq; +using PG.Commons.Hashing; namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; @@ -16,4 +17,9 @@ public override string Parse(XElement element) { return element.Value.Trim(); } + + public override string Parse(XElement element, IReadOnlyValueListDictionary parsedElements, out Crc32 nameCrc) + { + throw new NotSupportedException(); + } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/XmlFileContainerParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/XmlFileContainerParser.cs index 490ce83..b2954ab 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/XmlFileContainerParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/XmlFileContainerParser.cs @@ -1,16 +1,17 @@ using System; using System.Linq; using System.Xml.Linq; +using PG.Commons.Hashing; namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; public class XmlFileContainerParser(IServiceProvider serviceProvider) : PetroglyphXmlFileParser(serviceProvider) { - protected override IPetroglyphXmlElementParser? GetParser(string tag) + protected override bool LoadLineInfo => false; + + protected override void Parse(XElement element, IValueListDictionary parsedElements) { - if (tag == "File") - return PetroglyphXmlStringParser.Instance; - return null; + throw new NotSupportedException(); } public override XmlFileContainer Parse(XElement element) @@ -21,4 +22,9 @@ public override XmlFileContainer Parse(XElement element) ? new XmlFileContainer(files.OfType().ToList()) : new XmlFileContainer([]); } + protected override IPetroglyphXmlElementParser? GetParser(string tag) + { + return tag == "File" ? PetroglyphXmlStringParser.Instance : null; + } + } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ValueListDictionary.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ValueListDictionary.cs index 19cd60a..5da2eb7 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ValueListDictionary.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ValueListDictionary.cs @@ -1,26 +1,57 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; +using AnakinRaW.CommonUtilities.Collections; namespace PG.StarWarsGame.Files.XML; + +public interface IReadOnlyValueListDictionary : IEnumerable> where TKey : notnull +{ + ICollection Values { get; } + ICollection Keys { get; } + + bool ContainsKey(TKey key); + + ReadOnlyFrugalList GetValues(TKey key); + + TValue GetLastValue(TKey key); + + TValue GetFirstValue(TKey key); + + bool TryGetFirstValue(TKey key, [NotNullWhen(true)] out TValue value); + + bool TryGetLastValue(TKey key, [NotNullWhen(true)] out TValue value); + + bool TryGetValues(TKey key, out ReadOnlyFrugalList values); +} + +public interface IValueListDictionary : IReadOnlyValueListDictionary where TKey : notnull +{ + bool Add(TKey key, TValue value); +} + // NOT THREAD-SAFE! -public class ValueListDictionary where TKey : notnull +public class ValueListDictionary : IValueListDictionary where TKey : notnull { - private readonly Dictionary _singleValueDictionary = new (); - private readonly Dictionary> _multiValueDictionary = new(); + private readonly Dictionary _singleValueDictionary = new (); + private readonly Dictionary> _multiValueDictionary = new(); + public ICollection Keys => _singleValueDictionary.Keys.Concat(_multiValueDictionary.Keys).ToList(); + + public ICollection Values => this.Select(x => x.Value).ToList(); public bool ContainsKey(TKey key) { return _singleValueDictionary.ContainsKey(key) || _multiValueDictionary.ContainsKey(key); } - - public bool Add(TKey key, TValue? value) + + public bool Add(TKey key, TValue value) { - if (key == null) + if (key is null) throw new ArgumentNullException(nameof(key)); if (!_singleValueDictionary.ContainsKey(key)) @@ -48,7 +79,7 @@ public bool Add(TKey key, TValue? value) return true; } - public TValue? GetLastValue(TKey key) + public TValue GetLastValue(TKey key) { if (_singleValueDictionary.TryGetValue(key, out var value)) return value; @@ -59,66 +90,153 @@ public bool Add(TKey key, TValue? value) throw new KeyNotFoundException($"The key '{key}' was not found."); } - public T? GetLastValue(TKey key) where T : TValue + public TValue GetFirstValue(TKey key) { - var value = GetLastValue(key); - if (value is null) - return default; - return (T)value; + if (_singleValueDictionary.TryGetValue(key, out var value)) + return value; + + if (_multiValueDictionary.TryGetValue(key, out var valueList)) + return valueList.First(); + + throw new KeyNotFoundException($"The key '{key}' was not found."); } + + public ReadOnlyFrugalList GetValues(TKey key) + { + if (TryGetValues(key, out var values)) + return values; + throw new KeyNotFoundException($"The key '{key}' was not found."); - public IList GetValues(TKey key) + } + + public bool TryGetFirstValue(TKey key, [NotNullWhen(true)] out TValue value) { - if (!TryGetValues(key, out var values)) - throw new KeyNotFoundException($"The key '{key}' was not found."); - return values; + if (_singleValueDictionary.TryGetValue(key, out value!)) + return true; + + if (_multiValueDictionary.TryGetValue(key, out var valueList)) + { + value = valueList.First()!; + return true; + } + + return false; } - public bool TryGetLastValue(TKey key, [NotNullWhen(true)] out TValue? value) + public bool TryGetLastValue(TKey key, [NotNullWhen(true)] out TValue value) { if (_singleValueDictionary.TryGetValue(key, out value!)) return true; if (_multiValueDictionary.TryGetValue(key, out var valueList)) { - value = valueList.Last()!; + value = valueList.Last()!; return true; } return false; } - public bool TryGetValues(TKey key, [NotNullWhen(true)] out IList? values) + public bool TryGetValues(TKey key, out ReadOnlyFrugalList values) { if (_singleValueDictionary.TryGetValue(key, out var value)) { - values = new List(1) - { - value - }; + values = new ReadOnlyFrugalList(value); return true; } if (_multiValueDictionary.TryGetValue(key, out var valueList)) { - values = valueList; + values = new ReadOnlyFrugalList(valueList); return true; } - values = null; + values = ReadOnlyFrugalList.Empty; return false; } - public IEnumerable AggregateValues(ISet keys, Predicate filter, bool multipleValuesPerKey = false) where T : TValue + public IEnumerator> GetEnumerator() + { + return new Enumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public struct Enumerator : IEnumerator> + { + private Dictionary.Enumerator _singleEnumerator; + private Dictionary>.Enumerator _multiEnumerator; + private List.Enumerator _currentListEnumerator = default; + private bool _isMultiEnumeratorActive = false; + + internal Enumerator(ValueListDictionary valueListDictionary) + { + _singleEnumerator = valueListDictionary._singleValueDictionary.GetEnumerator(); + _multiEnumerator = valueListDictionary._multiValueDictionary.GetEnumerator(); + } + + public KeyValuePair Current => + _isMultiEnumeratorActive + ? new KeyValuePair(_multiEnumerator.Current.Key, _currentListEnumerator.Current) + : _singleEnumerator.Current; + + object IEnumerator.Current => Current; + + public bool MoveNext() + { + if (_singleEnumerator.MoveNext()) + return true; + + if (_isMultiEnumeratorActive) + { + if (_currentListEnumerator.MoveNext()) + return true; + _isMultiEnumeratorActive = false; + } + + if (_multiEnumerator.MoveNext()) + { + _currentListEnumerator = _multiEnumerator.Current.Value.GetEnumerator(); + _isMultiEnumeratorActive = true; + return _currentListEnumerator.MoveNext(); + } + + return false; + } + + public void Reset() + { + throw new NotSupportedException(); + } + + public void Dispose() + { + _singleEnumerator.Dispose(); + _multiEnumerator.Dispose(); + } + } +} + +public static class ValueListDictionaryExtensions +{ + public static IEnumerable AggregateValues( + this IReadOnlyValueListDictionary valueListDictionary, + ISet keys, Predicate filter, + AggregateStrategy aggregateStrategy) + where TKey : notnull + where T : TValue { foreach (var key in keys) { - if (!ContainsKey(key)) + if (!valueListDictionary.ContainsKey(key)) continue; - if (multipleValuesPerKey) + if (aggregateStrategy == AggregateStrategy.MultipleValuesPerKey) { - foreach (var value in GetValues(key)) + foreach (var value in valueListDictionary.GetValues(key)) { if (value is not null) { @@ -126,12 +244,14 @@ public IEnumerable AggregateValues(ISet keys, Predicate filter, b if (filter(typedValue)) yield return typedValue; } - + } } else { - var value = GetLastValue(key); + var value = aggregateStrategy == AggregateStrategy.FirstValuePerKey + ? valueListDictionary.GetFirstValue(key) + : valueListDictionary.GetLastValue(key); if (value is not null) { var typedValue = (T)value; @@ -141,4 +261,11 @@ public IEnumerable AggregateValues(ISet keys, Predicate filter, b } } } + + public enum AggregateStrategy + { + FirstValuePerKey, + LastValuePerKey, + MultipleValuesPerKey, + } } \ No newline at end of file From 46259a5c47c9736070fbbbe9893c768728dda4b8 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sun, 9 Jun 2024 23:16:26 +0200 Subject: [PATCH 04/25] update to span --- Directory.Build.props | 2 +- PetroglyphTools | 2 +- src/ModVerify/ModVerify.csproj | 2 +- .../PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj | 2 +- .../PG.StarWarsGame.Engine/Repositories/GameRepository.cs | 7 ++++--- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 1dda2dd..1d41c28 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ all - 3.6.133 + 3.6.139 \ No newline at end of file diff --git a/PetroglyphTools b/PetroglyphTools index adf607f..bed47f6 160000 --- a/PetroglyphTools +++ b/PetroglyphTools @@ -1 +1 @@ -Subproject commit adf607fbe479c315ea142564f08b637e839914b1 +Subproject commit bed47f643f89ad151035e236a3a3a0207cf7dde0 diff --git a/src/ModVerify/ModVerify.csproj b/src/ModVerify/ModVerify.csproj index bc7995d..1da53e4 100644 --- a/src/ModVerify/ModVerify.csproj +++ b/src/ModVerify/ModVerify.csproj @@ -20,7 +20,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj index ba82246..6b118d5 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj @@ -17,7 +17,7 @@ true - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs index 74c21df..55efd5e 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs @@ -249,9 +249,10 @@ internal void Seal() protected MegDataEntryReference? FindFileInMasterMeg(string filePath) { - // TODO To Span, as we don't use the name elsewhere - var normalizedPath = _megPathNormalizer.Normalize(filePath); - var crc = _crc32HashingService.GetCrc32(normalizedPath, PGConstants.PGCrc32Encoding); + Span fileNameBuffer = stackalloc char[260]; + var length = _megPathNormalizer.Normalize(filePath.AsSpan(), fileNameBuffer); + var fileName = fileNameBuffer.Slice(0, length); + var crc = _crc32HashingService.GetCrc32(fileName, PGConstants.PGCrc32Encoding); return MasterMegArchive?.FirstEntryWithCrc(crc); } From 1dbb03c014a4eb507bbbbb9db91583b7deac324e Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Thu, 13 Jun 2024 21:30:50 +0200 Subject: [PATCH 05/25] update sub --- PetroglyphTools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PetroglyphTools b/PetroglyphTools index bed47f6..aec9119 160000 --- a/PetroglyphTools +++ b/PetroglyphTools @@ -1 +1 @@ -Subproject commit bed47f643f89ad151035e236a3a3a0207cf7dde0 +Subproject commit aec9119d41da5b77ab9695daa7381fe687bd5659 From 5c86e97e0e96d68e96691345cf4941f1d3480adf Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Thu, 13 Jun 2024 21:36:21 +0200 Subject: [PATCH 06/25] update deps --- src/ModVerify/ModVerify.csproj | 2 +- .../PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ModVerify/ModVerify.csproj b/src/ModVerify/ModVerify.csproj index 1da53e4..424b40c 100644 --- a/src/ModVerify/ModVerify.csproj +++ b/src/ModVerify/ModVerify.csproj @@ -20,7 +20,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj index 6b118d5..0c08df4 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj @@ -17,7 +17,7 @@ true - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 841621798bd1ec9eeeb543d98f24a2ba28ce28dc Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Fri, 14 Jun 2024 16:49:59 +0200 Subject: [PATCH 07/25] add descrptions --- src/ModVerify.CliApp/ModVerify.CliApp.csproj | 1 + src/ModVerify/ModVerify.csproj | 1 + 2 files changed, 2 insertions(+) diff --git a/src/ModVerify.CliApp/ModVerify.CliApp.csproj b/src/ModVerify.CliApp/ModVerify.CliApp.csproj index 66924d6..46edaae 100644 --- a/src/ModVerify.CliApp/ModVerify.CliApp.csproj +++ b/src/ModVerify.CliApp/ModVerify.CliApp.csproj @@ -6,6 +6,7 @@ Exe AET.ModVerify.CommandLine AET.ModVerify + Application that allows to verify to verify game modifications for Empire at War / Forces of Corruption against a set of common rules. AlamoEngineTools.ModVerify.CliApp alamo,petroglyph,glyphx diff --git a/src/ModVerify/ModVerify.csproj b/src/ModVerify/ModVerify.csproj index 424b40c..0903c9e 100644 --- a/src/ModVerify/ModVerify.csproj +++ b/src/ModVerify/ModVerify.csproj @@ -4,6 +4,7 @@ netstandard2.0;netstandard2.1 AET.ModVerify AET.ModVerify + Provides interfaces and classes to verify Empire at War / Forces of Corruption game modifications. AlamoEngineTools.ModVerify AET.ModVerify alamo,petroglyph,glyphx From 6d7f358ddc04292984bf325ba0180c2b11b91aec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Gr=C3=BCnwald?= Date: Sat, 15 Jun 2024 15:06:27 +0200 Subject: [PATCH 08/25] Add the option to run the CLI with arguments --- src/ModVerify.CliApp/ModFinderService.cs | 9 +- src/ModVerify.CliApp/ModVerify.CliApp.csproj | 1 + src/ModVerify.CliApp/Program.cs | 86 ++++++++++++++------ src/ModVerify/Steps/DuplicateFinderStep.cs | 2 +- 4 files changed, 70 insertions(+), 28 deletions(-) diff --git a/src/ModVerify.CliApp/ModFinderService.cs b/src/ModVerify.CliApp/ModFinderService.cs index ab61067..71c96f6 100644 --- a/src/ModVerify.CliApp/ModFinderService.cs +++ b/src/ModVerify.CliApp/ModFinderService.cs @@ -29,9 +29,9 @@ public ModFinderService(IServiceProvider serviceProvider) _logger = _serviceProvider.GetService()?.CreateLogger(GetType()); } - public GameFinderResult FindAndAddModInCurrentDirectory() + public GameFinderResult FindAndAddModInDirectory(string path) { - var currentDirectory = _fileSystem.DirectoryInfo.New(Environment.CurrentDirectory); + var currentDirectory = _fileSystem.DirectoryInfo.New(path); // Assuming the currentDir is inside a Mod's directory, we need to go up two level (Game/Mods/ModDir) var potentialGameDirectory = currentDirectory.Parent?.Parent; @@ -90,4 +90,9 @@ public GameFinderResult FindAndAddModInCurrentDirectory() return new GameFinderResult(foc, eaw); } + + public GameFinderResult FindAndAddModInCurrentDirectory() + { + return FindAndAddModInDirectory(Environment.CurrentDirectory); + } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModVerify.CliApp.csproj b/src/ModVerify.CliApp/ModVerify.CliApp.csproj index 46edaae..a76e648 100644 --- a/src/ModVerify.CliApp/ModVerify.CliApp.csproj +++ b/src/ModVerify.CliApp/ModVerify.CliApp.csproj @@ -19,6 +19,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/ModVerify.CliApp/Program.cs b/src/ModVerify.CliApp/Program.cs index 7800a90..95f4228 100644 --- a/src/ModVerify.CliApp/Program.cs +++ b/src/ModVerify.CliApp/Program.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.IO.Abstractions; using System.Linq; using System.Runtime.CompilerServices; @@ -10,6 +11,7 @@ using AnakinRaW.CommonUtilities.Hashing; using AnakinRaW.CommonUtilities.Registry; using AnakinRaW.CommonUtilities.Registry.Windows; +using CommandLine; using EawModinfo.Spec; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -30,45 +32,62 @@ namespace ModVerify.CliApp; internal class Program { private static IServiceProvider _services = null!; + private static CliOptions _options; - static async Task Main(string[] args) + private static async Task Main(string[] args) { + _options = Parser.Default.ParseArguments(args).WithParsed(o => { }).Value; _services = CreateAppServices(); - var gameFinderResult = new ModFinderService(_services).FindAndAddModInCurrentDirectory(); - - var game = gameFinderResult.Game; - Console.WriteLine($"0: {game.Name}"); - - var list = new List { game }; + GameFinderResult gameFinderResult; + IPlayableObject? selectedObject = null; - var counter = 1; - foreach (var mod in game.Mods) + if (!string.IsNullOrEmpty(_options.Path)) { - var isSteam = mod.Type == ModType.Workshops; - var line = $"{counter++}: {mod.Name}"; - if (isSteam) - line += "*"; - Console.WriteLine(line); - list.Add(mod); + var fs = _services.GetService(); + if (!fs!.Directory.Exists(_options.Path)) + throw new DirectoryNotFoundException($"No directory found at {_options.Path}"); + + gameFinderResult = new ModFinderService(_services).FindAndAddModInDirectory(_options.Path); + var selectedPath = fs.Path.GetFullPath(_options.Path).ToUpper(); + selectedObject = + (from mod1 in gameFinderResult.Game.Mods.OfType() + let modPath = fs.Path.GetFullPath(mod1.Directory.FullName) + where selectedPath.Equals(modPath) + select mod1).FirstOrDefault(); + if (selectedObject == null) throw new Exception($"The selected directory {_options.Path} is not a mod."); } + else + { + gameFinderResult = new ModFinderService(_services).FindAndAddModInCurrentDirectory(); + var game = gameFinderResult.Game; + Console.WriteLine($"0: {game.Name}"); - IPlayableObject? selectedObject = null; + var list = new List { game }; - do - { - Console.Write("Select a game or mod to verify: "); - var numberString = Console.ReadLine(); + var counter = 1; + foreach (var mod in game.Mods) + { + var isSteam = mod.Type == ModType.Workshops; + var line = $"{counter++}: {mod.Name}"; + if (isSteam) + line += "*"; + Console.WriteLine(line); + list.Add(mod); + } - if (int.TryParse(numberString, out var number)) + do { + Console.Write("Select a game or mod to verify: "); + var numberString = Console.ReadLine(); + + if (!int.TryParse(numberString, out var number)) continue; if (number < list.Count) selectedObject = list[number]; - } - } while (selectedObject is null); + } while (selectedObject is null); + } Console.WriteLine($"Verifying {selectedObject.Name}..."); - var verifyPipeline = BuildPipeline(selectedObject, gameFinderResult.FallbackGame); try @@ -121,7 +140,7 @@ private static IServiceProvider CreateAppServices() RuntimeHelpers.RunClassConstructor(typeof(IMegArchive).TypeHandle); AloServiceContribution.ContributeServices(serviceCollection); serviceCollection.CollectPgServiceContributions(); - + PetroglyphEngineServiceContribution.ContributeServices(serviceCollection); ModVerifyServiceContribution.ContributeServices(serviceCollection); @@ -137,8 +156,25 @@ private static void ConfigureLogging(ILoggingBuilder loggingBuilder) #if DEBUG logLevel = LogLevel.Debug; loggingBuilder.AddDebug(); +#else + if (_options.Verbose) + { + logLevel = LogLevel.Debug; + loggingBuilder.AddDebug(); + } #endif loggingBuilder.AddConsole(); + loggingBuilder.SetMinimumLevel(logLevel); + } + + + internal class CliOptions + { + [Option('v', "verbose", Required = false, HelpText = "Set output to verbose messages.")] + public bool Verbose { get; set; } + + [Option('p', "path", Required = false, HelpText = "The path to a mod directory to verify.")] + public string Path { get; set; } } } diff --git a/src/ModVerify/Steps/DuplicateFinderStep.cs b/src/ModVerify/Steps/DuplicateFinderStep.cs index f74721e..47251c8 100644 --- a/src/ModVerify/Steps/DuplicateFinderStep.cs +++ b/src/ModVerify/Steps/DuplicateFinderStep.cs @@ -42,7 +42,7 @@ private string CreateDuplicateErrorMessage(string context, ReadOnlyFrugalList var message = $"{context} '{firstEntry.Name}' ({firstEntry.Crc32}) has duplicate definitions: "; foreach (var entry in entries) - message += $"['{entry.Name}' in {entry.Location.XmlFile}] "; + message += $"['{entry.Name}' in {entry.Location.XmlFile}:{entry.Location.Line}] "; return message.TrimEnd(); } From fe83a417a947adb2723328898e8d301aadeffaa5 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sat, 15 Jun 2024 21:23:08 +0200 Subject: [PATCH 09/25] add text compile app --- .../PG.StarWarsGame.Engine/Language/GameLanguageManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManager.cs index d4111bc..9f2351a 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.Extensions.Logging; using PG.Commons.Services; using PG.StarWarsGame.Engine.Utilities; From 7e1ef0fb490d2a3cb5dadbbda0d17c0df67f5e39 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sun, 16 Jun 2024 14:31:39 +0200 Subject: [PATCH 10/25] add method to get language from file name --- .../Language/GameLanguageManager.cs | 18 ++++++++++-------- .../Language/IGameLanguageManager.cs | 2 ++ .../PG.StarWarsGame.Engine/PGConstants.cs | 2 ++ .../Repositories/GameRepository.cs | 4 +++- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManager.cs index 9f2351a..bbed871 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManager.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using PG.Commons.Services; using PG.StarWarsGame.Engine.Utilities; @@ -59,6 +58,13 @@ internal sealed class GameLanguageManager(IServiceProvider serviceProvider) : Se }; + public bool TryGetLanguage(string languageName, out LanguageType language) + { + language = LanguageType.English; + return Enum.TryParse(languageName, true, out language); + } + + public bool IsFileNameLocalizable(string fileName, bool requiredEnglishName) { var fileSpan = fileName.AsSpan(); @@ -96,7 +102,7 @@ public bool IsFileNameLocalizable(string fileName, bool requiredEnglishName) public string LocalizeFileName(string fileName, LanguageType language, out bool localized) { - if (fileName.Length > 260) + if (fileName.Length > PGConstants.MaxPathLength) throw new ArgumentOutOfRangeException(nameof(fileName), "fileName is too long"); localized = true; @@ -113,9 +119,7 @@ public string LocalizeFileName(string fileName, LanguageType language, out bool // The game only localizes file names iff they have the english suffix // NB: Also note that the engine does *not* check whether the filename actually ends with this suffix - // but instead only take the first occurrence. This means that a file name like - // 'test_eng.wav_ger.wav' - // will trick the algorithm. + // but instead only take the first occurrence. This means that a file name like 'test_eng.wav_ger.wav' will trick the algorithm. var engSuffixIndex = fileSpan.IndexOf("_ENG.WAV".AsSpan(), StringComparison.OrdinalIgnoreCase); if (engSuffixIndex != -1) isWav = true; @@ -145,9 +149,7 @@ public string LocalizeFileName(string fileName, LanguageType language, out bool else throw new InvalidOperationException(); - // 260 is roughly the MAX file path size on default Windows, - // so we don't expect game files to be larger. - var sb = new ValueStringBuilder(stackalloc char[260]); + var sb = new ValueStringBuilder(stackalloc char[PGConstants.MaxPathLength]); sb.Append(withoutSuffix); sb.Append(newLocalizedSuffix); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/IGameLanguageManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/IGameLanguageManager.cs index d061c95..d335112 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/IGameLanguageManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/IGameLanguageManager.cs @@ -8,6 +8,8 @@ public interface IGameLanguageManager IReadOnlyCollection EawSupportedLanguages { get; } + bool TryGetLanguage(string languageName, out LanguageType language); + string LocalizeFileName(string fileName, LanguageType language, out bool localized); bool IsFileNameLocalizable(string fileName, bool requireEnglishName); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PGConstants.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/PGConstants.cs index a04aaa4..486aa40 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PGConstants.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PGConstants.cs @@ -5,4 +5,6 @@ namespace PG.StarWarsGame.Engine; public static class PGConstants { public static readonly Encoding PGCrc32Encoding = Encoding.ASCII; + + public const int MaxPathLength = 260; } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs index 55efd5e..30d648d 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs @@ -249,7 +249,9 @@ internal void Seal() protected MegDataEntryReference? FindFileInMasterMeg(string filePath) { - Span fileNameBuffer = stackalloc char[260]; + Span fileNameBuffer = stackalloc char[PGConstants.MaxPathLength]; + + // TODO: Is the engine really "to-uppering" the input??? var length = _megPathNormalizer.Normalize(filePath.AsSpan(), fileNameBuffer); var fileName = fileNameBuffer.Slice(0, length); var crc = _crc32HashingService.GetCrc32(fileName, PGConstants.PGCrc32Encoding); From 529516e336190e063b20512a85056db70f58088c Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sun, 16 Jun 2024 14:32:10 +0200 Subject: [PATCH 11/25] start extracting code out of launcher app --- .../PG.StarWarsGame.Engine/Language/GameLanguageManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManager.cs index bbed871..851cace 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManager.cs @@ -6,8 +6,9 @@ namespace PG.StarWarsGame.Engine.Language; +// TODO: Manager for each game internal sealed class GameLanguageManager(IServiceProvider serviceProvider) : ServiceBase(serviceProvider), IGameLanguageManager -{ +{ private static readonly IDictionary LanguageToFileSuffixMapMp3 = new Dictionary { @@ -63,8 +64,7 @@ public bool TryGetLanguage(string languageName, out LanguageType language) language = LanguageType.English; return Enum.TryParse(languageName, true, out language); } - - + public bool IsFileNameLocalizable(string fileName, bool requiredEnglishName) { var fileSpan = fileName.AsSpan(); From 6481d43f9bc51460601841c88d1682cf0254031d Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sun, 16 Jun 2024 20:38:13 +0200 Subject: [PATCH 12/25] rename class --- src/ModVerify.CliApp/Program.cs | 4 ++-- src/ModVerify/IVerificationProvider.cs | 2 +- .../{VerificationSettings.cs => ModVerifySettings.cs} | 4 ++-- src/ModVerify/Steps/DuplicateFinderStep.cs | 2 +- src/ModVerify/Steps/GameVerificationStep.cs | 4 ++-- src/ModVerify/Steps/VerifyReferencedModelsStep.cs | 2 +- src/ModVerify/VerificationProvider.cs | 2 +- src/ModVerify/VerifyGamePipeline.cs | 4 ++-- 8 files changed, 12 insertions(+), 12 deletions(-) rename src/ModVerify/{VerificationSettings.cs => ModVerifySettings.cs} (67%) diff --git a/src/ModVerify.CliApp/Program.cs b/src/ModVerify.CliApp/Program.cs index 95f4228..51045e8 100644 --- a/src/ModVerify.CliApp/Program.cs +++ b/src/ModVerify.CliApp/Program.cs @@ -118,7 +118,7 @@ private static VerifyGamePipeline BuildPipeline(IPlayableObject playableObject, fallbackGame.Directory.FullName); - return new ModVerifyPipeline(GameEngineType.Foc, gameLocations, VerificationSettings.Default, _services); + return new ModVerifyPipeline(GameEngineType.Foc, gameLocations, ModVerifySettings.Default, _services); } private static IServiceProvider CreateAppServices() @@ -181,7 +181,7 @@ internal class CliOptions internal class ModVerifyPipeline( GameEngineType targetType, GameLocations gameLocations, - VerificationSettings settings, + ModVerifySettings settings, IServiceProvider serviceProvider) : VerifyGamePipeline(targetType, gameLocations, settings, serviceProvider) { diff --git a/src/ModVerify/IVerificationProvider.cs b/src/ModVerify/IVerificationProvider.cs index fb2779b..3e4d0bc 100644 --- a/src/ModVerify/IVerificationProvider.cs +++ b/src/ModVerify/IVerificationProvider.cs @@ -6,5 +6,5 @@ namespace AET.ModVerify; public interface IVerificationProvider { - IEnumerable GetAllDefaultVerifiers(IGameDatabase database, VerificationSettings settings); + IEnumerable GetAllDefaultVerifiers(IGameDatabase database, ModVerifySettings settings); } \ No newline at end of file diff --git a/src/ModVerify/VerificationSettings.cs b/src/ModVerify/ModVerifySettings.cs similarity index 67% rename from src/ModVerify/VerificationSettings.cs rename to src/ModVerify/ModVerifySettings.cs index e98c5b4..f639c0d 100644 --- a/src/ModVerify/VerificationSettings.cs +++ b/src/ModVerify/ModVerifySettings.cs @@ -1,10 +1,10 @@ namespace AET.ModVerify; -public record VerificationSettings +public record ModVerifySettings { public int ParallelWorkers { get; init; } = 4; - public static readonly VerificationSettings Default = new() + public static readonly ModVerifySettings Default = new() { ThrowBehavior = VerifyThrowBehavior.None }; diff --git a/src/ModVerify/Steps/DuplicateFinderStep.cs b/src/ModVerify/Steps/DuplicateFinderStep.cs index 47251c8..8010eee 100644 --- a/src/ModVerify/Steps/DuplicateFinderStep.cs +++ b/src/ModVerify/Steps/DuplicateFinderStep.cs @@ -9,7 +9,7 @@ namespace AET.ModVerify.Steps; public sealed class DuplicateFinderStep( IGameDatabase gameDatabase, - VerificationSettings settings, + ModVerifySettings settings, IServiceProvider serviceProvider) : GameVerificationStep(gameDatabase, settings, serviceProvider) { diff --git a/src/ModVerify/Steps/GameVerificationStep.cs b/src/ModVerify/Steps/GameVerificationStep.cs index 065924a..de82d99 100644 --- a/src/ModVerify/Steps/GameVerificationStep.cs +++ b/src/ModVerify/Steps/GameVerificationStep.cs @@ -13,7 +13,7 @@ namespace AET.ModVerify.Steps; public abstract class GameVerificationStep( IGameDatabase gameDatabase, - VerificationSettings settings, + ModVerifySettings settings, IServiceProvider serviceProvider) : PipelineStep(serviceProvider) { @@ -24,7 +24,7 @@ public abstract class GameVerificationStep( public IReadOnlyCollection VerifyErrors => _verifyErrors; - protected VerificationSettings Settings { get; } = settings; + protected ModVerifySettings Settings { get; } = settings; protected IGameDatabase Database { get; } = gameDatabase ?? throw new ArgumentNullException(nameof(gameDatabase)); diff --git a/src/ModVerify/Steps/VerifyReferencedModelsStep.cs b/src/ModVerify/Steps/VerifyReferencedModelsStep.cs index a43d244..9f4609d 100644 --- a/src/ModVerify/Steps/VerifyReferencedModelsStep.cs +++ b/src/ModVerify/Steps/VerifyReferencedModelsStep.cs @@ -18,7 +18,7 @@ namespace AET.ModVerify.Steps; public sealed class VerifyReferencedModelsStep( IGameDatabase database, - VerificationSettings settings, + ModVerifySettings settings, IServiceProvider serviceProvider) : GameVerificationStep(database, settings, serviceProvider) { diff --git a/src/ModVerify/VerificationProvider.cs b/src/ModVerify/VerificationProvider.cs index 2a5c9d8..8330729 100644 --- a/src/ModVerify/VerificationProvider.cs +++ b/src/ModVerify/VerificationProvider.cs @@ -7,7 +7,7 @@ namespace AET.ModVerify; internal class VerificationProvider(IServiceProvider serviceProvider) : IVerificationProvider { - public IEnumerable GetAllDefaultVerifiers(IGameDatabase database, VerificationSettings settings) + public IEnumerable GetAllDefaultVerifiers(IGameDatabase database, ModVerifySettings settings) { yield return new VerifyReferencedModelsStep(database, settings, serviceProvider); yield return new DuplicateFinderStep(database, settings, serviceProvider); diff --git a/src/ModVerify/VerifyGamePipeline.cs b/src/ModVerify/VerifyGamePipeline.cs index 4d1d60a..1ba2407 100644 --- a/src/ModVerify/VerifyGamePipeline.cs +++ b/src/ModVerify/VerifyGamePipeline.cs @@ -20,9 +20,9 @@ public abstract class VerifyGamePipeline : Pipeline private readonly GameLocations _gameLocations; private readonly ParallelRunner _verifyRunner; - protected VerificationSettings Settings { get; } + protected ModVerifySettings Settings { get; } - protected VerifyGamePipeline(GameEngineType targetType, GameLocations gameLocations, VerificationSettings settings, IServiceProvider serviceProvider) + protected VerifyGamePipeline(GameEngineType targetType, GameLocations gameLocations, ModVerifySettings settings, IServiceProvider serviceProvider) : base(serviceProvider) { _targetType = targetType; From b12a78dff9e7e236047eedad77b1ced03d0cd529 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sat, 22 Jun 2024 12:05:33 +0200 Subject: [PATCH 13/25] implement logging for xml parsers --- src/ModVerify.CliApp/ModVerify.CliApp.csproj | 2 + src/ModVerify.CliApp/Program.cs | 42 ++++++++++++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/ModVerify.CliApp/ModVerify.CliApp.csproj b/src/ModVerify.CliApp/ModVerify.CliApp.csproj index a76e648..7ada303 100644 --- a/src/ModVerify.CliApp/ModVerify.CliApp.csproj +++ b/src/ModVerify.CliApp/ModVerify.CliApp.csproj @@ -17,6 +17,7 @@ + @@ -33,6 +34,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/ModVerify.CliApp/Program.cs b/src/ModVerify.CliApp/Program.cs index 51045e8..29f0c7c 100644 --- a/src/ModVerify.CliApp/Program.cs +++ b/src/ModVerify.CliApp/Program.cs @@ -3,11 +3,13 @@ using System.IO; using System.IO.Abstractions; using System.Linq; +using System.Net.Http.Headers; using System.Runtime.CompilerServices; using System.Threading.Tasks; using AET.ModVerify; using AET.ModVerify.Steps; using AET.SteamAbstraction; +using AnakinRaW.CommonUtilities.FileSystem; using AnakinRaW.CommonUtilities.Hashing; using AnakinRaW.CommonUtilities.Registry; using AnakinRaW.CommonUtilities.Registry.Windows; @@ -15,6 +17,7 @@ using EawModinfo.Spec; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Console; using PG.Commons.Extensibility; using PG.StarWarsGame.Engine; using PG.StarWarsGame.Engine.Database; @@ -26,6 +29,8 @@ using PG.StarWarsGame.Infrastructure.Games; using PG.StarWarsGame.Infrastructure.Mods; using PG.StarWarsGame.Infrastructure.Services.Dependencies; +using Serilog; +using Serilog.Filters; namespace ModVerify.CliApp; @@ -125,13 +130,13 @@ private static IServiceProvider CreateAppServices() { var fileSystem = new FileSystem(); var serviceCollection = new ServiceCollection(); - - serviceCollection.AddLogging(ConfigureLogging); - + serviceCollection.AddSingleton(new WindowsRegistry()); serviceCollection.AddSingleton(sp => new HashingService(sp)); serviceCollection.AddSingleton(fileSystem); + serviceCollection.AddLogging(builder => ConfigureLogging(builder, fileSystem)); + SteamAbstractionLayer.InitializeServices(serviceCollection); PetroglyphGameClients.InitializeServices(serviceCollection); PetroglyphGameInfrastructure.InitializeServices(serviceCollection); @@ -147,7 +152,7 @@ private static IServiceProvider CreateAppServices() return serviceCollection.BuildServiceProvider(); } - private static void ConfigureLogging(ILoggingBuilder loggingBuilder) + private static void ConfigureLogging(ILoggingBuilder loggingBuilder, IFileSystem fileSystem) { loggingBuilder.ClearProviders(); @@ -165,6 +170,35 @@ private static void ConfigureLogging(ILoggingBuilder loggingBuilder) #endif loggingBuilder.AddConsole(); loggingBuilder.SetMinimumLevel(logLevel); + + SetupXmlParseLogging(loggingBuilder, fileSystem); + } + + private static void SetupXmlParseLogging(ILoggingBuilder loggingBuilder, IFileSystem fileSystem) + { + const string xmlParseLogFileName = "XmlParseLog.txt"; + const string parserNamespace = nameof(PG.StarWarsGame.Engine.Xml.Parsers); + + fileSystem.File.TryDeleteWithRetry(xmlParseLogFileName); + + var logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .MinimumLevel.Warning() + .Filter.ByIncludingOnly(Matching.FromSource(parserNamespace)) + .WriteTo.File(xmlParseLogFileName, outputTemplate: "[{Level:u3}] [{SourceContext}] {Message}{NewLine}{Exception}") + .CreateLogger(); + + loggingBuilder.AddSerilog(logger); + + loggingBuilder.AddFilter((category, level) => + { + if (string.IsNullOrEmpty(category)) + return false; + if (category.StartsWith(parserNamespace)) + return false; + + return true; + }); } From 6d59c164a6914e2c1afb9ad81c3252f5ad2103b8 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sat, 22 Jun 2024 12:54:44 +0200 Subject: [PATCH 14/25] start reorganize xml parsers --- .../GameDatabaseCreationPipeline.cs | 3 +- .../Xml/Parsers/Data/GameObjectParser.cs | 29 ++---------- .../Xml/Parsers/Data/SfxEventParser.cs | 26 +++++------ .../Xml/Parsers/XmlObjectParser.cs | 45 +++++++++++++++++++ .../Xml/PetroglyphXmlParserFactory.cs | 1 - .../Parsers/PetroglyphXmlElementParser.cs | 42 +---------------- .../Parsers/PetroglyphXmlFileParser.cs | 4 +- .../Parsers/PetroglyphXmlParser.cs | 16 +++++++ .../Parsers/PetroglyphXmlPrimitiveParser.cs | 14 ++++++ .../CommaSeparatedStringKeyValueListParser.cs | 20 ++++----- .../Primitives/PetroglyphXmlStringParser.cs | 14 ++---- .../Primitives/XmlFileContainerParser.cs | 22 ++++----- 12 files changed, 119 insertions(+), 117 deletions(-) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlPrimitiveParser.cs diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs index 642461e..0bb21db 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs @@ -27,7 +27,8 @@ internal class GameDatabaseCreationPipeline(GameRepository repository, IServiceP protected override Task PrepareCoreAsync() { _parseXmlRunner = new ParallelRunner(4, ServiceProvider); - foreach (var xmlParserStep in CreateXmlParserSteps()) _parseXmlRunner.AddStep(xmlParserStep); + foreach (var xmlParserStep in CreateXmlParserSteps()) + _parseXmlRunner.AddStep(xmlParserStep); return Task.FromResult(true); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs index 5e07b24..f8325b8 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs @@ -4,39 +4,18 @@ using PG.Commons.Hashing; using PG.StarWarsGame.Engine.DataTypes; using PG.StarWarsGame.Files.XML; -using PG.StarWarsGame.Files.XML.Parsers; -using PG.StarWarsGame.Files.XML.Parsers.Primitives; namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; -public sealed class GameObjectParser(IServiceProvider serviceProvider) : PetroglyphXmlElementParser(serviceProvider) +public sealed class GameObjectParser(IServiceProvider serviceProvider) : XmlObjectParser(serviceProvider) { private readonly ICrc32HashingService _crc32Hashing = serviceProvider.GetRequiredService(); - protected override IPetroglyphXmlElementParser? GetParser(string tag) + protected override bool IsTagSupported(string tag) { - switch (tag) - { - case "Land_Terrain_Model_Mapping": - return new CommaSeparatedStringKeyValueListParser(ServiceProvider); - case "Galactic_Model_Name": - case "Destroyed_Galactic_Model_Name": - case "Land_Model_Name": - case "Space_Model_Name": - case "Model_Name": - case "Tactical_Model_Name": - case "Galactic_Fleet_Override_Model_Name": - case "GUI_Model_Name": - case "GUI_Model": - case "Land_Model_Anim_Override_Name": - case "xxxSpace_Model_Name": - case "Damaged_Smoke_Asset_Name": - return PetroglyphXmlStringParser.Instance; - default: - return null; - } + throw new NotImplementedException(); } - + public override GameObject Parse(XElement element) { throw new NotSupportedException(); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs index 4ec2f0d..46a85c7 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs @@ -1,31 +1,18 @@ using System; using System.Xml.Linq; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.DataTypes; using PG.StarWarsGame.Files.XML; -using PG.StarWarsGame.Files.XML.Parsers; namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; -public sealed class SfxEventParser(IServiceProvider serviceProvider) : PetroglyphXmlElementParser(serviceProvider) +public sealed class SfxEventParser(IServiceProvider serviceProvider) : XmlObjectParser(serviceProvider) { private readonly ICrc32HashingService _hashingService = serviceProvider.GetRequiredService(); - protected override IPetroglyphXmlElementParser? GetParser(string tag) - { - return null; - //switch (tag) - //{ - // case "Land_Terrain_Model_Mapping": - // return new CommaSeparatedStringKeyValueListParser(ServiceProvider); - // case "Galactic_Model_Name": - // case "Damaged_Smoke_Asset_Name": - // return PetroglyphXmlStringParser.Instance; - // default: - // return null; - //} - } + private readonly ILogger? _logger = serviceProvider.GetService()?.CreateLogger(typeof(SfxEventParser)); public override SfxEvent Parse( XElement element, @@ -42,7 +29,10 @@ public override SfxEvent Parse( var tagName = child.Name.LocalName; var parser = GetParser(tagName); if (parser is null) + { + //_logger?.LogWarning($"Unable to find parser for tag '{tagName}' in element '{name}'"); continue; + } if (tagName.Equals("Use_Preset")) { @@ -70,4 +60,8 @@ public override SfxEvent Parse(XElement element) throw new NotSupportedException(); } + protected override bool IsTagSupported(string tag) + { + throw new NotImplementedException(); + } } \ 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 new file mode 100644 index 0000000..cd2a202 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs @@ -0,0 +1,45 @@ +using System; +using System.Xml.Linq; +using PG.StarWarsGame.Engine.DataTypes; +using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Parsers; + +namespace PG.StarWarsGame.Engine.Xml.Parsers; + +public abstract class XmlObjectParser(IServiceProvider serviceProvider) : PetroglyphXmlElementParser where T : XmlObject +{ + protected IServiceProvider ServiceProvider { get; } = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + + protected abstract bool IsTagSupported(string tag); + + protected IPetroglyphXmlElementParser? GetParser(string tag) + { + if (!IsTagSupported(tag)) + return null; + + return null; + } + + protected ValueListDictionary ToKeyValuePairList(XElement element) + { + var keyValuePairList = new ValueListDictionary(); + foreach (var elm in element.Elements()) + { + var tagName = elm.Name.LocalName; + + var parser = GetParser(tagName); + + if (parser is not null) + { + var value = parser.Parse(elm); + keyValuePairList.Add(tagName, value); + } + } + return keyValuePairList; + } +} + +internal interface IXmlParserFactory +{ + +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs index 10af5a0..c780ec1 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using PG.StarWarsGame.Engine.DataTypes; using PG.StarWarsGame.Engine.Xml.Parsers.Data; using PG.StarWarsGame.Engine.Xml.Parsers.File; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlElementParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlElementParser.cs index b67fc88..14d3ce5 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlElementParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlElementParser.cs @@ -1,52 +1,14 @@ -using System; -using System.Linq; +using System.Linq; using System.Xml.Linq; -using PG.Commons.Hashing; -using PG.StarWarsGame.Files.XML.Parsers.Primitives; namespace PG.StarWarsGame.Files.XML.Parsers; -public abstract class PetroglyphXmlElementParser(IServiceProvider serviceProvider) : IPetroglyphXmlElementParser +public abstract class PetroglyphXmlElementParser : PetroglyphXmlParser { - protected IServiceProvider ServiceProvider { get; } = serviceProvider; - - - protected virtual IPetroglyphXmlElementParser? GetParser(string tag) - { - return PetroglyphXmlStringParser.Instance; - } - - public abstract T Parse(XElement element); - - public abstract T Parse(XElement element, IReadOnlyValueListDictionary parsedElements, out Crc32 nameCrc); - - public ValueListDictionary ToKeyValuePairList(XElement element) - { - var keyValuePairList = new ValueListDictionary(); - foreach (var elm in element.Elements()) - { - var tagName = elm.Name.LocalName; - - var parser = GetParser(tagName); - - if (parser is not null) - { - var value = parser.Parse(elm); - keyValuePairList.Add(tagName, value); - } - } - return keyValuePairList; - } - protected string GetNameAttributeValue(XElement element) { var nameAttribute = element.Attributes() .FirstOrDefault(a => a.Name.LocalName == "Name"); return nameAttribute is null ? string.Empty : nameAttribute.Value; } - - object? IPetroglyphXmlElementParser.Parse(XElement element) - { - return Parse(element); - } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs index c721f5a..ab44289 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs @@ -8,8 +8,10 @@ namespace PG.StarWarsGame.Files.XML.Parsers; public abstract class PetroglyphXmlFileParser(IServiceProvider serviceProvider) - : PetroglyphXmlElementParser(serviceProvider), IPetroglyphXmlFileParser + : PetroglyphXmlParser, IPetroglyphXmlFileParser { + protected IServiceProvider ServiceProvider { get; } = serviceProvider; + protected virtual bool LoadLineInfo => false; public T ParseFile(Stream xmlStream) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlParser.cs new file mode 100644 index 0000000..2032cfb --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlParser.cs @@ -0,0 +1,16 @@ +using System.Xml.Linq; +using PG.Commons.Hashing; + +namespace PG.StarWarsGame.Files.XML.Parsers; + +public abstract class PetroglyphXmlParser : IPetroglyphXmlElementParser +{ + public abstract T Parse(XElement element, IReadOnlyValueListDictionary parsedElements, out Crc32 nameCrc); + + public abstract T Parse(XElement element); + + object IPetroglyphXmlElementParser.Parse(XElement element) + { + return Parse(element); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlPrimitiveParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlPrimitiveParser.cs new file mode 100644 index 0000000..5af70bd --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlPrimitiveParser.cs @@ -0,0 +1,14 @@ +using System; +using System.Xml.Linq; +using PG.Commons.Hashing; + +namespace PG.StarWarsGame.Files.XML.Parsers; + +public abstract class PetroglyphXmlPrimitiveParser : PetroglyphXmlParser +{ + public sealed override T Parse(XElement element, IReadOnlyValueListDictionary parsedElements, + out Crc32 nameCrc) + { + throw new NotSupportedException(); + } +} \ 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 e0de592..307e7b0 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/CommaSeparatedStringKeyValueListParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/CommaSeparatedStringKeyValueListParser.cs @@ -1,16 +1,19 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Xml.Linq; -using PG.Commons.Hashing; namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; // Used e.g, by // Format: Key, Value, Key, Value // There might be arbitrary spaces, tabs and newlines -public sealed class CommaSeparatedStringKeyValueListParser(IServiceProvider serviceProvider) - : PetroglyphXmlElementParser>(serviceProvider) +public sealed class CommaSeparatedStringKeyValueListParser : PetroglyphXmlPrimitiveParser> { + public static readonly CommaSeparatedStringKeyValueListParser Instance = new(); + + private CommaSeparatedStringKeyValueListParser() + { + } + public override IList<(string key, string value)> Parse(XElement element) { var values = element.Value.Split(','); @@ -21,7 +24,7 @@ public sealed class CommaSeparatedStringKeyValueListParser(IServiceProvider serv var keyValueList = new List<(string key, string value)>(values.Length + 1 / 2); - for (int i = 0; i < values.Length; i += 2) + for (var i = 0; i < values.Length; i += 2) { // Case: Incomplete key-value pair if (values.Length - 1 < i + 1) @@ -35,9 +38,4 @@ public sealed class CommaSeparatedStringKeyValueListParser(IServiceProvider serv return keyValueList; } - - public override IList<(string key, string value)> Parse(XElement element, IReadOnlyValueListDictionary> parsedElements, out Crc32 nameCrc) - { - throw new NotSupportedException(); - } } \ 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 5f53e49..90e72c0 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlStringParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlStringParser.cs @@ -1,15 +1,12 @@ -using System; -using System.Xml.Linq; -using PG.Commons.Hashing; +using System.Xml.Linq; namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; -public sealed class PetroglyphXmlStringParser(IServiceProvider serviceProvider) - : PetroglyphXmlElementParser(serviceProvider) +public sealed class PetroglyphXmlStringParser : PetroglyphXmlPrimitiveParser { public static readonly PetroglyphXmlStringParser Instance = new(); - private PetroglyphXmlStringParser() : this(null!) + private PetroglyphXmlStringParser() { } @@ -17,9 +14,4 @@ public override string Parse(XElement element) { return element.Value.Trim(); } - - public override string Parse(XElement element, IReadOnlyValueListDictionary parsedElements, out Crc32 nameCrc) - { - throw new NotSupportedException(); - } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/XmlFileContainerParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/XmlFileContainerParser.cs index b2954ab..bda0df3 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/XmlFileContainerParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/XmlFileContainerParser.cs @@ -1,5 +1,5 @@ using System; -using System.Linq; +using System.Collections.Generic; using System.Xml.Linq; using PG.Commons.Hashing; @@ -16,15 +16,15 @@ protected override void Parse(XElement element, IValueListDictionary().ToList()) - : new XmlFileContainer([]); - } - protected override IPetroglyphXmlElementParser? GetParser(string tag) - { - return tag == "File" ? PetroglyphXmlStringParser.Instance : null; + var files = new List(); + foreach (var child in element.Elements()) + { + if (child.Name == "File") + { + var file = PetroglyphXmlStringParser.Instance.Parse(child); + files.Add(file); + } + } + return new XmlFileContainer(files); } - } \ No newline at end of file From dcce6453f8130185d4f71bbc69855d293d54a856 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sat, 22 Jun 2024 14:07:49 +0200 Subject: [PATCH 15/25] fix logging --- src/ModVerify.CliApp/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ModVerify.CliApp/Program.cs b/src/ModVerify.CliApp/Program.cs index 29f0c7c..8117b35 100644 --- a/src/ModVerify.CliApp/Program.cs +++ b/src/ModVerify.CliApp/Program.cs @@ -177,7 +177,7 @@ private static void ConfigureLogging(ILoggingBuilder loggingBuilder, IFileSystem private static void SetupXmlParseLogging(ILoggingBuilder loggingBuilder, IFileSystem fileSystem) { const string xmlParseLogFileName = "XmlParseLog.txt"; - const string parserNamespace = nameof(PG.StarWarsGame.Engine.Xml.Parsers); + const string parserNamespace = "PG.StarWarsGame.Engine.Xml.Parsers"; fileSystem.File.TryDeleteWithRetry(xmlParseLogFileName); From 64966e5943f96a8bf1cbfc87f626ee9c9f81c273 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sat, 22 Jun 2024 14:18:59 +0200 Subject: [PATCH 16/25] reorganize xml parsing --- .../Xml/IPetroglyphXmlFileParserFactory.cs | 5 +-- .../Xml/ParserNotFoundException.cs | 18 +++++++++ .../Xml/Parsers/Data/GameObjectParser.cs | 40 ++++++++++++------- .../Xml/Parsers/Data/SfxEventParser.cs | 19 ++++----- .../Xml/Parsers/XmlObjectParser.cs | 27 +++++++------ .../Xml/PetroglyphXmlParserFactory.cs | 4 +- .../Parsers/IPetroglyphXmlElementParser.cs | 17 ++------ .../Parsers/IPetroglyphXmlFileParser.cs | 4 +- .../Parsers/IPetroglyphXmlParser.cs | 13 ++++++ .../Parsers/PetroglyphXmlElementParser.cs | 8 ++++ .../Parsers/PetroglyphXmlFileParser.cs | 8 +--- .../Parsers/PetroglyphXmlParser.cs | 7 +--- .../PetroglyphXmlPrimitiveElementParser.cs | 3 ++ .../Parsers/PetroglyphXmlPrimitiveParser.cs | 14 ------- .../CommaSeparatedStringKeyValueListParser.cs | 2 +- .../Primitives/PetroglyphXmlStringParser.cs | 2 +- 16 files changed, 101 insertions(+), 90 deletions(-) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/ParserNotFoundException.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlPrimitiveElementParser.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlPrimitiveParser.cs diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/IPetroglyphXmlFileParserFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/IPetroglyphXmlFileParserFactory.cs index ac389ae..e70c889 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/IPetroglyphXmlFileParserFactory.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/IPetroglyphXmlFileParserFactory.cs @@ -1,11 +1,8 @@ -using System; -using PG.StarWarsGame.Files.XML.Parsers; +using PG.StarWarsGame.Files.XML.Parsers; namespace PG.StarWarsGame.Engine.Xml; public interface IPetroglyphXmlFileParserFactory { IPetroglyphXmlFileParser GetFileParser(); - - IPetroglyphXmlFileParser GetFileParser(Type type); } \ 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 new file mode 100644 index 0000000..19bc797 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/ParserNotFoundException.cs @@ -0,0 +1,18 @@ +using System; + +namespace PG.StarWarsGame.Engine.Xml; + +public sealed class ParserNotFoundException : Exception +{ + public override string Message { get; } + + public ParserNotFoundException(Type type) + { + Message = $"The parser for the type {type} was not found."; + } + + public ParserNotFoundException(string tag) + { + Message = $"The parser for the tag {tag} was not found."; + } +} \ 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 index f8325b8..bd93f11 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs @@ -1,31 +1,44 @@ using System; using System.Xml.Linq; -using Microsoft.Extensions.DependencyInjection; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.DataTypes; using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Parsers; +using PG.StarWarsGame.Files.XML.Parsers.Primitives; namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; public sealed class GameObjectParser(IServiceProvider serviceProvider) : XmlObjectParser(serviceProvider) { - private readonly ICrc32HashingService _crc32Hashing = serviceProvider.GetRequiredService(); - - protected override bool IsTagSupported(string tag) - { - throw new NotImplementedException(); - } - - public override GameObject Parse(XElement element) + protected override IPetroglyphXmlElementParser? GetParser(string tag) { - throw new NotSupportedException(); + switch (tag) + { + case "Land_Terrain_Model_Mapping": + return CommaSeparatedStringKeyValueListParser.Instance; + case "Galactic_Model_Name": + case "Destroyed_Galactic_Model_Name": + case "Land_Model_Name": + case "Space_Model_Name": + case "Model_Name": + case "Tactical_Model_Name": + case "Galactic_Fleet_Override_Model_Name": + case "GUI_Model_Name": + case "GUI_Model": + case "Land_Model_Anim_Override_Name": + case "xxxSpace_Model_Name": + case "Damaged_Smoke_Asset_Name": + return PetroglyphXmlStringParser.Instance; + default: + return null; + } } public override GameObject Parse(XElement element, IReadOnlyValueListDictionary parsedElements, out Crc32 nameCrc) { var properties = ToKeyValuePairList(element); var name = GetNameAttributeValue(element); - nameCrc = _crc32Hashing.GetCrc32Upper(name.AsSpan(), PGConstants.PGCrc32Encoding); + nameCrc = HashingService.GetCrc32Upper(name.AsSpan(), PGConstants.PGCrc32Encoding); var type = GetTagName(element); var objectType = EstimateType(type); var gameObject = new GameObject(type, name, nameCrc, objectType, properties, XmlLocationInfo.FromElement(element)); @@ -83,8 +96,5 @@ private static GameObjectType EstimateType(string tagName) }; } - public string GetTagName(XElement element) - { - return element.Name.LocalName; - } + 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 index 46a85c7..ad00da6 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs @@ -1,18 +1,18 @@ using System; using System.Xml.Linq; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.DataTypes; using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.Parsers; namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; public sealed class SfxEventParser(IServiceProvider serviceProvider) : XmlObjectParser(serviceProvider) { - private readonly ICrc32HashingService _hashingService = serviceProvider.GetRequiredService(); - - private readonly ILogger? _logger = serviceProvider.GetService()?.CreateLogger(typeof(SfxEventParser)); + protected override IPetroglyphXmlElementParser? GetParser(string tag) + { + return null; + } public override SfxEvent Parse( XElement element, @@ -20,7 +20,7 @@ public override SfxEvent Parse( out Crc32 nameCrc) { var name = GetNameAttributeValue(element); - nameCrc = _hashingService.GetCrc32Upper(name.AsSpan(), PGConstants.PGCrc32Encoding); + nameCrc = HashingService.GetCrc32Upper(name.AsSpan(), PGConstants.PGCrc32Encoding); var valueList = new ValueListDictionary(); @@ -37,7 +37,7 @@ public override SfxEvent Parse( if (tagName.Equals("Use_Preset")) { var presetName = parser.Parse(child) as string; - var presetNameCrc = _hashingService.GetCrc32Upper(presetName.AsSpan(), PGConstants.PGCrc32Encoding); + var presetNameCrc = HashingService.GetCrc32Upper(presetName.AsSpan(), PGConstants.PGCrc32Encoding); if (presetNameCrc == default || !parsedElements.TryGetFirstValue(presetNameCrc, out var preset)) { // Unable to find Preset @@ -59,9 +59,4 @@ public override SfxEvent Parse(XElement element) { throw new NotSupportedException(); } - - protected override bool IsTagSupported(string tag) - { - throw new NotImplementedException(); - } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs index cd2a202..239eb97 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs @@ -1,25 +1,31 @@ using System; using System.Xml.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PG.Commons.Hashing; using PG.StarWarsGame.Engine.DataTypes; using PG.StarWarsGame.Files.XML; using PG.StarWarsGame.Files.XML.Parsers; namespace PG.StarWarsGame.Engine.Xml.Parsers; -public abstract class XmlObjectParser(IServiceProvider serviceProvider) : PetroglyphXmlElementParser where T : XmlObject +public abstract class XmlObjectParser : PetroglyphXmlElementParser where T : XmlObject { - protected IServiceProvider ServiceProvider { get; } = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + protected IServiceProvider ServiceProvider { get; } - protected abstract bool IsTagSupported(string tag); + protected ILogger? Logger { get; } + + protected ICrc32HashingService HashingService { get; } - protected IPetroglyphXmlElementParser? GetParser(string tag) + protected XmlObjectParser(IServiceProvider serviceProvider) { - if (!IsTagSupported(tag)) - return null; - - return null; + ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + Logger = serviceProvider.GetService()?.CreateLogger(GetType()); + HashingService = serviceProvider.GetRequiredService(); } + protected abstract IPetroglyphXmlElementParser? GetParser(string tag); + protected ValueListDictionary ToKeyValuePairList(XElement element) { var keyValuePairList = new ValueListDictionary(); @@ -37,9 +43,4 @@ protected ValueListDictionary ToKeyValuePairList(XElement elemen } return keyValuePairList; } -} - -internal interface IXmlParserFactory -{ - } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs index c780ec1..0341eb6 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs @@ -15,7 +15,7 @@ public IPetroglyphXmlFileParser GetFileParser() return (IPetroglyphXmlFileParser)GetFileParser(typeof(T)); } - public IPetroglyphXmlFileParser GetFileParser(Type type) + private IPetroglyphXmlFileParser GetFileParser(Type type) { if (type == typeof(XmlFileContainer)) return new XmlFileContainerParser(serviceProvider); @@ -29,6 +29,6 @@ public IPetroglyphXmlFileParser GetFileParser(Type type) if (type == typeof(SfxEvent)) return new SfxEventFileParser(serviceProvider); - throw new NotImplementedException($"The parser for the type {type} is not yet implemented."); + throw new ParserNotFoundException(type); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlElementParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlElementParser.cs index 8fb31c6..e699b80 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlElementParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlElementParser.cs @@ -1,16 +1,5 @@ -using System.Xml.Linq; -using PG.Commons.Hashing; +namespace PG.StarWarsGame.Files.XML.Parsers; -namespace PG.StarWarsGame.Files.XML.Parsers; +public interface IPetroglyphXmlElementParser : IPetroglyphXmlParser; -public interface IPetroglyphXmlElementParser -{ - public object Parse(XElement element); -} - -public interface IPetroglyphXmlElementParser : IPetroglyphXmlElementParser -{ - public new T Parse(XElement element); - - public T Parse(XElement element, IReadOnlyValueListDictionary parsedElements, out Crc32 nameCrc); -} \ No newline at end of file +public interface IPetroglyphXmlElementParser : IPetroglyphXmlElementParser, IPetroglyphXmlParser; \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlFileParser.cs index 817a13c..2c81137 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlFileParser.cs @@ -3,12 +3,12 @@ namespace PG.StarWarsGame.Files.XML.Parsers; -public interface IPetroglyphXmlFileParser : IPetroglyphXmlElementParser +public interface IPetroglyphXmlFileParser : IPetroglyphXmlParser { public object? ParseFile(Stream stream); } -public interface IPetroglyphXmlFileParser : IPetroglyphXmlElementParser, IPetroglyphXmlFileParser +public interface IPetroglyphXmlFileParser : IPetroglyphXmlParser, IPetroglyphXmlFileParser { public new T ParseFile(Stream stream); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlParser.cs new file mode 100644 index 0000000..cd8a607 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlParser.cs @@ -0,0 +1,13 @@ +using System.Xml.Linq; + +namespace PG.StarWarsGame.Files.XML.Parsers; + +public interface IPetroglyphXmlParser +{ + public object Parse(XElement element); +} + +public interface IPetroglyphXmlParser : IPetroglyphXmlParser +{ + public new T Parse(XElement element); +} \ 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 index 14d3ce5..8ab4eee 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlElementParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlElementParser.cs @@ -1,14 +1,22 @@ using System.Linq; using System.Xml.Linq; +using PG.Commons.Hashing; namespace PG.StarWarsGame.Files.XML.Parsers; public abstract class PetroglyphXmlElementParser : PetroglyphXmlParser { + protected string GetTagName(XElement element) + { + return element.Name.LocalName; + } + protected string GetNameAttributeValue(XElement element) { var nameAttribute = element.Attributes() .FirstOrDefault(a => a.Name.LocalName == "Name"); return nameAttribute is null ? string.Empty : nameAttribute.Value; } + + public abstract T Parse(XElement element, IReadOnlyValueListDictionary parsedElements, out Crc32 nameCrc); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs index ab44289..828d49e 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs @@ -7,8 +7,7 @@ namespace PG.StarWarsGame.Files.XML.Parsers; -public abstract class PetroglyphXmlFileParser(IServiceProvider serviceProvider) - : PetroglyphXmlParser, IPetroglyphXmlFileParser +public abstract class PetroglyphXmlFileParser(IServiceProvider serviceProvider) : PetroglyphXmlParser, IPetroglyphXmlFileParser { protected IServiceProvider ServiceProvider { get; } = serviceProvider; @@ -27,11 +26,6 @@ public void ParseFile(Stream xmlStream, IValueListDictionary parsedEnt Parse(root, parsedEntries); } - public sealed override T Parse(XElement element, IReadOnlyValueListDictionary parsedElements, out Crc32 nameCrc) - { - throw new NotSupportedException(); - } - protected abstract void Parse(XElement element, IValueListDictionary parsedElements); private XElement? GetRootElement(Stream xmlStream) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlParser.cs index 2032cfb..c0181dd 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlParser.cs @@ -1,15 +1,12 @@ using System.Xml.Linq; -using PG.Commons.Hashing; namespace PG.StarWarsGame.Files.XML.Parsers; -public abstract class PetroglyphXmlParser : IPetroglyphXmlElementParser +public abstract class PetroglyphXmlParser : IPetroglyphXmlParser { - public abstract T Parse(XElement element, IReadOnlyValueListDictionary parsedElements, out Crc32 nameCrc); - public abstract T Parse(XElement element); - object IPetroglyphXmlElementParser.Parse(XElement element) + object IPetroglyphXmlParser.Parse(XElement element) { return Parse(element); } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlPrimitiveElementParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlPrimitiveElementParser.cs new file mode 100644 index 0000000..b481d17 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlPrimitiveElementParser.cs @@ -0,0 +1,3 @@ +namespace PG.StarWarsGame.Files.XML.Parsers; + +public abstract class PetroglyphXmlPrimitiveElementParser : PetroglyphXmlParser, IPetroglyphXmlElementParser; \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlPrimitiveParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlPrimitiveParser.cs deleted file mode 100644 index 5af70bd..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlPrimitiveParser.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Xml.Linq; -using PG.Commons.Hashing; - -namespace PG.StarWarsGame.Files.XML.Parsers; - -public abstract class PetroglyphXmlPrimitiveParser : PetroglyphXmlParser -{ - public sealed override T Parse(XElement element, IReadOnlyValueListDictionary parsedElements, - out Crc32 nameCrc) - { - throw new NotSupportedException(); - } -} \ 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 307e7b0..a300719 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/CommaSeparatedStringKeyValueListParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/CommaSeparatedStringKeyValueListParser.cs @@ -6,7 +6,7 @@ namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; // Used e.g, by // Format: Key, Value, Key, Value // There might be arbitrary spaces, tabs and newlines -public sealed class CommaSeparatedStringKeyValueListParser : PetroglyphXmlPrimitiveParser> +public sealed class CommaSeparatedStringKeyValueListParser : PetroglyphXmlPrimitiveElementParser> { public static readonly CommaSeparatedStringKeyValueListParser Instance = new(); 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 90e72c0..b5551ec 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlStringParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlStringParser.cs @@ -2,7 +2,7 @@ namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; -public sealed class PetroglyphXmlStringParser : PetroglyphXmlPrimitiveParser +public sealed class PetroglyphXmlStringParser : PetroglyphXmlPrimitiveElementParser { public static readonly PetroglyphXmlStringParser Instance = new(); From b7720021d718026aafdbd5a4e9f3e2a304b0d251 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sat, 22 Jun 2024 14:44:03 +0200 Subject: [PATCH 17/25] add icon --- aet.ico | Bin 0 -> 38078 bytes src/ModVerify.CliApp/FodyWeavers.xml | 4 +--- src/ModVerify.CliApp/ModVerify.CliApp.csproj | 9 +++++++++ 3 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 aet.ico diff --git a/aet.ico b/aet.ico new file mode 100644 index 0000000000000000000000000000000000000000..ba9d880f309fe001d7d86d65db6c83841fbea70a GIT binary patch literal 38078 zcmeI536NY>6^475z=SQsPJoyWs35B;Ff2l7u@F&8Q4vstWB?^lfED(zWhSyou_`GM zK@wRM5L3#MVow7?G$M+uia>`gfC|H!M%r$_zvrFF<>tNae!b1SkVE~s=bn4+_TT&N zHj|J_b?{$LPl`XsrLLQqO0Ablr9K5PrLsWwvRph6xPO*PrPOjP=V199SUv}a>l}FG zkw;Q<=gu8UbWneKh}#gm{|go@Xv*3?dGcg;%V@Vgh6?Wnm$tj&M(f*i&pngQ|Bi6* zGWY;&S_dB^>d4cyED7iT%MkG%D1oc%B;{K2tyE{;hO;G@|9$EFyFlD82AhC-et?{ z@3HAJ%=oXyy~dzbbKkf<8O6rh4-Ye^W`)Cq{}t%Z`4#$YJUoxjD$VM9L^)~fdP}uB z-BA1UJ`wrqG7l5}&x`1<+jr?rpMMdg-BqDIdD8ZZ$QRo@4EXP((LT>%an1d<+=-~? z@G5Q6oA=j2!!pGD_tAkcuET7nG~C(R3()&R)EZ=M)0?+`{?L#NA^$(1;ntUGp}zU` z8|rBM%(vFizW4FjFMiQWats0gy3@}D)i`(4U&deOKYjBPKpLz8TKTg>?XzwhqI7*H z2Y|X{2>6f7eN?|?*Zg}8JO>^I_kr8Mjo>P9CHN6I7w8^49q27jZ#_C^o(Oc;n-)NJ z`RHdcDyLH2Zkpa@4*{{I&HSHD1KoBnOS;BjO56cxBmcGBt&%<-8iVh~8G|Rd@uW+9LjSF1);r+%JfzE06u% zh&1D=mEk+oy*-GO7V-acDh%tF)<(Tyy#;ipe+fJV#NT{yKe!p_XMZ|FUIS(Uoh4@j zohPpc{{&Y48|V|AxW4@}K^m+8Bx?h`DahCIDbyUD2krn50k!8};60$Wy1w=5&r(k0 z?{>9-|COk3Td+0Q1nAzn4j2WD57dU_267u8+FvqGywcA~G)@|S%X=GoC0Uh3{eFD{+YyfH%jjh(mb!*K~%`buc zA3~pu|67CpDU?m?or%U}HK6am%Zx#&b;rt>?4?zU&cfjV5V@vTE5J=pAph=}_LQJ6 zuMs{5G%sfZ?a%HyuX(QDnb6PIB~yU$uX9=)nTWIUPK%Q%$~S@h|2^oxIQSCSG>HG2 zQ2V0JF)sJUv%+5iiM|uFYpmkRd@|?^r@9-Ggz>Mn+IUmAA<)m`rHd02oxybG+8k)T zwmi3^n>>kPc8yyc*#l?g)tl?eaWeY83FE(q_8Vu~dnLaFX4BczKc5`sq~D2dG84t} z9dE`M;T=K#D`MnX$~9s9-x%~+y!|QY%u9|uGhOZ$Tif#_^x=TPye^(wAt z?^fB`-w%$PS^f#)Uvt@bRH*M>XUna?W?(D`<8T&s%cnDtEAC$Y9XFvyUH4mkYbsx3 z7<&v7#J{_asJ~Z(YN9jxbHMmlsJ&Ct1@x_0+BGuPAv%Xz8Ty_k$0?(U{OfGeO3`{_ z{Zy#Ato5e@NLB%QpV66TS1<**XCKR}dHM|S-{KZw>jZH{>)0#6%GX+SRIPnn^gTs!>l=4tSm$V%fUoIsW^&L6_Ay>~%G zdRU%HwpS@fcel9m3o>Zk(V0uSTgSu1wN8dgllfO2I{~e+S`&1}ZieWaa{DA*=X`N5 znGW3aJaPRF&;CHavvnvq4g3r|4_seg-;MM*vL(*yukK?Kt-o`CTkgBWd%#lC0{&IU zg+TRdZ9WN9T?g@rl943n=TUBdq>D=v_iUs(KW_;kq4wBgf#$1Urs5ZYIC4GC%6Ev9 zN#`zc;MZ{rw(T6hR-OOF*`vVvcPV;3XA3p;bHK{6Q2lbh>8Xy-O!Qjhbeya_H+;61 z%z=--b+twFfBnGb29t?sKDgye7dQKWR`IW*bq;jTcyo!bsf3uKb;ECe)+?ho@X@+6 z#!HKeu0TTAx~95x*Q+L)PkIB9|6eOO??PG*_bL z?#0zDL;BHP_E~|CyC!)qp;4{fVe|8XQ11^Lrx3MvRjYRtzE?Q@mTz8*`B#gL8>{n0 zY+Ea#2GKgU7+78l-L*%>WCNca6^SXA;cw+y_yV>{d)CUNzwG2Y0=*X5A18NBD8m0H z&Gv|*e9~VE$@Dxn^-E_D@wN$2-rc|sU>l%2N!T8= zG4`Gt|(Aw;_xjU z#vBR!J}R#7zYB!P;5;GG@*eCt>W-l_eFNH4CA$0RTR#<4x|?b3*SDedc@eNW6fOp9 z16Nih@&s`GUUMB{>Z`&3gf{W7ru`1MZ8!aP?5oSc5uKsm0_J03+VPQkq2qJ7muAs@ zQQURc%2N2l!08x5*Dn2gL7D|W4)M_)yl#R2cK-W^8`03$f!|NXp8*qqiPmo4PjkZ~NKSC09S$E0=+f74B9nD|DvvjWEk<^P(QrPYZ2F7N_%e+ z_~S7To9=SDPZ#vrR{d|7>9()>-gz~>8ojx4s+Yrx{?gf{V?((L$ zeS4=vjg%3jYzTG+2Y^$7?u}0X-Ro3lLw7Io52y7<&8bCJ#OYb!a(Y&Uj9>G7^|M!3 z)+2Ht5SNDneLK}e_mG`{+R~6DbpFq#(=PuHRv_UF&-Y#8BLn~43E%e8?+en}5+sy= z-QD&C_W~P-JwgbR&tk{Ne2>7Ux$C~2TG}nx+a?L+-{oftkq?00wT}T>7rp_m1-4Eb zw-(C&Z(wnSieCmAedC~r+GQEwU!A=MxLn57HCC&tpmxhL!vE3m7R9;NpIgB^@K%VQ z&O<8!S2iP}yG0gE1TB*U@von?Xg%KnjPeLMnRpccyE(qPYaax(2b=$sPWsm9vw_w# zOJ9V(8fb|mi2rXwfN>&zo&vfvT@C&NEX@s{#xBu*t#jy+V11ywm+Pndv(7!P&D9Sl zttFBm{>MOoJ1@o0T|nn~mn&DFPntw$BeN^dK2F^13Ks&MiTrwIVQYybh=29p-Dfrb zb&u8D*15;$OnmB0GZJ)x8DKWp4(NT!oY+Ps9c>_TOtYLU;E^V;9{_` zM@SEGpHs!J0ILI&ZBbqXzOP@N`L{Go82^>H*foSxU|v=%*H!RJ)c+Aay_U+aX{ zp6h|mnd19TKzps`xz08=2Gv6S&Poqxt0b8Jbs@?>_uP)nJx6-%ti2JJJqql`lumgBHu*0qz}Y9PxgjG@XYeeZb1nchZ*qIKliwQTe>6v-~AMcc_hk$#|5vfjE80 zj81q~kfywn3^4u`&XBRyl5qaR=ox|WB4GQmxSSi}xH~qb>1?z%7z+x(#^6x&mdi58 z|0GB=4lTSPgh=;2V`h-9{X)LW*Z&a0YBYTjKjYm(eGjgzMMS?VVd+}$bx&@)EQ9l3 z9DWPj46;D`!Z=`}^=T0>?k)7sPUhdza2e&l8fTAt{A;gRw_2uFq&JKIWJ%!#o5b0ZCbf%CA^s!?yh4g@aMqy;8sJ_gjXML%3 zQGL0H$;^1NOYg4=4`? zshqdy*j;^Kuth^z7)#Upr+4K|&rKiIZDY|teT3K?aNk8!aX=__P>0Zr+0mz(eoiWD zbGy_BxxGPxt7rND<(d8dzMf3rqG>s()3dbM^Q$eXH$SF4Q_D&DG2_oLpTg*AgL)}9 fje!d!H|>k2mU2@wgL=PHF9SYkXCf@om)8Cdx^+Yi literal 0 HcmV?d00001 diff --git a/src/ModVerify.CliApp/FodyWeavers.xml b/src/ModVerify.CliApp/FodyWeavers.xml index 5029e70..5bce71f 100644 --- a/src/ModVerify.CliApp/FodyWeavers.xml +++ b/src/ModVerify.CliApp/FodyWeavers.xml @@ -1,3 +1 @@ - - - \ No newline at end of file + \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModVerify.CliApp.csproj b/src/ModVerify.CliApp/ModVerify.CliApp.csproj index 7ada303..9d56615 100644 --- a/src/ModVerify.CliApp/ModVerify.CliApp.csproj +++ b/src/ModVerify.CliApp/ModVerify.CliApp.csproj @@ -4,6 +4,7 @@ false net8.0;net48 Exe + $(RepoRootPath)aet.ico AET.ModVerify.CommandLine AET.ModVerify Application that allows to verify to verify game modifications for Empire at War / Forces of Corruption against a set of common rules. @@ -42,4 +43,12 @@ + + + + + + + + From e2c4b230bf5c47f576b30f5642b0802964c67676 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Wed, 3 Jul 2024 00:22:59 +0200 Subject: [PATCH 18/25] start parse sfxevents --- src/ModVerify.CliApp/Program.cs | 21 ++- src/ModVerify/GameVerifySettings.cs | 23 +++ src/ModVerify/IVerificationProvider.cs | 2 +- src/ModVerify/ModVerifySettings.cs | 13 -- src/ModVerify/Steps/DuplicateFinderStep.cs | 2 +- src/ModVerify/Steps/GameVerificationStep.cs | 4 +- .../Steps/VerifyReferencedModelsStep.cs | 2 +- src/ModVerify/VerificationProvider.cs | 2 +- src/ModVerify/VerifyGamePipeline.cs | 4 +- .../DataTypes/GameObject.cs | 16 +- .../DataTypes/SfxEvent.cs | 92 +++++++++- .../DataTypes/XmlObject.cs | 28 ++++ .../Xml/Parsers/Data/GameObjectParser.cs | 13 +- .../Xml/Parsers/Data/SfxEventParser.cs | 158 ++++++++++++++---- .../Parsers/File/GameObjectFileFileParser.cs | 6 +- .../Xml/Parsers/File/SfxEventFileParser.cs | 7 +- .../Xml/Parsers/XmlObjectParser.cs | 43 ++--- .../Parsers/PetroglyphXmlElementParser.cs | 9 +- .../Parsers/PetroglyphXmlFileParser.cs | 6 +- .../Parsers/PetroglyphXmlParser.cs | 19 ++- .../PetroglyphXmlPrimitiveElementParser.cs | 11 +- .../CommaSeparatedStringKeyValueListParser.cs | 7 +- .../Primitives/IPrimitiveParserProvider.cs | 20 +++ .../Primitives/PetroglyphXmlBooleanParser.cs | 30 ++++ .../Primitives/PetroglyphXmlByteParser.cs | 27 +++ .../Primitives/PetroglyphXmlFloatParser.cs | 25 +++ .../Primitives/PetroglyphXmlIntegerParser.cs | 28 ++++ .../PetroglyphXmlLooseStringListParser.cs | 34 ++++ .../Primitives/PetroglyphXmlStringParser.cs | 7 +- .../PetroglyphXmlUnsignedIntegerParser.cs | 26 +++ .../Primitives/PrimitiveParserProvider.cs | 24 +++ .../Primitives/XmlFileContainerParser.cs | 2 +- .../ValueListDictionary.cs | 41 +++++ .../XmlServiceContribution.cs | 15 ++ 34 files changed, 638 insertions(+), 129 deletions(-) create mode 100644 src/ModVerify/GameVerifySettings.cs delete mode 100644 src/ModVerify/ModVerifySettings.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/IPrimitiveParserProvider.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlBooleanParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlByteParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlFloatParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlIntegerParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlLooseStringListParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlUnsignedIntegerParser.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PrimitiveParserProvider.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/XmlServiceContribution.cs diff --git a/src/ModVerify.CliApp/Program.cs b/src/ModVerify.CliApp/Program.cs index 8117b35..9e925c3 100644 --- a/src/ModVerify.CliApp/Program.cs +++ b/src/ModVerify.CliApp/Program.cs @@ -3,7 +3,6 @@ using System.IO; using System.IO.Abstractions; using System.Linq; -using System.Net.Http.Headers; using System.Runtime.CompilerServices; using System.Threading.Tasks; using AET.ModVerify; @@ -30,12 +29,16 @@ using PG.StarWarsGame.Infrastructure.Mods; using PG.StarWarsGame.Infrastructure.Services.Dependencies; using Serilog; +using Serilog.Events; using Serilog.Filters; namespace ModVerify.CliApp; internal class Program { + private const string EngineParserNamespace = "PG.StarWarsGame.Engine.Xml.Parsers"; + private const string ParserNamespace = "PG.StarWarsGame.Engine.Xml.Parsers"; + private static IServiceProvider _services = null!; private static CliOptions _options; @@ -123,7 +126,7 @@ private static VerifyGamePipeline BuildPipeline(IPlayableObject playableObject, fallbackGame.Directory.FullName); - return new ModVerifyPipeline(GameEngineType.Foc, gameLocations, ModVerifySettings.Default, _services); + return new ModVerifyPipeline(GameEngineType.Foc, gameLocations, GameVerifySettings.Default, _services); } private static IServiceProvider CreateAppServices() @@ -177,30 +180,34 @@ private static void ConfigureLogging(ILoggingBuilder loggingBuilder, IFileSystem private static void SetupXmlParseLogging(ILoggingBuilder loggingBuilder, IFileSystem fileSystem) { const string xmlParseLogFileName = "XmlParseLog.txt"; - const string parserNamespace = "PG.StarWarsGame.Engine.Xml.Parsers"; fileSystem.File.TryDeleteWithRetry(xmlParseLogFileName); var logger = new LoggerConfiguration() .Enrich.FromLogContext() .MinimumLevel.Warning() - .Filter.ByIncludingOnly(Matching.FromSource(parserNamespace)) + .Filter.ByIncludingOnly(IsXmlParserLogging) .WriteTo.File(xmlParseLogFileName, outputTemplate: "[{Level:u3}] [{SourceContext}] {Message}{NewLine}{Exception}") .CreateLogger(); loggingBuilder.AddSerilog(logger); - loggingBuilder.AddFilter((category, level) => + loggingBuilder.AddFilter((category, _) => { if (string.IsNullOrEmpty(category)) return false; - if (category.StartsWith(parserNamespace)) + if (category.StartsWith(EngineParserNamespace) || category.StartsWith(ParserNamespace)) return false; return true; }); } + private static bool IsXmlParserLogging(LogEvent logEvent) + { + return Matching.FromSource(ParserNamespace)(logEvent) || Matching.FromSource(EngineParserNamespace)(logEvent); + } + internal class CliOptions { @@ -215,7 +222,7 @@ internal class CliOptions internal class ModVerifyPipeline( GameEngineType targetType, GameLocations gameLocations, - ModVerifySettings settings, + GameVerifySettings settings, IServiceProvider serviceProvider) : VerifyGamePipeline(targetType, gameLocations, settings, serviceProvider) { diff --git a/src/ModVerify/GameVerifySettings.cs b/src/ModVerify/GameVerifySettings.cs new file mode 100644 index 0000000..9ccb73e --- /dev/null +++ b/src/ModVerify/GameVerifySettings.cs @@ -0,0 +1,23 @@ +namespace AET.ModVerify; + +public record GameVerifySettings +{ + public int ParallelWorkers { get; init; } = 4; + + public static readonly GameVerifySettings Default = new() + { + ThrowBehavior = VerifyThrowBehavior.None + }; + + public VerifyThrowBehavior ThrowBehavior { get; init; } + + public VerifyLocalizationOption VerifyLocalization { get; init; } +} + +public enum VerifyLocalizationOption +{ + English, + CurrentSystem, + AllInstalled, + All +} \ No newline at end of file diff --git a/src/ModVerify/IVerificationProvider.cs b/src/ModVerify/IVerificationProvider.cs index 3e4d0bc..ccd3266 100644 --- a/src/ModVerify/IVerificationProvider.cs +++ b/src/ModVerify/IVerificationProvider.cs @@ -6,5 +6,5 @@ namespace AET.ModVerify; public interface IVerificationProvider { - IEnumerable GetAllDefaultVerifiers(IGameDatabase database, ModVerifySettings settings); + IEnumerable GetAllDefaultVerifiers(IGameDatabase database, GameVerifySettings settings); } \ No newline at end of file diff --git a/src/ModVerify/ModVerifySettings.cs b/src/ModVerify/ModVerifySettings.cs deleted file mode 100644 index f639c0d..0000000 --- a/src/ModVerify/ModVerifySettings.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace AET.ModVerify; - -public record ModVerifySettings -{ - public int ParallelWorkers { get; init; } = 4; - - public static readonly ModVerifySettings Default = new() - { - ThrowBehavior = VerifyThrowBehavior.None - }; - - public VerifyThrowBehavior ThrowBehavior { get; init; } -} \ No newline at end of file diff --git a/src/ModVerify/Steps/DuplicateFinderStep.cs b/src/ModVerify/Steps/DuplicateFinderStep.cs index 8010eee..f3473dd 100644 --- a/src/ModVerify/Steps/DuplicateFinderStep.cs +++ b/src/ModVerify/Steps/DuplicateFinderStep.cs @@ -9,7 +9,7 @@ namespace AET.ModVerify.Steps; public sealed class DuplicateFinderStep( IGameDatabase gameDatabase, - ModVerifySettings settings, + GameVerifySettings settings, IServiceProvider serviceProvider) : GameVerificationStep(gameDatabase, settings, serviceProvider) { diff --git a/src/ModVerify/Steps/GameVerificationStep.cs b/src/ModVerify/Steps/GameVerificationStep.cs index de82d99..ebc27d3 100644 --- a/src/ModVerify/Steps/GameVerificationStep.cs +++ b/src/ModVerify/Steps/GameVerificationStep.cs @@ -13,7 +13,7 @@ namespace AET.ModVerify.Steps; public abstract class GameVerificationStep( IGameDatabase gameDatabase, - ModVerifySettings settings, + GameVerifySettings settings, IServiceProvider serviceProvider) : PipelineStep(serviceProvider) { @@ -24,7 +24,7 @@ public abstract class GameVerificationStep( public IReadOnlyCollection VerifyErrors => _verifyErrors; - protected ModVerifySettings Settings { get; } = settings; + protected GameVerifySettings Settings { get; } = settings; protected IGameDatabase Database { get; } = gameDatabase ?? throw new ArgumentNullException(nameof(gameDatabase)); diff --git a/src/ModVerify/Steps/VerifyReferencedModelsStep.cs b/src/ModVerify/Steps/VerifyReferencedModelsStep.cs index 9f4609d..c2ff800 100644 --- a/src/ModVerify/Steps/VerifyReferencedModelsStep.cs +++ b/src/ModVerify/Steps/VerifyReferencedModelsStep.cs @@ -18,7 +18,7 @@ namespace AET.ModVerify.Steps; public sealed class VerifyReferencedModelsStep( IGameDatabase database, - ModVerifySettings settings, + GameVerifySettings settings, IServiceProvider serviceProvider) : GameVerificationStep(database, settings, serviceProvider) { diff --git a/src/ModVerify/VerificationProvider.cs b/src/ModVerify/VerificationProvider.cs index 8330729..625faf5 100644 --- a/src/ModVerify/VerificationProvider.cs +++ b/src/ModVerify/VerificationProvider.cs @@ -7,7 +7,7 @@ namespace AET.ModVerify; internal class VerificationProvider(IServiceProvider serviceProvider) : IVerificationProvider { - public IEnumerable GetAllDefaultVerifiers(IGameDatabase database, ModVerifySettings settings) + public IEnumerable GetAllDefaultVerifiers(IGameDatabase database, GameVerifySettings settings) { yield return new VerifyReferencedModelsStep(database, settings, serviceProvider); yield return new DuplicateFinderStep(database, settings, serviceProvider); diff --git a/src/ModVerify/VerifyGamePipeline.cs b/src/ModVerify/VerifyGamePipeline.cs index 1ba2407..b71204f 100644 --- a/src/ModVerify/VerifyGamePipeline.cs +++ b/src/ModVerify/VerifyGamePipeline.cs @@ -20,9 +20,9 @@ public abstract class VerifyGamePipeline : Pipeline private readonly GameLocations _gameLocations; private readonly ParallelRunner _verifyRunner; - protected ModVerifySettings Settings { get; } + protected GameVerifySettings Settings { get; } - protected VerifyGamePipeline(GameEngineType targetType, GameLocations gameLocations, ModVerifySettings settings, IServiceProvider serviceProvider) + protected VerifyGamePipeline(GameEngineType targetType, GameLocations gameLocations, GameVerifySettings settings, IServiceProvider serviceProvider) : base(serviceProvider) { _targetType = targetType; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/GameObject.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/GameObject.cs index 320649f..6cfd4c0 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/GameObject.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/GameObject.cs @@ -8,18 +8,15 @@ namespace PG.StarWarsGame.Engine.DataTypes; public sealed class GameObject : XmlObject { - private readonly IReadOnlyValueListDictionary _properties; - internal GameObject( string type, string name, Crc32 nameCrc, GameObjectType estimatedType, - IReadOnlyValueListDictionary properties, + IReadOnlyValueListDictionary properties, XmlLocationInfo location) - : base(name, nameCrc, location) + : base(name, nameCrc, properties, location) { - _properties = properties; Type = type ?? throw new ArgumentNullException(nameof(type)); EstimatedType = estimatedType; } @@ -36,7 +33,7 @@ public ISet Models { get { - var models = _properties.AggregateValues + var models = XmlProperties.AggregateValues (new HashSet { "Galactic_Model_Name", @@ -65,11 +62,4 @@ public ISet Models public IList<(string Terrain, string Model)>? LandTerrainModelMapping => GetLastPropertyOrDefault>("Land_Terrain_Model_Mapping"); - - private T? GetLastPropertyOrDefault(string tagName, T? defaultValue = default) - { - if (!_properties.TryGetLastValue(tagName, out var value)) - return defaultValue; - return (T)value; - } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/SfxEvent.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/SfxEvent.cs index 8414df3..d74d45e 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/SfxEvent.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/SfxEvent.cs @@ -1,20 +1,100 @@ -using PG.Commons.Hashing; +using System; +using System.Collections.Generic; +using System.Linq; +using PG.Commons.Hashing; +using PG.StarWarsGame.Engine.Xml.Parsers.Data; using PG.StarWarsGame.Files.XML; namespace PG.StarWarsGame.Engine.DataTypes; public sealed class SfxEvent : XmlObject { - private int _volumeValue; + private IReadOnlyList? _preSamples; + private IReadOnlyList? _samples; + private IReadOnlyList? _postSamples; + private bool? _isPreset; + private bool? _is2D; + private bool? _is3D; + private bool? _isGui; + private bool? _isHudVo; + private bool? _isUnitResponseVo; + private bool? _isAmbientVo; + private bool? _isLocalized; public bool IsPreset { get; } - public SfxEvent? Preset { get; } + public bool Is3D => LazyInitValue(ref _is3D, SfxEventXmlTags.Is3D, false)!.Value; - public int Volume => Preset?.Volume ?? _volumeValue; + public bool Is2D => LazyInitValue(ref _is2D, SfxEventXmlTags.Is2D, false)!.Value; + + public bool IsGui => LazyInitValue(ref _isGui, SfxEventXmlTags.IsGui, false)!.Value; - internal SfxEvent(string name, Crc32 nameCrc, XmlLocationInfo location) - : base(name, nameCrc, location) + public bool IsHudVo => LazyInitValue(ref _isHudVo, SfxEventXmlTags.IsHudVo, false)!.Value; + + public bool IsUnitResponseVo => LazyInitValue(ref _isUnitResponseVo, SfxEventXmlTags.IsUnitResponseVo, false)!.Value; + + public bool IsAmbientVo => LazyInitValue(ref _isAmbientVo, SfxEventXmlTags.IsAmbientVo, false)!.Value; + + public bool IsLocalized => LazyInitValue(ref _isLocalized, SfxEventXmlTags.Localize, false)!.Value; + + public string? UsePresetName { get; } + + public bool PlaySequentially => LazyInitValue(ref _is3D, SfxEventXmlTags.Is3D, false)!.Value; + + public IEnumerable AllSamples => PreSamples.Concat(Samples).Concat(PostSamples); + + public IReadOnlyList PreSamples => LazyInitValue(ref _preSamples, SfxEventXmlTags.PreSamples, Array.Empty()); + + public IReadOnlyList Samples => LazyInitValue(ref _samples, SfxEventXmlTags.Samples, Array.Empty()); + + public IReadOnlyList PostSamples => LazyInitValue(ref _postSamples, SfxEventXmlTags.PostSamples, Array.Empty()); + + public IReadOnlyList LocalizedTextIDs { get; } + + public byte Priority { get; } + + public byte Probability { get; } + + public sbyte PlayCount { get; } + + public float LoopFadeInSeconds { get; } + + public float LoopFadeOutInSeconds { get; } + + public sbyte MaxInstances { get; } + + public byte MinVolume { get; } + + public byte MaxVolume { get; } + + public byte MinPitch { get; } + + public byte MaxPitch { get; } + + public byte MinPan2D { get; } + + public byte MaxPan2D { get; } + + public uint MinPredelay { get; } + + public uint MaxPredelay { get; } + + public uint MinPostdelay { get; } + + public uint MaxPostdelay { get; } + + public float VolumeSaturationDistance { get; } + + public bool KillsPreviousObjectsSfx { get; } + + public string? OverlapTestName { get; } + + public string? ChainedSfxEventName { get; } + + internal SfxEvent(string name, Crc32 nameCrc, IReadOnlyValueListDictionary properties, + XmlLocationInfo location) + : base(name, nameCrc, properties, location) { + } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/XmlObject.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/XmlObject.cs index 29914cb..b8161b1 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/XmlObject.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/XmlObject.cs @@ -8,6 +8,7 @@ namespace PG.StarWarsGame.Engine.DataTypes; public abstract class XmlObject( string name, Crc32 nameCrc, + IReadOnlyValueListDictionary properties, XmlLocationInfo location) : IHasCrc32 { @@ -15,5 +16,32 @@ public abstract class XmlObject( public Crc32 Crc32 { get; } = nameCrc; + public IReadOnlyValueListDictionary XmlProperties { get; } = properties ?? throw new ArgumentNullException(nameof(properties)); + public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); + + public T? GetLastPropertyOrDefault(string tagName, T? defaultValue = default) + { + if (!XmlProperties.TryGetLastValue(tagName, out var value)) + return defaultValue; + return (T)value; + } + + protected T LazyInitValue(ref T? field, string tag, T defaultValue, Func? coerceFunc = null) + { + if (field is null) + { + if (XmlProperties.TryGetLastValue(tag, out var value)) + { + var tValue = (T)value; + if (coerceFunc is not null) + tValue = coerceFunc(tValue); + field = tValue; + } + else + field = defaultValue; + } + + return field; + } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs index bd93f11..99f5917 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs @@ -8,14 +8,17 @@ namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; -public sealed class GameObjectParser(IServiceProvider serviceProvider) : XmlObjectParser(serviceProvider) +public sealed class GameObjectParser( + IReadOnlyValueListDictionary parsedElements, + IServiceProvider serviceProvider) + : XmlObjectParser(parsedElements, serviceProvider) { protected override IPetroglyphXmlElementParser? GetParser(string tag) { switch (tag) { case "Land_Terrain_Model_Mapping": - return CommaSeparatedStringKeyValueListParser.Instance; + return PrimitiveParserProvider.CommaSeparatedStringKeyValueListParser; case "Galactic_Model_Name": case "Destroyed_Galactic_Model_Name": case "Land_Model_Name": @@ -28,15 +31,15 @@ public sealed class GameObjectParser(IServiceProvider serviceProvider) : XmlObje case "Land_Model_Anim_Override_Name": case "xxxSpace_Model_Name": case "Damaged_Smoke_Asset_Name": - return PetroglyphXmlStringParser.Instance; + return PrimitiveParserProvider.StringParser; default: return null; } } - public override GameObject Parse(XElement element, IReadOnlyValueListDictionary parsedElements, out Crc32 nameCrc) + public override GameObject Parse(XElement element, out Crc32 nameCrc) { - var properties = ToKeyValuePairList(element); + var properties = ParseXmlElement(element); var name = GetNameAttributeValue(element); nameCrc = HashingService.GetCrc32Upper(name.AsSpan(), PGConstants.PGCrc32Encoding); var type = GetTagName(element); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs index ad00da6..991865b 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs @@ -1,5 +1,6 @@ using System; using System.Xml.Linq; +using Microsoft.Extensions.Logging; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.DataTypes; using PG.StarWarsGame.Files.XML; @@ -7,56 +8,153 @@ namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; -public sealed class SfxEventParser(IServiceProvider serviceProvider) : XmlObjectParser(serviceProvider) +public static class SfxEventXmlTags +{ + 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"; +} + +public sealed class SfxEventParser( + IReadOnlyValueListDictionary parsedElements, + IServiceProvider serviceProvider) + : XmlObjectParser(parsedElements, serviceProvider) { protected override IPetroglyphXmlElementParser? GetParser(string tag) { - return null; + switch (tag) + { + case SfxEventXmlTags.UsePreset: + case SfxEventXmlTags.OverlapTest: + case SfxEventXmlTags.ChainedSfxEvent: + return PrimitiveParserProvider.StringParser; + case SfxEventXmlTags.IsPreset: + case SfxEventXmlTags.Is3D: + case SfxEventXmlTags.Is2D: + case SfxEventXmlTags.IsGui: + case SfxEventXmlTags.IsHudVo: + case SfxEventXmlTags.IsUnitResponseVo: + case SfxEventXmlTags.IsAmbientVo: + case SfxEventXmlTags.Localize: + case SfxEventXmlTags.PlaySequentially: + case SfxEventXmlTags.KillsPreviousObjectSFX: + return PrimitiveParserProvider.BooleanParser; + case SfxEventXmlTags.Samples: + case SfxEventXmlTags.PreSamples: + case SfxEventXmlTags.PostSamples: + case SfxEventXmlTags.TextID: + return PrimitiveParserProvider.LooseStringListParser; + case SfxEventXmlTags.Priority: + case SfxEventXmlTags.MinPitch: + case SfxEventXmlTags.MaxPitch: + case SfxEventXmlTags.MinPan2D: + case SfxEventXmlTags.MaxPan2D: + case SfxEventXmlTags.PlayCount: + case SfxEventXmlTags.MaxInstances: + return PrimitiveParserProvider.IntParser; + case SfxEventXmlTags.Probability: + case SfxEventXmlTags.MinVolume: + case SfxEventXmlTags.MaxVolume: + return PrimitiveParserProvider.ByteParser; + case SfxEventXmlTags.MinPredelay: + case SfxEventXmlTags.MaxPredelay: + case SfxEventXmlTags.MinPostdelay: + case SfxEventXmlTags.MaxPostdelay: + return PrimitiveParserProvider.UIntParser; + case SfxEventXmlTags.LoopFadeInSeconds: + case SfxEventXmlTags.LoopFadeOutSeconds: + case SfxEventXmlTags.VolumeSaturationDistance: + return PrimitiveParserProvider.FloatParser; + default: + return null; + } } public override SfxEvent Parse( XElement element, - IReadOnlyValueListDictionary parsedElements, out Crc32 nameCrc) { var name = GetNameAttributeValue(element); nameCrc = HashingService.GetCrc32Upper(name.AsSpan(), PGConstants.PGCrc32Encoding); - var valueList = new ValueListDictionary(); + var properties = ParseXmlElement(element); - foreach (var child in element.Elements()) - { - var tagName = child.Name.LocalName; - var parser = GetParser(tagName); - if (parser is null) - { - //_logger?.LogWarning($"Unable to find parser for tag '{tagName}' in element '{name}'"); - continue; - } + return new SfxEvent(name, nameCrc, properties, XmlLocationInfo.FromElement(element)); + } - if (tagName.Equals("Use_Preset")) - { - var presetName = parser.Parse(child) as string; - var presetNameCrc = HashingService.GetCrc32Upper(presetName.AsSpan(), PGConstants.PGCrc32Encoding); - if (presetNameCrc == default || !parsedElements.TryGetFirstValue(presetNameCrc, out var preset)) - { - // Unable to find Preset - continue; - } - } - else + protected override bool OnParsed(string tag, object value, ValueListDictionary properties, string? elementName) + { + if (tag == SfxEventXmlTags.UsePreset) + { + var presetName = value as string; + var presetNameCrc = HashingService.GetCrc32Upper(presetName.AsSpan(), PGConstants.PGCrc32Encoding); + if (presetNameCrc == default || !ParsedElements.TryGetFirstValue(presetNameCrc, out var preset)) { - valueList.Add(tagName, parser.Parse(child)); + Logger?.LogWarning($"Cannot to find preset '{presetName}' for SFXEvent '{elementName ?? "NONE"}'"); + return false; } + CopySfxPreset(properties, preset); } - var sfxEvent = new SfxEvent(name, nameCrc, XmlLocationInfo.FromElement(element)); - - return sfxEvent; + return true; } - public override SfxEvent Parse(XElement element) + + private void CopySfxPreset(ValueListDictionary currentXmlProperties, SfxEvent preset) { - throw new NotSupportedException(); + /* + * The engine also copies the Use_Preset *of* the preset, (which almost most cases is null) + * As this would cause that the SfxEvent using the preset, would not have a reference to its original preset, we do not copy the preset + * Example: + * + * + * Preset Yes + * 90 + * + * + * Engine Behavior: SFXEvent instance(Name: A, Use_Preset: null, Min_Volume: 90) + * PG.StarWarsGame.Engine Behavior: SFXEvent instance(Name: A, Use_Preset: Preset, Min_Volume: 90) + */ + + foreach (var keyValuePair in preset.XmlProperties) + { + if (keyValuePair.Key is SfxEventXmlTags.UsePreset or SfxEventXmlTags.IsPreset) + continue; + currentXmlProperties.Add(keyValuePair.Key, keyValuePair.Value); + } } + + + 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/GameObjectFileFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileFileParser.cs index e02a86d..238a26c 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileFileParser.cs @@ -13,12 +13,12 @@ internal class GameObjectFileFileParser(IServiceProvider serviceProvider) { protected override void Parse(XElement element, IValueListDictionary parsedElements) { - var parser = new GameObjectParser(ServiceProvider); + var parser = new GameObjectParser(parsedElements, ServiceProvider); foreach (var xElement in element.Elements()) { - var sfxEvent = parser.Parse(xElement, parsedElements, out var nameCrc); - parsedElements.Add(nameCrc, sfxEvent); + var gameObject = parser.Parse(xElement, out var nameCrc); + parsedElements.Add(nameCrc, gameObject); } } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs index c02b6bb..35e8e02 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs @@ -13,12 +13,11 @@ internal class SfxEventFileParser(IServiceProvider serviceProvider) { protected override void Parse(XElement element, IValueListDictionary parsedElements) { - var parser = new SfxEventParser(ServiceProvider); - var parsedObjects = new ValueListDictionary(); + var parser = new SfxEventParser(parsedElements, ServiceProvider); foreach (var xElement in element.Elements()) { - var sfxEvent = parser.Parse(xElement, parsedObjects, out var nameCrc); - parsedObjects.Add(nameCrc, sfxEvent); + var sfxEvent = parser.Parse(xElement, out var nameCrc); + parsedElements.Add(nameCrc, sfxEvent); } } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs index 239eb97..f2eab56 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs @@ -1,7 +1,6 @@ using System; using System.Xml.Linq; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.DataTypes; using PG.StarWarsGame.Files.XML; @@ -9,38 +8,44 @@ namespace PG.StarWarsGame.Engine.Xml.Parsers; -public abstract class XmlObjectParser : PetroglyphXmlElementParser where T : XmlObject -{ - protected IServiceProvider ServiceProvider { get; } +public abstract class XmlObjectParser(IReadOnlyValueListDictionary parsedElements, IServiceProvider serviceProvider) + : PetroglyphXmlElementParser(serviceProvider) where T : XmlObject +{ + protected IReadOnlyValueListDictionary ParsedElements { get; } = parsedElements ?? throw new ArgumentNullException(nameof(parsedElements)); - protected ILogger? Logger { get; } - - protected ICrc32HashingService HashingService { get; } + protected ICrc32HashingService HashingService { get; } = serviceProvider.GetRequiredService(); - protected XmlObjectParser(IServiceProvider serviceProvider) - { - ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - Logger = serviceProvider.GetService()?.CreateLogger(GetType()); - HashingService = serviceProvider.GetRequiredService(); - } + public abstract T Parse(XElement element, out Crc32 nameCrc); protected abstract IPetroglyphXmlElementParser? GetParser(string tag); - protected ValueListDictionary ToKeyValuePairList(XElement element) + protected ValueListDictionary ParseXmlElement(XElement element, string? name = null) { - var keyValuePairList = new ValueListDictionary(); + var xmlProperties = new ValueListDictionary(); foreach (var elm in element.Elements()) { var tagName = elm.Name.LocalName; var parser = GetParser(tagName); - if (parser is not null) + if (parser is null) { - var value = parser.Parse(elm); - keyValuePairList.Add(tagName, value); + // TODO + //var nameOrPosition = name ?? XmlLocationInfo.FromElement(element).ToString(); + //Logger?.LogWarning($"Unable to find parser for tag '{tagName}' in element '{nameOrPosition}'"); + continue; } + + var value = parser.Parse(elm); + + if (OnParsed(tagName, value, xmlProperties, name)) + xmlProperties.Add(tagName, value); } - return keyValuePairList; + return xmlProperties; + } + + protected virtual bool OnParsed(string tag, object value, ValueListDictionary properties, string? elementName) + { + return true; } } \ 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 index 8ab4eee..013db76 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlElementParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlElementParser.cs @@ -1,10 +1,11 @@ -using System.Linq; +using System; +using System.Linq; using System.Xml.Linq; -using PG.Commons.Hashing; namespace PG.StarWarsGame.Files.XML.Parsers; -public abstract class PetroglyphXmlElementParser : PetroglyphXmlParser +public abstract class PetroglyphXmlElementParser(IServiceProvider serviceProvider) + : PetroglyphXmlParser(serviceProvider) { protected string GetTagName(XElement element) { @@ -17,6 +18,4 @@ protected string GetNameAttributeValue(XElement element) .FirstOrDefault(a => a.Name.LocalName == "Name"); return nameAttribute is null ? string.Empty : nameAttribute.Value; } - - public abstract T Parse(XElement element, IReadOnlyValueListDictionary parsedElements, out Crc32 nameCrc); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs index 828d49e..871ff6f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs @@ -7,11 +7,9 @@ namespace PG.StarWarsGame.Files.XML.Parsers; -public abstract class PetroglyphXmlFileParser(IServiceProvider serviceProvider) : PetroglyphXmlParser, IPetroglyphXmlFileParser +public abstract class PetroglyphXmlFileParser(IServiceProvider serviceProvider) : PetroglyphXmlParser(serviceProvider), IPetroglyphXmlFileParser { - protected IServiceProvider ServiceProvider { get; } = serviceProvider; - - protected virtual bool LoadLineInfo => false; + protected virtual bool LoadLineInfo => true; public T ParseFile(Stream xmlStream) { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlParser.cs index c0181dd..3ef8976 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlParser.cs @@ -1,9 +1,26 @@ -using System.Xml.Linq; +using Microsoft.Extensions.Logging; +using System; +using System.Xml.Linq; +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Files.XML.Parsers.Primitives; namespace PG.StarWarsGame.Files.XML.Parsers; public abstract class PetroglyphXmlParser : IPetroglyphXmlParser { + protected IServiceProvider ServiceProvider { get; } + + protected ILogger? Logger { get; } + + protected IPrimitiveParserProvider PrimitiveParserProvider { get; } + + protected PetroglyphXmlParser(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + Logger = serviceProvider.GetService()?.CreateLogger(GetType()); + PrimitiveParserProvider = serviceProvider.GetRequiredService(); + } + public abstract T Parse(XElement element); object IPetroglyphXmlParser.Parse(XElement element) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlPrimitiveElementParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlPrimitiveElementParser.cs index b481d17..7a155c5 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlPrimitiveElementParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlPrimitiveElementParser.cs @@ -1,3 +1,10 @@ -namespace PG.StarWarsGame.Files.XML.Parsers; +using System; -public abstract class PetroglyphXmlPrimitiveElementParser : PetroglyphXmlParser, IPetroglyphXmlElementParser; \ No newline at end of file +namespace PG.StarWarsGame.Files.XML.Parsers; + +public abstract class PetroglyphXmlPrimitiveElementParser : PetroglyphXmlParser, IPetroglyphXmlElementParser +{ + private protected PetroglyphXmlPrimitiveElementParser(IServiceProvider serviceProvider) : base(serviceProvider) + { + } +} \ 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 a300719..5e15971 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/CommaSeparatedStringKeyValueListParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/CommaSeparatedStringKeyValueListParser.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Xml.Linq; namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; @@ -8,9 +9,7 @@ namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; // There might be arbitrary spaces, tabs and newlines public sealed class CommaSeparatedStringKeyValueListParser : PetroglyphXmlPrimitiveElementParser> { - public static readonly CommaSeparatedStringKeyValueListParser Instance = new(); - - private CommaSeparatedStringKeyValueListParser() + internal CommaSeparatedStringKeyValueListParser(IServiceProvider serviceProvider) : base(serviceProvider) { } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/IPrimitiveParserProvider.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/IPrimitiveParserProvider.cs new file mode 100644 index 0000000..5f250e4 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/IPrimitiveParserProvider.cs @@ -0,0 +1,20 @@ +namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; + +public interface IPrimitiveParserProvider +{ + PetroglyphXmlStringParser StringParser { get; } + + PetroglyphXmlUnsignedIntegerParser UIntParser { get; } + + PetroglyphXmlLooseStringListParser LooseStringListParser { get; } + + PetroglyphXmlIntegerParser IntParser { get; } + + PetroglyphXmlFloatParser FloatParser { get; } + + PetroglyphXmlByteParser ByteParser { get; } + + PetroglyphXmlBooleanParser BooleanParser { get; } + + CommaSeparatedStringKeyValueListParser CommaSeparatedStringKeyValueListParser { get; } +} \ 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 new file mode 100644 index 0000000..d3d28a0 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlBooleanParser.cs @@ -0,0 +1,30 @@ +using System; +using System.Xml.Linq; + +namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; + +public sealed class PetroglyphXmlBooleanParser : PetroglyphXmlPrimitiveElementParser +{ + internal PetroglyphXmlBooleanParser(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + public override bool Parse(XElement element) + { + var valueSpan = element.Value.AsSpan(); + var trimmed = valueSpan.Trim(); + + if (trimmed.Length == 0) + return false; + + // Yes! The engine only checks if the values is exact 1 or starts with Tt or Yy + // At least it's efficient, I guess... + if (trimmed.Length == 1 && trimmed[0] == '1') + return true; + + if (trimmed[0] is 'y' or 'Y' or 't' or 'T') + return true; + + return false; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlByteParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlByteParser.cs new file mode 100644 index 0000000..0da16a3 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlByteParser.cs @@ -0,0 +1,27 @@ +using System; +using System.Xml.Linq; +using Microsoft.Extensions.Logging; + +namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; + +public sealed class PetroglyphXmlByteParser : PetroglyphXmlPrimitiveElementParser +{ + internal PetroglyphXmlByteParser(IServiceProvider serviceProvider) : base(serviceProvider) + { + + } + + public override byte Parse(XElement element) + { + var intValue = PrimitiveParserProvider.IntParser.Parse(element); + + var asByte = (byte)intValue; + if (intValue != asByte) + { + var location = XmlLocationInfo.FromElement(element); + Logger?.LogWarning($"Expected a byte value (0 - 255) but got value '{intValue}' at {location}"); + } + + 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 new file mode 100644 index 0000000..bbb2a8f --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlFloatParser.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Globalization; +using System.Xml.Linq; + +namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; + +public sealed class PetroglyphXmlFloatParser : PetroglyphXmlPrimitiveElementParser +{ + internal PetroglyphXmlFloatParser(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + public override float Parse(XElement element) + { + // The engine always loads FP numbers a long double and then converts that result to float + if (!double.TryParse(element.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var doubleValue)) + { + var location = XmlLocationInfo.FromElement(element); + Logger?.LogWarning($"Expected double but got value '{element.Value}' at {location}"); + return 0.0f; + } + return (float)doubleValue; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlIntegerParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlIntegerParser.cs new file mode 100644 index 0000000..ab67620 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlIntegerParser.cs @@ -0,0 +1,28 @@ +using System; +using System.Xml.Linq; +using Microsoft.Extensions.Logging; + +namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; + +public sealed class PetroglyphXmlIntegerParser : PetroglyphXmlPrimitiveElementParser +{ + internal PetroglyphXmlIntegerParser(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + public override int Parse(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(element.Value, out var i)) + { + var location = XmlLocationInfo.FromElement(element); + Logger?.LogWarning($"Expected integer but got '{element.Value}' at {location}"); + return 0; + } + + return i; + } +} \ 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 new file mode 100644 index 0000000..6bc124a --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlLooseStringListParser.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; + +public sealed class PetroglyphXmlLooseStringListParser : PetroglyphXmlPrimitiveElementParser> +{ + // These are the characters the engine uses as a generic list separator + private static readonly char[] Separators = + [ + ' ', + ',', + '\t', + '\n', + '\r' + ]; + + internal PetroglyphXmlLooseStringListParser(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + public override IList Parse(XElement element) + { + var trimmedValued = element.Value.Trim(); + + if (trimmedValued.Length == 0) + return Array.Empty(); + + var entries = trimmedValued.Split(Separators, StringSplitOptions.RemoveEmptyEntries); + + return entries; + } +} \ 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 b5551ec..a4a7e98 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlStringParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlStringParser.cs @@ -1,12 +1,11 @@ -using System.Xml.Linq; +using System; +using System.Xml.Linq; namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; public sealed class PetroglyphXmlStringParser : PetroglyphXmlPrimitiveElementParser { - public static readonly PetroglyphXmlStringParser Instance = new(); - - private PetroglyphXmlStringParser() + internal PetroglyphXmlStringParser(IServiceProvider serviceProvider) : base(serviceProvider) { } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlUnsignedIntegerParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlUnsignedIntegerParser.cs new file mode 100644 index 0000000..7faf6a7 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlUnsignedIntegerParser.cs @@ -0,0 +1,26 @@ +using System; +using System.Xml.Linq; +using Microsoft.Extensions.Logging; + +namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; + +public sealed class PetroglyphXmlUnsignedIntegerParser : PetroglyphXmlPrimitiveElementParser +{ + internal PetroglyphXmlUnsignedIntegerParser(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + public override uint Parse(XElement element) + { + var intValue = PrimitiveParserProvider.IntParser.Parse(element); + + var asUint = (uint)intValue; + if (intValue != asUint) + { + var location = XmlLocationInfo.FromElement(element); + Logger?.LogWarning($"Expected unsigned integer but got '{intValue}' at {location}"); + } + + return asUint; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PrimitiveParserProvider.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PrimitiveParserProvider.cs new file mode 100644 index 0000000..d749197 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PrimitiveParserProvider.cs @@ -0,0 +1,24 @@ +using System; + +namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; + +internal class PrimitiveParserProvider(IServiceProvider serviceProvider) : IPrimitiveParserProvider +{ + private readonly Lazy _lazyStringParser = new(() => new PetroglyphXmlStringParser(serviceProvider)); + private readonly Lazy _lazyUintParser = new(() => new PetroglyphXmlUnsignedIntegerParser(serviceProvider)); + private readonly Lazy _lazyLooseStringListParser = new(() => new PetroglyphXmlLooseStringListParser(serviceProvider)); + private readonly Lazy _lazyIntParser = new(() => new PetroglyphXmlIntegerParser(serviceProvider)); + private readonly Lazy _lazyFloatParser = new(() => new PetroglyphXmlFloatParser(serviceProvider)); + private readonly Lazy _lazyByteParser = new(() => new PetroglyphXmlByteParser(serviceProvider)); + private readonly Lazy _lazyBoolParser = new(() => new PetroglyphXmlBooleanParser(serviceProvider)); + private readonly Lazy _lazyCommaStringKeyListParser = new(() => new CommaSeparatedStringKeyValueListParser(serviceProvider)); + + public PetroglyphXmlStringParser StringParser => _lazyStringParser.Value; + public PetroglyphXmlUnsignedIntegerParser UIntParser => _lazyUintParser.Value; + public PetroglyphXmlLooseStringListParser LooseStringListParser => _lazyLooseStringListParser.Value; + public PetroglyphXmlIntegerParser IntParser => _lazyIntParser.Value; + public PetroglyphXmlFloatParser FloatParser => _lazyFloatParser.Value; + public PetroglyphXmlByteParser ByteParser => _lazyByteParser.Value; + public PetroglyphXmlBooleanParser BooleanParser => _lazyBoolParser.Value; + public CommaSeparatedStringKeyValueListParser CommaSeparatedStringKeyValueListParser => _lazyCommaStringKeyListParser.Value; +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/XmlFileContainerParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/XmlFileContainerParser.cs index bda0df3..2c8424a 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/XmlFileContainerParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/XmlFileContainerParser.cs @@ -21,7 +21,7 @@ public override XmlFileContainer Parse(XElement element) { if (child.Name == "File") { - var file = PetroglyphXmlStringParser.Instance.Parse(child); + var file = PrimitiveParserProvider.StringParser.Parse(child); files.Add(file); } } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ValueListDictionary.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ValueListDictionary.cs index 5da2eb7..73f8aab 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ValueListDictionary.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ValueListDictionary.cs @@ -12,8 +12,15 @@ namespace PG.StarWarsGame.Files.XML; public interface IReadOnlyValueListDictionary : IEnumerable> where TKey : notnull { ICollection Values { get; } + ICollection Keys { get; } + int Count { get; } + + TKey this[int index] { get; } + + TValue GetValueAtIndex(int index); + bool ContainsKey(TKey key); ReadOnlyFrugalList GetValues(TKey key); @@ -37,13 +44,45 @@ public interface IValueListDictionary : IReadOnlyValueListDictiona // NOT THREAD-SAFE! public class ValueListDictionary : IValueListDictionary where TKey : notnull { + private readonly List _insertionTrackingList = new(); private readonly Dictionary _singleValueDictionary = new (); private readonly Dictionary> _multiValueDictionary = new(); + private readonly EqualityComparer _equalityComparer = EqualityComparer.Default; + + public int Count => _insertionTrackingList.Count; + + public TKey this[int index] => _insertionTrackingList[index]; + public ICollection Keys => _singleValueDictionary.Keys.Concat(_multiValueDictionary.Keys).ToList(); public ICollection Values => this.Select(x => x.Value).ToList(); + public TValue GetValueAtIndex(int index) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + var key = this[index]; + if (_singleValueDictionary.TryGetValue(key, out var value)) + return value; + + if (index == 0) + return _multiValueDictionary[key].First(); + + if (index == Count - 1) + return _multiValueDictionary[key].Last(); + + var keyCount = 0; + foreach (var k in _insertionTrackingList.Take(index + 1)) + { + if (_equalityComparer.Equals(key, k)) + keyCount++; + } + + return _multiValueDictionary[key][keyCount - 1]; + } + public bool ContainsKey(TKey key) { return _singleValueDictionary.ContainsKey(key) || _multiValueDictionary.ContainsKey(key); @@ -54,6 +93,8 @@ public bool Add(TKey key, TValue value) if (key is null) throw new ArgumentNullException(nameof(key)); + _insertionTrackingList.Add(key); + if (!_singleValueDictionary.ContainsKey(key)) { if (!_multiValueDictionary.TryGetValue(key, out var list)) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XmlServiceContribution.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XmlServiceContribution.cs new file mode 100644 index 0000000..9761d64 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XmlServiceContribution.cs @@ -0,0 +1,15 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Files.XML.Parsers.Primitives; + +namespace PG.StarWarsGame.Files.XML; + +public static class XmlServiceContribution +{ + public static void ContributeServices(IServiceCollection serviceCollection) + { + serviceCollection.AddSingleton(sp => new PrimitiveParserProvider(sp)); + } +} \ No newline at end of file From 0b3ce5590bce1d52e1f964a515dabc6e905094c5 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sat, 6 Jul 2024 15:23:35 +0200 Subject: [PATCH 19/25] parse sfxevent --- .../DataTypes/SfxEvent.cs | 210 +++++++++++++++--- .../PG.StarWarsGame.Engine.csproj | 4 + .../Xml/Parsers/Data/SfxEventParser.cs | 60 +---- .../Xml/Parsers/File/SfxEventFileParser.cs | 3 + .../Xml/Tags/SfxEventXmlTags.cs | 41 ++++ .../Primitives/IPrimitiveParserProvider.cs | 2 + .../PetroglyphXmlLooseStringListParser.cs | 7 + .../PetroglyphXmlMax100ByteParser.cs | 37 +++ .../Primitives/PrimitiveParserProvider.cs | 2 + 9 files changed, 290 insertions(+), 76 deletions(-) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Tags/SfxEventXmlTags.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlMax100ByteParser.cs diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/SfxEvent.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/SfxEvent.cs index d74d45e..9920449 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/SfxEvent.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/SfxEvent.cs @@ -2,13 +2,44 @@ using System.Collections.Generic; using System.Linq; using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.Xml.Parsers.Data; +using PG.StarWarsGame.Engine.Xml.Tags; using PG.StarWarsGame.Files.XML; namespace PG.StarWarsGame.Engine.DataTypes; public sealed class SfxEvent : XmlObject { + public const byte MaxVolumeValue = 100; + public const byte MaxPitchValue = 200; + public const byte MinPitchValue = 50; + public const byte MaxPan2dValue = 100; + public const byte MinPriorityValue = 1; + public const byte MaxPriorityValue = 5; + public const byte MaxProbability = 100; + public const sbyte MinMaxInstances = 0; + public const sbyte InfinitivePlayCount = -1; + public const float MinLoopSeconds = 0.0f; + public const float MinVolumeSaturation = 0.0f; + + // Default values which are not the default value of the type + public const byte DefaultPriority = 3; + public const bool DefaultIs3d = true; + public const byte DefaultProbability = 100; + public const sbyte DefaultPlayCount = 1; + public const sbyte DefaultMaxInstances = 1; + public const byte DefaultMinVolume = 100; + public const byte DefaultMaxVolume = 100; + public const byte DefaultMinPitch = 100; + public const byte DefaultMaxPitch = 100; + public const byte DefaultMinPan2d = 50; + public const byte DefaultMaxPan2d = 50; + public const float DefaultVolumeSaturationDistance = 300.0f; + + private SfxEvent? _preset; + private string? _presetName; + private string? _overlapTestName; + private string? _chainedSfxEvent; + private IReadOnlyList? _textIds; private IReadOnlyList? _preSamples; private IReadOnlyList? _samples; private IReadOnlyList? _postSamples; @@ -20,10 +51,70 @@ public sealed class SfxEvent : XmlObject private bool? _isUnitResponseVo; private bool? _isAmbientVo; private bool? _isLocalized; + private bool? _playSequentially; + private bool? _killsPreviousObjectsSfx; + private byte? _priority; + private byte? _probability; + private sbyte? _playCount; + private sbyte? _maxInstances; + private uint? _minPredelay; + private uint? _maxPredelay; + private uint? _minPostdelay; + private uint? _maxPostdelay; + private float? _loopFadeInSeconds; + private float? _loopFadeOutSeconds; + private float? _volumeSaturationDistance; + + private static readonly Func PriorityCoercion = priority => + { + if (!priority.HasValue) + return DefaultPriority; + if (priority < MinPriorityValue) + return MinPriorityValue; + if (priority > MaxPriorityValue) + return MaxPriorityValue; + return priority; + }; + + private static readonly Func MaxInstancesCoercion = maxInstances => + { + if (!maxInstances.HasValue) + return DefaultMaxInstances; + if (maxInstances < MinMaxInstances) + return MinMaxInstances; + return maxInstances; + }; + + private static readonly Func ProbabilityCoercion = probability => + { + if (!probability.HasValue) + return DefaultProbability; + if (probability > MaxProbability) + return MaxProbability; + return probability; + }; + + private static readonly Func PlayCountCoercion = playCount => + { + if (!playCount.HasValue) + return DefaultPlayCount; + if (playCount < InfinitivePlayCount) + return InfinitivePlayCount; + return playCount; + }; + + private static readonly Func LoopAndSaturationCoercion = loopSeconds => + { + if (!loopSeconds.HasValue) + return DefaultVolumeSaturationDistance; + if (loopSeconds < MinLoopSeconds) + return MinLoopSeconds; + return loopSeconds; + }; - public bool IsPreset { get; } + public bool IsPreset => LazyInitValue(ref _isPreset, SfxEventXmlTags.IsPreset, false)!.Value; - public bool Is3D => LazyInitValue(ref _is3D, SfxEventXmlTags.Is3D, false)!.Value; + public bool Is3D => LazyInitValue(ref _is3D, SfxEventXmlTags.Is3D, DefaultIs3d)!.Value; public bool Is2D => LazyInitValue(ref _is2D, SfxEventXmlTags.Is2D, false)!.Value; @@ -37,9 +128,11 @@ public sealed class SfxEvent : XmlObject public bool IsLocalized => LazyInitValue(ref _isLocalized, SfxEventXmlTags.Localize, false)!.Value; - public string? UsePresetName { get; } + public SfxEvent? Preset => LazyInitValue(ref _preset, SfxEventXmlTags.PresetXRef, null); + + public string? UsePresetName => LazyInitValue(ref _presetName, SfxEventXmlTags.UsePreset, null); - public bool PlaySequentially => LazyInitValue(ref _is3D, SfxEventXmlTags.Is3D, false)!.Value; + public bool PlaySequentially => LazyInitValue(ref _playSequentially, SfxEventXmlTags.PlaySequentially, false)!.Value; public IEnumerable AllSamples => PreSamples.Concat(Samples).Concat(PostSamples); @@ -49,19 +142,36 @@ public sealed class SfxEvent : XmlObject public IReadOnlyList PostSamples => LazyInitValue(ref _postSamples, SfxEventXmlTags.PostSamples, Array.Empty()); - public IReadOnlyList LocalizedTextIDs { get; } + public IReadOnlyList LocalizedTextIDs => LazyInitValue(ref _textIds, SfxEventXmlTags.TextID, Array.Empty()); + + public byte Priority => LazyInitValue(ref _priority, SfxEventXmlTags.Priority, DefaultPriority, PriorityCoercion)!.Value; + + public byte Probability => LazyInitValue(ref _probability, SfxEventXmlTags.Probability, DefaultProbability, ProbabilityCoercion)!.Value; - public byte Priority { get; } + public sbyte PlayCount => LazyInitValue(ref _playCount, SfxEventXmlTags.PlayCount, DefaultPlayCount, PlayCountCoercion)!.Value; - public byte Probability { get; } + public float LoopFadeInSeconds => LazyInitValue(ref _loopFadeInSeconds, SfxEventXmlTags.LoopFadeInSeconds, 0f, LoopAndSaturationCoercion)!.Value; - public sbyte PlayCount { get; } + public float LoopFadeOutSeconds => LazyInitValue(ref _loopFadeOutSeconds, SfxEventXmlTags.LoopFadeOutSeconds, 0f, LoopAndSaturationCoercion)!.Value; - public float LoopFadeInSeconds { get; } + public sbyte MaxInstances => LazyInitValue(ref _maxInstances, SfxEventXmlTags.MaxInstances, DefaultMaxInstances, MaxInstancesCoercion)!.Value; - public float LoopFadeOutInSeconds { get; } + public uint MinPredelay => LazyInitValue(ref _minPredelay, SfxEventXmlTags.MinPredelay, 0u)!.Value; - public sbyte MaxInstances { get; } + public uint MaxPredelay => LazyInitValue(ref _maxPredelay, SfxEventXmlTags.MaxPredelay, 0u)!.Value; + + public uint MinPostdelay => LazyInitValue(ref _minPostdelay, SfxEventXmlTags.MinPostdelay, 0u)!.Value; + + public uint MaxPostdelay => LazyInitValue(ref _maxPostdelay, SfxEventXmlTags.MaxPostdelay, 0u)!.Value; + + public float VolumeSaturationDistance => LazyInitValue(ref _volumeSaturationDistance, + SfxEventXmlTags.VolumeSaturationDistance, DefaultVolumeSaturationDistance, LoopAndSaturationCoercion)!.Value; + + public bool KillsPreviousObjectsSfx => LazyInitValue(ref _killsPreviousObjectsSfx, SfxEventXmlTags.KillsPreviousObjectSFX, false)!.Value; + + public string? OverlapTestName => LazyInitValue(ref _overlapTestName, SfxEventXmlTags.OverlapTest, null); + + public string? ChainedSfxEventName => LazyInitValue(ref _chainedSfxEvent, SfxEventXmlTags.ChainedSfxEvent, null); public byte MinVolume { get; } @@ -75,26 +185,76 @@ public sealed class SfxEvent : XmlObject public byte MaxPan2D { get; } - public uint MinPredelay { get; } - - public uint MaxPredelay { get; } + internal SfxEvent(string name, Crc32 nameCrc, IReadOnlyValueListDictionary properties, + XmlLocationInfo location) + : base(name, nameCrc, properties, location) + { + var minMaxVolume = GetMinMaxVolume(properties); + MinVolume = minMaxVolume.min; + MaxVolume = minMaxVolume.max; - public uint MinPostdelay { get; } + var minMaxPitch = GetMinMaxPitch(properties); + MinPitch = minMaxPitch.min; + MaxPitch = minMaxPitch.max; - public uint MaxPostdelay { get; } + var minMaxPan = GetMinMaxPan2d(properties); + MinPan2D = minMaxPan.min; + MaxPan2D = minMaxPan.max; + } - public float VolumeSaturationDistance { get; } + private static (byte min, byte max) GetMinMaxVolume(IReadOnlyValueListDictionary properties) + { + return GetMinMaxValues(properties, SfxEventXmlTags.MinVolume, SfxEventXmlTags.MaxVolume, DefaultMinVolume, + DefaultMaxVolume, null, MaxVolumeValue); + } - public bool KillsPreviousObjectsSfx { get; } + private static (byte min, byte max) GetMinMaxPitch(IReadOnlyValueListDictionary properties) + { + return GetMinMaxValues(properties, SfxEventXmlTags.MinPitch, SfxEventXmlTags.MaxPitch, DefaultMinPitch, + DefaultMaxPitch, MinPitchValue, MaxPitchValue); + } - public string? OverlapTestName { get; } + private static (byte min, byte max) GetMinMaxPan2d(IReadOnlyValueListDictionary properties) + { + return GetMinMaxValues(properties, SfxEventXmlTags.MinPan2D, SfxEventXmlTags.MaxPan2D, DefaultMinPan2d, + DefaultMaxPan2d, null, MaxPan2dValue); + } - public string? ChainedSfxEventName { get; } - internal SfxEvent(string name, Crc32 nameCrc, IReadOnlyValueListDictionary properties, - XmlLocationInfo location) - : base(name, nameCrc, properties, location) + private static (byte min, byte max) GetMinMaxValues( + IReadOnlyValueListDictionary properties, + string minTag, + string maxTag, + byte defaultMin, + byte defaultMax, + byte? totalMinValue, + byte? totalMaxValue) { - + var minValue = !properties.TryGetLastValue(minTag, out var minObj) ? defaultMin : Convert.ToByte(minObj); + var maxValue = !properties.TryGetLastValue(maxTag, out var maxObj) ? defaultMax : Convert.ToByte(maxObj); + + if (totalMaxValue.HasValue) + { + if (minValue > totalMaxValue) + minValue = totalMaxValue.Value; + if (maxValue > totalMaxValue) + maxValue = totalMaxValue.Value; + } + + if (totalMinValue.HasValue) + { + if (minValue < totalMinValue) + minValue = totalMinValue.Value; + if (maxValue < totalMinValue) + maxValue = totalMinValue.Value; + } + + if (minValue > maxValue) + minValue = maxValue; + + if (maxValue < minValue) + maxValue = minValue; + + return (minValue, maxValue); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj index 0c08df4..ddd8497 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj @@ -26,6 +26,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs index 991865b..b767564 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs @@ -3,49 +3,12 @@ using Microsoft.Extensions.Logging; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.DataTypes; +using PG.StarWarsGame.Engine.Xml.Tags; using PG.StarWarsGame.Files.XML; using PG.StarWarsGame.Files.XML.Parsers; namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; -public static class SfxEventXmlTags -{ - 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"; -} - public sealed class SfxEventParser( IReadOnlyValueListDictionary parsedElements, IServiceProvider serviceProvider) @@ -86,7 +49,7 @@ public sealed class SfxEventParser( case SfxEventXmlTags.Probability: case SfxEventXmlTags.MinVolume: case SfxEventXmlTags.MaxVolume: - return PrimitiveParserProvider.ByteParser; + return PrimitiveParserProvider.Max100ByteParser; case SfxEventXmlTags.MinPredelay: case SfxEventXmlTags.MaxPredelay: case SfxEventXmlTags.MinPostdelay: @@ -101,9 +64,7 @@ public sealed class SfxEventParser( } } - public override SfxEvent Parse( - XElement element, - out Crc32 nameCrc) + public override SfxEvent Parse(XElement element, out Crc32 nameCrc) { var name = GetNameAttributeValue(element); nameCrc = HashingService.GetCrc32Upper(name.AsSpan(), PGConstants.PGCrc32Encoding); @@ -119,19 +80,15 @@ protected override bool OnParsed(string tag, object value, ValueListDictionary currentXmlProperties, SfxEvent preset) + private static void CopySfxPreset(ValueListDictionary currentXmlProperties, SfxEvent preset) { /* * The engine also copies the Use_Preset *of* the preset, (which almost most cases is null) @@ -153,8 +110,9 @@ private void CopySfxPreset(ValueListDictionary currentXmlProper continue; currentXmlProperties.Add(keyValuePair.Key, keyValuePair.Value); } - } + currentXmlProperties.Add(SfxEventXmlTags.PresetXRef, preset); + } 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/SfxEventFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs index 35e8e02..559414e 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs @@ -1,5 +1,6 @@ using System; using System.Xml.Linq; +using Microsoft.Extensions.Logging; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.DataTypes; using PG.StarWarsGame.Engine.Xml.Parsers.Data; @@ -17,6 +18,8 @@ protected override void Parse(XElement element, IValueListDictionary Parse(XElement element) if (trimmedValued.Length == 0) return Array.Empty(); + if (trimmedValued.Length > 0x2000) + { + Logger?.LogWarning($"Input value is too long '{trimmedValued.Length}' at {XmlLocationInfo.FromElement(element)}"); + return Array.Empty(); + } + var entries = trimmedValued.Split(Separators, StringSplitOptions.RemoveEmptyEntries); return entries; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlMax100ByteParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlMax100ByteParser.cs new file mode 100644 index 0000000..7c767b3 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlMax100ByteParser.cs @@ -0,0 +1,37 @@ +using System; +using System.Xml.Linq; +using Microsoft.Extensions.Logging; + +namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; + +public sealed class PetroglyphXmlMax100ByteParser : PetroglyphXmlPrimitiveElementParser +{ + internal PetroglyphXmlMax100ByteParser(IServiceProvider serviceProvider) : base(serviceProvider) + { + + } + + public override byte Parse(XElement element) + { + var intValue = PrimitiveParserProvider.IntParser.Parse(element); + + if (intValue > 100) + intValue = 100; + + var asByte = (byte)intValue; + if (intValue != asByte) + { + var location = XmlLocationInfo.FromElement(element); + Logger?.LogWarning($"Expected a byte value (0 - 255) but got value '{intValue}' at {location}"); + } + + // Add additional check, cause the PG implementation is broken, but we need to stay "bug-compatible". + if (asByte > 100) + { + var location = XmlLocationInfo.FromElement(element); + Logger?.LogWarning($"Expected a byte value (0 - 100) but got value '{asByte}' at {location}"); + } + + return asByte; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PrimitiveParserProvider.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PrimitiveParserProvider.cs index d749197..57f5f15 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PrimitiveParserProvider.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PrimitiveParserProvider.cs @@ -10,6 +10,7 @@ internal class PrimitiveParserProvider(IServiceProvider serviceProvider) : IPrim private readonly Lazy _lazyIntParser = new(() => new PetroglyphXmlIntegerParser(serviceProvider)); private readonly Lazy _lazyFloatParser = new(() => new PetroglyphXmlFloatParser(serviceProvider)); private readonly Lazy _lazyByteParser = new(() => new PetroglyphXmlByteParser(serviceProvider)); + private readonly Lazy _lazyMax100ByteParser = new(() => new PetroglyphXmlMax100ByteParser(serviceProvider)); private readonly Lazy _lazyBoolParser = new(() => new PetroglyphXmlBooleanParser(serviceProvider)); private readonly Lazy _lazyCommaStringKeyListParser = new(() => new CommaSeparatedStringKeyValueListParser(serviceProvider)); @@ -19,6 +20,7 @@ internal class PrimitiveParserProvider(IServiceProvider serviceProvider) : IPrim public PetroglyphXmlIntegerParser IntParser => _lazyIntParser.Value; public PetroglyphXmlFloatParser FloatParser => _lazyFloatParser.Value; public PetroglyphXmlByteParser ByteParser => _lazyByteParser.Value; + public PetroglyphXmlMax100ByteParser Max100ByteParser => _lazyMax100ByteParser.Value; public PetroglyphXmlBooleanParser BooleanParser => _lazyBoolParser.Value; public CommaSeparatedStringKeyValueListParser CommaSeparatedStringKeyValueListParser => _lazyCommaStringKeyListParser.Value; } \ No newline at end of file From 15e3c7f3ba212ac97ab142838b5a10f7c3eb2050 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Fri, 19 Jul 2024 19:52:29 +0200 Subject: [PATCH 20/25] update module --- PetroglyphTools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PetroglyphTools b/PetroglyphTools index aec9119..2710513 160000 --- a/PetroglyphTools +++ b/PetroglyphTools @@ -1 +1 @@ -Subproject commit aec9119d41da5b77ab9695daa7381fe687bd5659 +Subproject commit 271051320757451afeec76cf21d236e85f26f016 From 92a7041f37b5d94b9a72438b483278db1fcba9db Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sat, 20 Jul 2024 11:17:21 +0200 Subject: [PATCH 21/25] add reporting and other stuff --- src/ModVerify.CliApp/ModVerify.CliApp.csproj | 2 +- src/ModVerify.CliApp/Program.cs | 60 ++++- src/ModVerify/GameVerificationException.cs | 13 +- src/ModVerify/GameVerifySettings.cs | 23 -- src/ModVerify/IGameVerifier.cs | 13 ++ src/ModVerify/IVerificationProvider.cs | 5 +- src/ModVerify/ModVerify.csproj | 1 + .../Reporting/AssetsEqualityComparer.cs | 24 ++ .../Reporting/FileBasedReporterSettings.cs | 14 ++ .../GlobalVerificationReportSettings.cs | 8 + .../Reporting/IVerificationReporter.cs | 8 + .../Reporting/Json/JsonSuppressionFilter.cs | 19 ++ .../Reporting/Json/JsonSuppressionList.cs | 16 ++ .../Json/JsonVerificationBaseline.cs | 22 ++ .../Reporting/Json/JsonVerificationError.cs | 41 ++++ .../Reporting/Json/JsonVerificationReport.cs | 10 + .../Reporting/Json/ModVerifyJsonSettings.cs | 11 + .../Reporting/Reporters/ConsoleReporter.cs | 26 +++ .../Reporting/Reporters/FileBasedReporter.cs | 21 ++ .../Reporting/Reporters/JSON/JsonReporter.cs | 20 ++ .../Reporters/JSON/JsonReporterSettings.cs | 3 + .../Reporting/Reporters/ReporterBase.cs | 20 ++ .../Reporters/Text/TextFileReporter.cs | 73 ++++++ .../Text/TextFileReporterSettings.cs | 6 + .../VerificationReportersExtensions.cs | 47 ++++ src/ModVerify/Reporting/SuppressionFilter.cs | 135 +++++++++++ src/ModVerify/Reporting/SuppressionList.cs | 90 ++++++++ .../Reporting/VerificationBaseline.cs | 53 +++++ src/ModVerify/Reporting/VerificationError.cs | 130 +++++++++++ .../Reporting/VerificationReportBroker.cs | 59 +++++ .../Reporting/VerificationReportSettings.cs | 6 + .../Reporting/VerificationSeverity.cs | 21 ++ src/ModVerify/Settings/GameVerifySettings.cs | 21 ++ .../Settings/VerificationAbortSettings.cs | 12 + .../Settings/VerifyLocalizationOption.cs | 9 + src/ModVerify/Steps/GameVerificationStep.cs | 91 -------- src/ModVerify/VerificationError.cs | 50 ----- src/ModVerify/VerificationProvider.cs | 10 +- src/ModVerify/Verifiers/AudioFilesVerifier.cs | 209 ++++++++++++++++++ .../DuplicateNameFinder.cs} | 30 ++- src/ModVerify/Verifiers/GameVerifierBase.cs | 71 ++++++ .../ReferencedModelsVerifier.cs} | 109 ++++++--- src/ModVerify/Verifiers/VerifierErrorCodes.cs | 25 +++ src/ModVerify/VerifyGamePipeline.cs | 30 ++- src/ModVerify/VerifyThrowBehavior.cs | 8 - .../Database/GameDatabase.cs | 6 +- .../Database/GameDatabaseService.cs | 2 +- .../Database/IGameDatabase.cs | 14 +- .../Database/IGameDatabaseService.cs | 5 +- .../GameDatabaseCreationPipeline.cs | 6 +- .../Language/EawGameLanguageManager.cs | 15 ++ .../Language/FocGameLanguageManager.cs | 22 ++ .../Language/GameLanguageManager.cs | 128 +++++++---- .../Language/GameLanguageManagerProvider.cs | 19 ++ .../Language/IGameLanguageManager.cs | 15 +- .../Language/IGameLanguageManagerProvider.cs | 6 + .../PG.StarWarsGame.Engine.csproj | 8 +- .../PetroglyphEngineServiceContribution.cs | 2 +- .../Repositories/AudioRepository.cs | 6 - .../Repositories/EffectsRepository.cs | 65 ++---- .../Repositories/GameRepository.cs | 92 +++++++- .../Repositories/IGameRepository.cs | 5 + .../Repositories/MultiPassRepository.cs | 46 ++++ .../Repositories/TextureRepository.cs | 64 ++++++ .../Binary/Reader/ParticleReaderV1.cs | 119 +++++++++- .../Binary/Reader/ParticleReaderV2.cs | 5 +- .../Data/AlamoParticle.cs | 8 +- .../Binary/Metadata/ChunkType.cs | 10 +- .../Binary/Reader/ChunkReader.cs | 21 +- 69 files changed, 1991 insertions(+), 373 deletions(-) delete mode 100644 src/ModVerify/GameVerifySettings.cs create mode 100644 src/ModVerify/IGameVerifier.cs create mode 100644 src/ModVerify/Reporting/AssetsEqualityComparer.cs create mode 100644 src/ModVerify/Reporting/FileBasedReporterSettings.cs create mode 100644 src/ModVerify/Reporting/GlobalVerificationReportSettings.cs create mode 100644 src/ModVerify/Reporting/IVerificationReporter.cs create mode 100644 src/ModVerify/Reporting/Json/JsonSuppressionFilter.cs create mode 100644 src/ModVerify/Reporting/Json/JsonSuppressionList.cs create mode 100644 src/ModVerify/Reporting/Json/JsonVerificationBaseline.cs create mode 100644 src/ModVerify/Reporting/Json/JsonVerificationError.cs create mode 100644 src/ModVerify/Reporting/Json/JsonVerificationReport.cs create mode 100644 src/ModVerify/Reporting/Json/ModVerifyJsonSettings.cs create mode 100644 src/ModVerify/Reporting/Reporters/ConsoleReporter.cs create mode 100644 src/ModVerify/Reporting/Reporters/FileBasedReporter.cs create mode 100644 src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs create mode 100644 src/ModVerify/Reporting/Reporters/JSON/JsonReporterSettings.cs create mode 100644 src/ModVerify/Reporting/Reporters/ReporterBase.cs create mode 100644 src/ModVerify/Reporting/Reporters/Text/TextFileReporter.cs create mode 100644 src/ModVerify/Reporting/Reporters/Text/TextFileReporterSettings.cs create mode 100644 src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs create mode 100644 src/ModVerify/Reporting/SuppressionFilter.cs create mode 100644 src/ModVerify/Reporting/SuppressionList.cs create mode 100644 src/ModVerify/Reporting/VerificationBaseline.cs create mode 100644 src/ModVerify/Reporting/VerificationError.cs create mode 100644 src/ModVerify/Reporting/VerificationReportBroker.cs create mode 100644 src/ModVerify/Reporting/VerificationReportSettings.cs create mode 100644 src/ModVerify/Reporting/VerificationSeverity.cs create mode 100644 src/ModVerify/Settings/GameVerifySettings.cs create mode 100644 src/ModVerify/Settings/VerificationAbortSettings.cs create mode 100644 src/ModVerify/Settings/VerifyLocalizationOption.cs delete mode 100644 src/ModVerify/Steps/GameVerificationStep.cs delete mode 100644 src/ModVerify/VerificationError.cs create mode 100644 src/ModVerify/Verifiers/AudioFilesVerifier.cs rename src/ModVerify/{Steps/DuplicateFinderStep.cs => Verifiers/DuplicateNameFinder.cs} (51%) create mode 100644 src/ModVerify/Verifiers/GameVerifierBase.cs rename src/ModVerify/{Steps/VerifyReferencedModelsStep.cs => Verifiers/ReferencedModelsVerifier.cs} (56%) create mode 100644 src/ModVerify/Verifiers/VerifierErrorCodes.cs delete mode 100644 src/ModVerify/VerifyThrowBehavior.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Language/EawGameLanguageManager.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Language/FocGameLanguageManager.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManagerProvider.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Language/IGameLanguageManagerProvider.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/AudioRepository.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/MultiPassRepository.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/TextureRepository.cs diff --git a/src/ModVerify.CliApp/ModVerify.CliApp.csproj b/src/ModVerify.CliApp/ModVerify.CliApp.csproj index 9d56615..4100a37 100644 --- a/src/ModVerify.CliApp/ModVerify.CliApp.csproj +++ b/src/ModVerify.CliApp/ModVerify.CliApp.csproj @@ -35,7 +35,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/ModVerify.CliApp/Program.cs b/src/ModVerify.CliApp/Program.cs index 9e925c3..2af2dc2 100644 --- a/src/ModVerify.CliApp/Program.cs +++ b/src/ModVerify.CliApp/Program.cs @@ -6,7 +6,10 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; using AET.ModVerify; -using AET.ModVerify.Steps; +using AET.ModVerify.Reporting; +using AET.ModVerify.Reporting.Reporters; +using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers; using AET.SteamAbstraction; using AnakinRaW.CommonUtilities.FileSystem; using AnakinRaW.CommonUtilities.Hashing; @@ -23,6 +26,7 @@ using PG.StarWarsGame.Files.ALO; using PG.StarWarsGame.Files.DAT.Services.Builder; using PG.StarWarsGame.Files.MEG.Data.Archives; +using PG.StarWarsGame.Files.XML; using PG.StarWarsGame.Infrastructure; using PG.StarWarsGame.Infrastructure.Clients; using PG.StarWarsGame.Infrastructure.Games; @@ -125,10 +129,50 @@ private static VerifyGamePipeline BuildPipeline(IPlayableObject playableObject, playableObject.Game.Directory.FullName, fallbackGame.Directory.FullName); + var settings = BuildSettings(); + - return new ModVerifyPipeline(GameEngineType.Foc, gameLocations, GameVerifySettings.Default, _services); + return new ModVerifyPipeline(GameEngineType.Foc, gameLocations, settings, _services); } + + private static GameVerifySettings BuildSettings() + { + var settings = GameVerifySettings.Default; + + var reportSettings = settings.GlobalReportSettings; + + if (_options.Baseline is not null) + { + using var fs = _services.GetRequiredService().FileStream + .New(_options.Baseline, FileMode.Open, FileAccess.Read); + var baseline = VerificationBaseline.FromJson(fs); + + reportSettings = reportSettings with + { + Baseline = baseline + }; + } + + if (_options.Suppressions is not null) + { + using var fs = _services.GetRequiredService().FileStream + .New(_options.Suppressions, FileMode.Open, FileAccess.Read); + var baseline = SuppressionList.FromJson(fs); + + reportSettings = reportSettings with + { + Suppressions = baseline + }; + } + + return settings with + { + GlobalReportSettings = reportSettings + }; + } + + private static IServiceProvider CreateAppServices() { var fileSystem = new FileSystem(); @@ -148,9 +192,13 @@ private static IServiceProvider CreateAppServices() RuntimeHelpers.RunClassConstructor(typeof(IMegArchive).TypeHandle); AloServiceContribution.ContributeServices(serviceCollection); serviceCollection.CollectPgServiceContributions(); + XmlServiceContribution.ContributeServices(serviceCollection); PetroglyphEngineServiceContribution.ContributeServices(serviceCollection); ModVerifyServiceContribution.ContributeServices(serviceCollection); + serviceCollection.RegisterConsoleReporter(); + serviceCollection.RegisterJsonReporter(); + serviceCollection.RegisterTextFileReporter(); return serviceCollection.BuildServiceProvider(); } @@ -216,6 +264,12 @@ internal class CliOptions [Option('p', "path", Required = false, HelpText = "The path to a mod directory to verify.")] public string Path { get; set; } + + [Option("baseline", Required = false, HelpText = "Path to a JSON baseline file.")] + public string? Baseline { get; set; } + + [Option("suppressions", Required = false, HelpText = "Path to a JSON suppression file.")] + public string? Suppressions { get; set; } } } @@ -226,7 +280,7 @@ internal class ModVerifyPipeline( IServiceProvider serviceProvider) : VerifyGamePipeline(targetType, gameLocations, settings, serviceProvider) { - protected override IEnumerable CreateVerificationSteps(IGameDatabase database) + protected override IEnumerable CreateVerificationSteps(IGameDatabase database) { var verifyProvider = ServiceProvider.GetRequiredService(); return verifyProvider.GetAllDefaultVerifiers(database, Settings); diff --git a/src/ModVerify/GameVerificationException.cs b/src/ModVerify/GameVerificationException.cs index 0844557..64415d9 100644 --- a/src/ModVerify/GameVerificationException.cs +++ b/src/ModVerify/GameVerificationException.cs @@ -1,17 +1,16 @@ using System; using System.Collections.Generic; using System.Text; -using AET.ModVerify.Steps; -using AnakinRaW.CommonUtilities.SimplePipeline; +using AET.ModVerify.Reporting; namespace AET.ModVerify; -public sealed class GameVerificationException(IEnumerable failedSteps) : Exception +public sealed class GameVerificationException(IEnumerable errors) : Exception { private readonly string? _error = null; - private readonly IEnumerable _failedSteps = failedSteps ?? throw new ArgumentNullException(nameof(failedSteps)); + private readonly IEnumerable _errors = errors ?? throw new ArgumentNullException(nameof(errors)); - public GameVerificationException(GameVerificationStep step) : this([step]) + public GameVerificationException(VerificationError error) : this([error]) { } @@ -26,8 +25,8 @@ private string Error return _error; var stringBuilder = new StringBuilder(); - foreach (var step in _failedSteps) - stringBuilder.Append($"Verification step '{step}' has errors;"); + foreach (var error in _errors) + stringBuilder.AppendLine($"Verification error: {error.Id}:{error.Message};"); return stringBuilder.ToString().TrimEnd(';'); } } diff --git a/src/ModVerify/GameVerifySettings.cs b/src/ModVerify/GameVerifySettings.cs deleted file mode 100644 index 9ccb73e..0000000 --- a/src/ModVerify/GameVerifySettings.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace AET.ModVerify; - -public record GameVerifySettings -{ - public int ParallelWorkers { get; init; } = 4; - - public static readonly GameVerifySettings Default = new() - { - ThrowBehavior = VerifyThrowBehavior.None - }; - - public VerifyThrowBehavior ThrowBehavior { get; init; } - - public VerifyLocalizationOption VerifyLocalization { get; init; } -} - -public enum VerifyLocalizationOption -{ - English, - CurrentSystem, - AllInstalled, - All -} \ No newline at end of file diff --git a/src/ModVerify/IGameVerifier.cs b/src/ModVerify/IGameVerifier.cs new file mode 100644 index 0000000..1184c86 --- /dev/null +++ b/src/ModVerify/IGameVerifier.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using AET.ModVerify.Reporting; + +namespace AET.ModVerify; + +public interface IGameVerifier +{ + string Name { get; } + + string FriendlyName { get; } + + IReadOnlyCollection VerifyErrors { get; } +} \ No newline at end of file diff --git a/src/ModVerify/IVerificationProvider.cs b/src/ModVerify/IVerificationProvider.cs index ccd3266..2d87ab5 100644 --- a/src/ModVerify/IVerificationProvider.cs +++ b/src/ModVerify/IVerificationProvider.cs @@ -1,10 +1,11 @@ using System.Collections.Generic; -using AET.ModVerify.Steps; +using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers; using PG.StarWarsGame.Engine.Database; namespace AET.ModVerify; public interface IVerificationProvider { - IEnumerable GetAllDefaultVerifiers(IGameDatabase database, GameVerifySettings settings); + IEnumerable GetAllDefaultVerifiers(IGameDatabase database, GameVerifySettings settings); } \ No newline at end of file diff --git a/src/ModVerify/ModVerify.csproj b/src/ModVerify/ModVerify.csproj index 0903c9e..d6ce1ec 100644 --- a/src/ModVerify/ModVerify.csproj +++ b/src/ModVerify/ModVerify.csproj @@ -30,6 +30,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/ModVerify/Reporting/AssetsEqualityComparer.cs b/src/ModVerify/Reporting/AssetsEqualityComparer.cs new file mode 100644 index 0000000..8f058bc --- /dev/null +++ b/src/ModVerify/Reporting/AssetsEqualityComparer.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace AET.ModVerify.Reporting; + +internal class AssetsEqualityComparer : IEqualityComparer> +{ + readonly IEqualityComparer> _setComparer = HashSet.CreateSetComparer(); + + public static AssetsEqualityComparer Instance { get; } = new(); + + private AssetsEqualityComparer() + { + } + + public bool Equals(HashSet x, HashSet y) + { + return _setComparer.Equals(x, y); + } + + public int GetHashCode(HashSet obj) + { + return _setComparer.GetHashCode(obj); + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/FileBasedReporterSettings.cs b/src/ModVerify/Reporting/FileBasedReporterSettings.cs new file mode 100644 index 0000000..6616258 --- /dev/null +++ b/src/ModVerify/Reporting/FileBasedReporterSettings.cs @@ -0,0 +1,14 @@ +using System; + +namespace AET.ModVerify.Reporting; + +public record FileBasedReporterSettings : VerificationReportSettings +{ + private readonly string _outputDirectory = Environment.CurrentDirectory; + + public string OutputDirectory + { + get => _outputDirectory; + init => _outputDirectory = string.IsNullOrEmpty(value) ? Environment.CurrentDirectory : value; + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/GlobalVerificationReportSettings.cs b/src/ModVerify/Reporting/GlobalVerificationReportSettings.cs new file mode 100644 index 0000000..a94225e --- /dev/null +++ b/src/ModVerify/Reporting/GlobalVerificationReportSettings.cs @@ -0,0 +1,8 @@ +namespace AET.ModVerify.Reporting; + +public record GlobalVerificationReportSettings : VerificationReportSettings +{ + 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/IVerificationReporter.cs b/src/ModVerify/Reporting/IVerificationReporter.cs new file mode 100644 index 0000000..a1a9c48 --- /dev/null +++ b/src/ModVerify/Reporting/IVerificationReporter.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace AET.ModVerify.Reporting; + +public interface IVerificationReporter +{ + public void Report(IReadOnlyCollection errors); +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonSuppressionFilter.cs b/src/ModVerify/Reporting/Json/JsonSuppressionFilter.cs new file mode 100644 index 0000000..92047d8 --- /dev/null +++ b/src/ModVerify/Reporting/Json/JsonSuppressionFilter.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AET.ModVerify.Reporting.Json; + +internal class JsonSuppressionFilter(SuppressionFilter filter) +{ + [JsonPropertyName("id")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Id { get; } = filter.Id; + + [JsonPropertyName("Verifier")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Verifier { get; } = filter.Verifier; + + [JsonPropertyName("assets")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IEnumerable? Assets { get; } = filter.Assets; +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonSuppressionList.cs b/src/ModVerify/Reporting/Json/JsonSuppressionList.cs new file mode 100644 index 0000000..1a8cb4a --- /dev/null +++ b/src/ModVerify/Reporting/Json/JsonSuppressionList.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace AET.ModVerify.Reporting.Json; + +internal class JsonSuppressionList +{ + [JsonPropertyName("suppressions")] + public ICollection Filters { get; } + + public JsonSuppressionList(IEnumerable suppressionList) + { + Filters = suppressionList.Select(x => new JsonSuppressionFilter(x)).ToList(); + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonVerificationBaseline.cs b/src/ModVerify/Reporting/Json/JsonVerificationBaseline.cs new file mode 100644 index 0000000..303e168 --- /dev/null +++ b/src/ModVerify/Reporting/Json/JsonVerificationBaseline.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace AET.ModVerify.Reporting.Json; + +internal class JsonVerificationBaseline +{ + [JsonPropertyName("errors")] + public IEnumerable Errors { get; } + + public JsonVerificationBaseline(VerificationBaseline baseline) + { + Errors = baseline.Select(x => new JsonVerificationError(x)); + } + + [JsonConstructor] + private JsonVerificationBaseline(IEnumerable errors) + { + Errors = errors; + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonVerificationError.cs b/src/ModVerify/Reporting/Json/JsonVerificationError.cs new file mode 100644 index 0000000..9c69d8d --- /dev/null +++ b/src/ModVerify/Reporting/Json/JsonVerificationError.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AET.ModVerify.Reporting.Json; + +internal class JsonVerificationError +{ + [JsonPropertyName("id")] + public string Id { get; } + + [JsonPropertyName("verifier")] + public string Verifier { get; } + + [JsonPropertyName("message")] + public string Message { get; } + + [JsonPropertyName("severity")] + public VerificationSeverity Severity { get; } + + [JsonPropertyName("assets")] + public IEnumerable Assets { get; } + + [JsonConstructor] + private JsonVerificationError(string id, string verifier, string message, VerificationSeverity severity, IEnumerable assets) + { + Id = id; + Verifier = verifier; + Message = message; + Severity = severity; + Assets = assets; + } + + public JsonVerificationError(VerificationError error) + { + Id = error.Id; + Verifier = error.Verifier; + Message = error.Message; + Severity = error.Severity; + Assets = error.AffectedAssets; + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/JsonVerificationReport.cs b/src/ModVerify/Reporting/Json/JsonVerificationReport.cs new file mode 100644 index 0000000..6ebb926 --- /dev/null +++ b/src/ModVerify/Reporting/Json/JsonVerificationReport.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AET.ModVerify.Reporting.Json; + +internal class JsonVerificationReport(IEnumerable errors) +{ + [JsonPropertyName("errors")] + public IEnumerable Errors { get; } = errors; +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Json/ModVerifyJsonSettings.cs b/src/ModVerify/Reporting/Json/ModVerifyJsonSettings.cs new file mode 100644 index 0000000..054f0ad --- /dev/null +++ b/src/ModVerify/Reporting/Json/ModVerifyJsonSettings.cs @@ -0,0 +1,11 @@ +using System.Text.Json; + +namespace AET.ModVerify.Reporting.Json; + +internal static class ModVerifyJsonSettings +{ + public static readonly JsonSerializerOptions JsonSettings = new() + { + WriteIndented = true + }; +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/ConsoleReporter.cs b/src/ModVerify/Reporting/Reporters/ConsoleReporter.cs new file mode 100644 index 0000000..9dd7f30 --- /dev/null +++ b/src/ModVerify/Reporting/Reporters/ConsoleReporter.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AET.ModVerify.Reporting.Reporters; + +internal class ConsoleReporter(VerificationReportSettings settings, IServiceProvider serviceProvider) : ReporterBase(settings, serviceProvider) +{ + public override void Report(IReadOnlyCollection errors) + { + var filteredErrors = FilteredErrors(errors).ToList(); + + Console.WriteLine(); + Console.WriteLine("GAME VERIFICATION RESULT"); + Console.WriteLine($"Errors of severity {Settings.MinimumReportSeverity}: {filteredErrors.Count}"); + Console.WriteLine(); + + if (filteredErrors.Count == 0) + Console.WriteLine("No errors!"); + + foreach (var error in filteredErrors) + Console.WriteLine(error); + + Console.WriteLine(); + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/FileBasedReporter.cs b/src/ModVerify/Reporting/Reporters/FileBasedReporter.cs new file mode 100644 index 0000000..d0213d0 --- /dev/null +++ b/src/ModVerify/Reporting/Reporters/FileBasedReporter.cs @@ -0,0 +1,21 @@ +using System; +using System.IO; +using System.IO.Abstractions; +using Microsoft.Extensions.DependencyInjection; + +namespace AET.ModVerify.Reporting.Reporters; + +public abstract class FileBasedReporter(T settings, IServiceProvider serviceProvider) : ReporterBase(settings, serviceProvider) where T : FileBasedReporterSettings +{ + private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); + + protected Stream CreateFile(string fileName) + { + var outputDirectory = Settings.OutputDirectory; + _fileSystem.Directory.CreateDirectory(outputDirectory); + + var filePath = _fileSystem.Path.Combine(outputDirectory, fileName); + + return _fileSystem.FileStream.New(_fileSystem.Path.GetFullPath(filePath), FileMode.Create, FileAccess.Write); + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs b/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs new file mode 100644 index 0000000..abbe5f0 --- /dev/null +++ b/src/ModVerify/Reporting/Reporters/JSON/JsonReporter.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using AET.ModVerify.Reporting.Json; + +namespace AET.ModVerify.Reporting.Reporters.JSON; + +internal class JsonReporter(JsonReporterSettings settings, IServiceProvider serviceProvider) : FileBasedReporter(settings, serviceProvider) +{ + public const string FileName = "VerificationResult.json"; + + + public override void Report(IReadOnlyCollection errors) + { + var report = new JsonVerificationReport(errors.Select(x => new JsonVerificationError(x))); + using var fs = CreateFile(FileName); + JsonSerializer.Serialize(fs, report, ModVerifyJsonSettings.JsonSettings); + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/JSON/JsonReporterSettings.cs b/src/ModVerify/Reporting/Reporters/JSON/JsonReporterSettings.cs new file mode 100644 index 0000000..fd5962d --- /dev/null +++ b/src/ModVerify/Reporting/Reporters/JSON/JsonReporterSettings.cs @@ -0,0 +1,3 @@ +namespace AET.ModVerify.Reporting.Reporters.JSON; + +public record JsonReporterSettings : FileBasedReporterSettings; \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/ReporterBase.cs b/src/ModVerify/Reporting/Reporters/ReporterBase.cs new file mode 100644 index 0000000..d7945af --- /dev/null +++ b/src/ModVerify/Reporting/Reporters/ReporterBase.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AET.ModVerify.Reporting.Reporters; + +public abstract class ReporterBase(T settings, IServiceProvider serviceProvider) : IVerificationReporter where T : VerificationReportSettings +{ + protected IServiceProvider ServiceProvider { get; } = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + + protected T Settings { get; } = settings ?? throw new ArgumentNullException(nameof(settings)); + + + public abstract void Report(IReadOnlyCollection errors); + + protected IEnumerable FilteredErrors(IReadOnlyCollection errors) + { + return errors.Where(x => x.Severity >= Settings.MinimumReportSeverity); + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/Text/TextFileReporter.cs b/src/ModVerify/Reporting/Reporters/Text/TextFileReporter.cs new file mode 100644 index 0000000..86ce6d4 --- /dev/null +++ b/src/ModVerify/Reporting/Reporters/Text/TextFileReporter.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; + +namespace AET.ModVerify.Reporting.Reporters.Text; + +internal class TextFileReporter(TextFileReporterSettings settings, IServiceProvider serviceProvider) : FileBasedReporter(settings, serviceProvider) +{ + internal const string SingleReportFileName = "VerificationResult.txt"; + + private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); + + public override void Report(IReadOnlyCollection errors) + { + if (Settings.SplitIntoFiles) + ReportByVerifier(errors); + else + ReportWhole(errors); + } + + private void ReportWhole(IReadOnlyCollection errors) + { + using var streamWriter = new StreamWriter(CreateFile(SingleReportFileName)); + + foreach (var error in errors.OrderBy(x => x.Id)) + WriteError(error, streamWriter); + + } + + private void ReportByVerifier(IReadOnlyCollection errors) + { + var grouped = errors.GroupBy(x => x.Verifier); + foreach (var group in grouped) + ReportToSingleFile(group); + } + + private void ReportToSingleFile(IGrouping group) + { + var fileName = $"{GetVerifierName(group.Key)}Results.txt"; + using var streamWriter = new StreamWriter(CreateFile(fileName)); + foreach (var error in group.OrderBy(x => x.Id)) + WriteError(error, streamWriter); + } + + private static string GetVerifierName(string verifierTypeName) + { + var typeNameSpan = verifierTypeName.AsSpan(); + var nameIndex = typeNameSpan.LastIndexOf('.'); + + if (nameIndex == -1) + 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) + return name.ToString().Replace('/', '.'); + + return name.ToString(); + } + + private static void WriteError(VerificationError error, StreamWriter writer) + { + writer.WriteLine($"[{error.Id}] {error.Message}"); + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/Text/TextFileReporterSettings.cs b/src/ModVerify/Reporting/Reporters/Text/TextFileReporterSettings.cs new file mode 100644 index 0000000..e6847a3 --- /dev/null +++ b/src/ModVerify/Reporting/Reporters/Text/TextFileReporterSettings.cs @@ -0,0 +1,6 @@ +namespace AET.ModVerify.Reporting.Reporters.Text; + +public record TextFileReporterSettings : FileBasedReporterSettings +{ + public bool SplitIntoFiles { get; init; } = true; +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs b/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs new file mode 100644 index 0000000..04f7c7d --- /dev/null +++ b/src/ModVerify/Reporting/Reporters/VerificationReportersExtensions.cs @@ -0,0 +1,47 @@ +using AET.ModVerify.Reporting.Reporters.JSON; +using AET.ModVerify.Reporting.Reporters.Text; +using Microsoft.Extensions.DependencyInjection; + +namespace AET.ModVerify.Reporting.Reporters; + +public static class VerificationReportersExtensions +{ + public static IServiceCollection RegisterJsonReporter(this IServiceCollection serviceCollection) + { + return RegisterJsonReporter(serviceCollection, new JsonReporterSettings + { + OutputDirectory = "." + }); + } + + public static IServiceCollection RegisterTextFileReporter(this IServiceCollection serviceCollection) + { + return RegisterTextFileReporter(serviceCollection, new TextFileReporterSettings + { + OutputDirectory = "." + }); + } + + public static IServiceCollection RegisterConsoleReporter(this IServiceCollection serviceCollection) + { + return RegisterConsoleReporter(serviceCollection, new VerificationReportSettings + { + MinimumReportSeverity = VerificationSeverity.Error + }); + } + + public static IServiceCollection RegisterJsonReporter(this IServiceCollection serviceCollection, JsonReporterSettings settings) + { + return serviceCollection.AddSingleton(sp => new JsonReporter(settings, sp)); + } + + public static IServiceCollection RegisterTextFileReporter(this IServiceCollection serviceCollection, TextFileReporterSettings settings) + { + return serviceCollection.AddSingleton(sp => new TextFileReporter(settings, sp)); + } + + public static IServiceCollection RegisterConsoleReporter(this IServiceCollection serviceCollection, VerificationReportSettings settings) + { + return serviceCollection.AddSingleton(sp => new ConsoleReporter(settings, sp)); + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/SuppressionFilter.cs b/src/ModVerify/Reporting/SuppressionFilter.cs new file mode 100644 index 0000000..7fb41de --- /dev/null +++ b/src/ModVerify/Reporting/SuppressionFilter.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AET.ModVerify.Reporting.Json; + +namespace AET.ModVerify.Reporting; + +public sealed class SuppressionFilter : IEquatable +{ + private static readonly AssetsEqualityComparer AssetComparer = AssetsEqualityComparer.Instance; + + private readonly HashSet? _assets; + + public string? Id { get; } + + public string? Verifier { get; } + + public IReadOnlyCollection? Assets { get; } + + public SuppressionFilter(string? id, string? verifier, ICollection? assets) + { + Id = id; + Verifier = verifier; + if (assets is not null) + _assets = new HashSet(assets); + Assets = _assets?.ToList() ?? null; + } + + internal SuppressionFilter(JsonSuppressionFilter filter) + { + Id = filter.Id; + Verifier = filter.Verifier; + + if (filter.Assets is not null) + _assets = new HashSet(filter.Assets); + Assets = _assets?.ToList() ?? null; + } + + public bool Suppresses(VerificationError error) + { + var suppresses = false; + + if (Id is not null) + { + if (Id.Equals(error.Id)) + suppresses = true; + else + return false; + } + + if (Verifier is not null) + { + if (Verifier.Equals(error.Verifier)) + suppresses = true; + else + return false; + } + + if (_assets is not null) + { + if (_assets.Count != error.AffectedAssets.Count) + return false; + + if (!_assets.SetEquals(error.AffectedAssets)) + return false; + + suppresses = true; + } + + return suppresses; + } + + public bool IsDisabled() + { + return Id == null && Verifier == null && (Assets == null || !Assets.Any()); + } + + public int Specificity() + { + var specificity = 0; + if (Id != null) + specificity++; + if (Verifier != null) + specificity++; + if (Assets != null && Assets.Any()) + specificity++; + return specificity; + } + + public bool IsSupersededBy(SuppressionFilter other) + { + if (Id != null && other.Id != null && other.Id != Id) + return false; + + if (Verifier != null && other.Verifier != null && other.Verifier != Verifier) + return false; + + if (Assets != null && other.Assets != null && !AssetComparer.Equals(_assets, other._assets)) + return false; + + return other.Specificity() < Specificity(); + } + + public bool Equals(SuppressionFilter? other) + { + if (ReferenceEquals(null, other)) + return false; + if (ReferenceEquals(this, other)) + return true; + + if (Id != other.Id) + return false; + if (Verifier != other.Verifier) + return false; + + return AssetComparer.Equals(_assets, other._assets); + } + + public override bool Equals(object? obj) + { + return obj is SuppressionFilter other && Equals(other); + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + hashCode.Add(Id); + hashCode.Add(Verifier); + if (_assets is not null) + hashCode.Add(_assets, AssetComparer); + else + hashCode.Add((HashSet)null!); + return hashCode.ToHashCode(); + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/SuppressionList.cs b/src/ModVerify/Reporting/SuppressionList.cs new file mode 100644 index 0000000..23d1563 --- /dev/null +++ b/src/ModVerify/Reporting/SuppressionList.cs @@ -0,0 +1,90 @@ +using AET.ModVerify.Reporting.Json; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; + +namespace AET.ModVerify.Reporting; + +public sealed class SuppressionList : IReadOnlyCollection +{ + public static readonly SuppressionList Empty = new([]); + + private readonly IReadOnlyCollection _filters; + private readonly IReadOnlyCollection _minimizedFilters; + + public int Count => _filters.Count; + + public SuppressionList(IEnumerable suppressionFilters) + { + if (suppressionFilters == null) + throw new ArgumentNullException(nameof(suppressionFilters)); + + _filters = new List(suppressionFilters); + _minimizedFilters = MinimizeSuppressions(_filters); + } + + internal SuppressionList(JsonSuppressionList suppressionList) + { + if (suppressionList == null) + throw new ArgumentNullException(nameof(suppressionList)); + + _filters = suppressionList.Filters.Select(x => new SuppressionFilter(x)).ToList(); + _minimizedFilters = MinimizeSuppressions(_filters); + } + + public void ToJson(Stream stream) + { + JsonSerializer.Serialize(stream, new JsonSuppressionList(this), ModVerifyJsonSettings.JsonSettings); + } + + public static SuppressionList FromJson(Stream stream) + { + var baselineJson = JsonSerializer.Deserialize(stream, JsonSerializerOptions.Default); + if (baselineJson is null) + throw new InvalidOperationException("Unable to deserialize baseline"); + return new SuppressionList(baselineJson); + } + + + public bool Suppresses(VerificationError error) + { + foreach (var filter in _minimizedFilters) + { + if (filter.Suppresses(error)) + { + return true; + } + } + + return false; + } + + private static IReadOnlyCollection MinimizeSuppressions(IEnumerable filters) + { + var sortedFilters = filters.Where(f => !f.IsDisabled()) + .OrderBy(x => x.Specificity()); + + var result = new List(); + + foreach (var filter in sortedFilters) + { + if (result.All(x => !filter.IsSupersededBy(x))) + result.Add(filter); + } + + return result; + } + + public IEnumerator GetEnumerator() + { + return _filters.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/VerificationBaseline.cs b/src/ModVerify/Reporting/VerificationBaseline.cs new file mode 100644 index 0000000..3305b6f --- /dev/null +++ b/src/ModVerify/Reporting/VerificationBaseline.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using AET.ModVerify.Reporting.Json; + +namespace AET.ModVerify.Reporting; + +public sealed class VerificationBaseline : IReadOnlyCollection +{ + public static readonly VerificationBaseline Empty = new([]); + + private readonly HashSet _errors; + + /// + public int Count => _errors.Count; + + internal VerificationBaseline(JsonVerificationBaseline baseline) + { + _errors = new HashSet(baseline.Errors.Select(x => new VerificationError(x))); + } + + public VerificationBaseline(IEnumerable errors) + { + _errors = new(errors); + } + + public void ToJson(Stream stream) + { + JsonSerializer.Serialize(stream, new JsonVerificationBaseline(this), ModVerifyJsonSettings.JsonSettings); + } + + public static VerificationBaseline FromJson(Stream stream) + { + var baselineJson = JsonSerializer.Deserialize(stream, JsonSerializerOptions.Default); + if (baselineJson is null) + throw new InvalidOperationException("Unable to deserialize baseline"); + return new VerificationBaseline(baselineJson); + } + + /// + public IEnumerator GetEnumerator() + { + return _errors.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/VerificationError.cs b/src/ModVerify/Reporting/VerificationError.cs new file mode 100644 index 0000000..fa25cf3 --- /dev/null +++ b/src/ModVerify/Reporting/VerificationError.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AET.ModVerify.Reporting.Json; +using AnakinRaW.CommonUtilities; + +namespace AET.ModVerify.Reporting; + +public sealed class VerificationError : IEquatable +{ + private static readonly AssetsEqualityComparer AssetComparer = AssetsEqualityComparer.Instance; + + private readonly HashSet _assets; + + public string Id { get; } + + public string Message { get; } + + public string Verifier { get; } + + public IReadOnlyCollection AffectedAssets { get; } + + public VerificationSeverity Severity { get; } + + public VerificationError(string id, string message, string verifier, IEnumerable affectedAssets, VerificationSeverity severity) + { + if (affectedAssets == null) + throw new ArgumentNullException(nameof(affectedAssets)); + ThrowHelper.ThrowIfNullOrEmpty(id); + Id = id; + Message = message ?? throw new ArgumentNullException(nameof(message)); + Verifier = verifier; + Severity = severity; + _assets = new HashSet(affectedAssets); + AffectedAssets = _assets.ToList(); + } + + internal VerificationError(JsonVerificationError error) + { + Id = error.Id; + Message = error.Message; + Verifier = error.Verifier; + _assets = new HashSet(error.Assets); + AffectedAssets = _assets.ToList(); + } + + public static VerificationError Create( + IGameVerifier verifier, + string id, + string message, + VerificationSeverity severity, + IEnumerable assets) + { + return new VerificationError(id, message, verifier.Name, assets, severity); + } + + + public static VerificationError Create( + IGameVerifier verifier, + string id, + string message, + VerificationSeverity severity, + params string[] assets) + { + return new VerificationError(id, message, verifier.Name, assets, severity); + } + + public static VerificationError Create( + IGameVerifier verifier, + string id, + string message, + VerificationSeverity severity) + { + return Create(verifier, id, message, severity, []); + } + + internal static VerificationError Create( + IGameVerifier verifier, + string id, + Exception exception, + VerificationSeverity severity, + params string[] assets) + { + return new VerificationError(id, exception.Message, verifier.Name, assets, severity); + } + + internal static VerificationError Create( + IGameVerifier verifier, + string id, + Exception exception, + VerificationSeverity severity) + { + return Create(verifier, id, exception, severity, []); + } + + + public bool Equals(VerificationError? other) + { + if (other is null) + return false; + if (ReferenceEquals(this, other)) + return true; + + if (!Id.Equals(other.Id)) + return false; + if (!Verifier.Equals(other.Verifier)) + return false; + + return AssetComparer.Equals(_assets, other._assets); + } + + public override bool Equals(object? obj) + { + return obj is VerificationError other && Equals(other); + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + hashCode.Add(Id); + hashCode.Add(Verifier); + hashCode.Add(_assets, AssetComparer); + return hashCode.ToHashCode(); + } + + public override string ToString() + { + return $"[{Severity}] [{Verifier}] {Id}: Message={Message}; Affected Assets=[{string.Join(",", AffectedAssets)}];"; + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/VerificationReportBroker.cs b/src/ModVerify/Reporting/VerificationReportBroker.cs new file mode 100644 index 0000000..57e7a03 --- /dev/null +++ b/src/ModVerify/Reporting/VerificationReportBroker.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AET.ModVerify.Verifiers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace AET.ModVerify.Reporting; + +internal class VerificationReportBroker(GlobalVerificationReportSettings reportSettings, IServiceProvider serviceProvider) +{ + private readonly ILogger? _logger = serviceProvider.GetService()?.CreateLogger(typeof(VerificationReportBroker)); + + + public IReadOnlyCollection Report(IEnumerable steps) + { + var suppressions = new SuppressionList(reportSettings.Suppressions); + + var errors = GetReportableErrors(steps, suppressions); + + var reporters = serviceProvider.GetServices(); + + + foreach (var reporter in reporters) + { + try + { + reporter.Report(errors); + } + catch (Exception e) + { + _logger?.LogError(e, "Exception while reporting verification error"); + } + } + + return errors; + } + + private IReadOnlyCollection GetReportableErrors(IEnumerable steps, SuppressionList suppressions) + { + var allErrors = steps.SelectMany(s => s.VerifyErrors); + + var errorsToReport = new List(); + foreach (var error in allErrors) + { + if (reportSettings.Baseline.Contains(error)) + continue; + + if (suppressions.Suppresses(error)) + continue; + + errorsToReport.Add(error); + } + + // NB: We don't filter for severity here, as that is something the individual reporters should handle. + // This allows better control over what gets reported. + return errorsToReport; + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/VerificationReportSettings.cs b/src/ModVerify/Reporting/VerificationReportSettings.cs new file mode 100644 index 0000000..fc02024 --- /dev/null +++ b/src/ModVerify/Reporting/VerificationReportSettings.cs @@ -0,0 +1,6 @@ +namespace AET.ModVerify.Reporting; + +public record VerificationReportSettings +{ + public VerificationSeverity MinimumReportSeverity { get; init; } = VerificationSeverity.Information; +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/VerificationSeverity.cs b/src/ModVerify/Reporting/VerificationSeverity.cs new file mode 100644 index 0000000..f6617e9 --- /dev/null +++ b/src/ModVerify/Reporting/VerificationSeverity.cs @@ -0,0 +1,21 @@ +namespace AET.ModVerify.Reporting; + +public enum VerificationSeverity +{ + /// + /// Indicates that a finding is most likely not affecting the game. + /// + Information = 0, + /// + /// Indicates that a finding might cause undefined behavior or unpredictable results are to be expected. + /// + Warning, + /// + /// Indicates that a finding will most likely not function as expected in the game. + /// + Error, + /// + /// Indicates that a finding will most likely cause a CTD of the game. + /// + Critical +} \ No newline at end of file diff --git a/src/ModVerify/Settings/GameVerifySettings.cs b/src/ModVerify/Settings/GameVerifySettings.cs new file mode 100644 index 0000000..cd95a09 --- /dev/null +++ b/src/ModVerify/Settings/GameVerifySettings.cs @@ -0,0 +1,21 @@ +using AET.ModVerify.Reporting; + +namespace AET.ModVerify.Settings; + +public record GameVerifySettings +{ + public int ParallelVerifiers { get; init; } = 4; + + public static readonly GameVerifySettings Default = new() + { + AbortSettings = new(), + GlobalReportSettings = new(), + LocalizationOption = VerifyLocalizationOption.English + }; + + public VerificationAbortSettings AbortSettings { get; init; } + + public GlobalVerificationReportSettings GlobalReportSettings { get; init; } + + public VerifyLocalizationOption LocalizationOption { get; init; } +} \ No newline at end of file diff --git a/src/ModVerify/Settings/VerificationAbortSettings.cs b/src/ModVerify/Settings/VerificationAbortSettings.cs new file mode 100644 index 0000000..767cef4 --- /dev/null +++ b/src/ModVerify/Settings/VerificationAbortSettings.cs @@ -0,0 +1,12 @@ +using AET.ModVerify.Reporting; + +namespace AET.ModVerify.Settings; + +public record VerificationAbortSettings +{ + public bool FailFast { get; init; } = false; + + public VerificationSeverity MinimumAbortSeverity { get; init; } = VerificationSeverity.Warning; + + public bool ThrowsGameVerificationException { get; init; } = false; +} \ No newline at end of file diff --git a/src/ModVerify/Settings/VerifyLocalizationOption.cs b/src/ModVerify/Settings/VerifyLocalizationOption.cs new file mode 100644 index 0000000..1c0ee7f --- /dev/null +++ b/src/ModVerify/Settings/VerifyLocalizationOption.cs @@ -0,0 +1,9 @@ +namespace AET.ModVerify.Settings; + +public enum VerifyLocalizationOption +{ + English, + CurrentSystem, + AllInstalled, + All +} \ No newline at end of file diff --git a/src/ModVerify/Steps/GameVerificationStep.cs b/src/ModVerify/Steps/GameVerificationStep.cs deleted file mode 100644 index ebc27d3..0000000 --- a/src/ModVerify/Steps/GameVerificationStep.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions; -using System.Threading; -using AnakinRaW.CommonUtilities.SimplePipeline.Steps; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using PG.StarWarsGame.Engine.Database; -using PG.StarWarsGame.Engine.Repositories; - -namespace AET.ModVerify.Steps; - -public abstract class GameVerificationStep( - IGameDatabase gameDatabase, - GameVerifySettings settings, - IServiceProvider serviceProvider) - : PipelineStep(serviceProvider) -{ - protected readonly IFileSystem FileSystem = serviceProvider.GetRequiredService(); - private readonly List _verifyErrors = new(); - - private StreamWriter _errorLog = null!; - - public IReadOnlyCollection VerifyErrors => _verifyErrors; - - protected GameVerifySettings Settings { get; } = settings; - - protected IGameDatabase Database { get; } = gameDatabase ?? throw new ArgumentNullException(nameof(gameDatabase)); - - protected IGameRepository Repository => gameDatabase.GameRepository; - - protected abstract string LogFileName { get; } - - public abstract string Name { get; } - - protected sealed override void RunCore(CancellationToken token) - { - Logger?.LogInformation($"Running verifier '{Name}'..."); - try - { - _errorLog = CreateVerificationLogFile(); - RunVerification(token); - } - finally - { - Logger?.LogInformation($"Finished verifier '{Name}'"); - _errorLog.Dispose(); - _errorLog = null!; - } - } - - protected abstract void RunVerification(CancellationToken token); - - protected void AddError(VerificationError error) - { - if (!OnError(error)) - { - Logger?.LogTrace($"Error suppressed for verifier '{Name}': '{error}'"); - return; - } - _verifyErrors.Add(error); - _errorLog.WriteLine(error.Message); - - if (Settings.ThrowBehavior == VerifyThrowBehavior.FailFast) - throw new GameVerificationException(this); - } - - protected virtual bool OnError(VerificationError error) - { - return true; - } - - protected void GuardedVerify(Action action, Predicate handleException, string errorContext) - { - try - { - action(); - } - catch (Exception e) when (handleException(e)) - { - AddError(VerificationError.CreateFromException(e, errorContext)); - } - } - private StreamWriter CreateVerificationLogFile() - { - var fileName = $"VerifyResult_{LogFileName}.txt"; - var fs = FileSystem.FileStream.New(fileName, FileMode.Create, FileAccess.Write, FileShare.Read); - return new StreamWriter(fs); - } -} \ No newline at end of file diff --git a/src/ModVerify/VerificationError.cs b/src/ModVerify/VerificationError.cs deleted file mode 100644 index b98b7a5..0000000 --- a/src/ModVerify/VerificationError.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using AnakinRaW.CommonUtilities; - -namespace AET.ModVerify; - -public class VerificationError -{ - public const string GeneralExceptionId = "VE01"; - - public string Id { get; } - - public string Message { get; } - - public override string ToString() - { - return $"{Id}: {Message}"; - } - - protected VerificationError(string id, string message) - { - ThrowHelper.ThrowIfNullOrEmpty(id); - Id = id; - Message = message ?? throw new ArgumentNullException(nameof(message)); - } - - public static VerificationError Create(string id, string message) - { - return new VerificationError(id, message); - } - - public static VerificationError CreateFromException(Exception exception, string failedOperation) - { - return Create(GeneralExceptionId, $"Verification of {failedOperation} caused an {exception.GetType().Name}: {exception.Message}"); - } -} - -public class VerificationError : VerificationError -{ - public T Context { get; } - - protected VerificationError(string id, string message, T context) : base(id, message) - { - Context = context; - } - - public static VerificationError Create(string id, string message, T context) - { - return new VerificationError(id, message, context); - } -} \ No newline at end of file diff --git a/src/ModVerify/VerificationProvider.cs b/src/ModVerify/VerificationProvider.cs index 625faf5..a1800dc 100644 --- a/src/ModVerify/VerificationProvider.cs +++ b/src/ModVerify/VerificationProvider.cs @@ -1,15 +1,17 @@ using System; using System.Collections.Generic; -using AET.ModVerify.Steps; +using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers; using PG.StarWarsGame.Engine.Database; namespace AET.ModVerify; internal class VerificationProvider(IServiceProvider serviceProvider) : IVerificationProvider { - public IEnumerable GetAllDefaultVerifiers(IGameDatabase database, GameVerifySettings settings) + public IEnumerable GetAllDefaultVerifiers(IGameDatabase database, GameVerifySettings settings) { - yield return new VerifyReferencedModelsStep(database, settings, serviceProvider); - yield return new DuplicateFinderStep(database, settings, serviceProvider); + yield return new ReferencedModelsVerifier(database, settings, serviceProvider); + yield return new DuplicateNameFinder(database, settings, serviceProvider); + yield return new AudioFilesVerifier(database, settings, serviceProvider); } } \ No newline at end of file diff --git a/src/ModVerify/Verifiers/AudioFilesVerifier.cs b/src/ModVerify/Verifiers/AudioFilesVerifier.cs new file mode 100644 index 0000000..1f4a3e9 --- /dev/null +++ b/src/ModVerify/Verifiers/AudioFilesVerifier.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using System.Threading; +using AET.ModVerify.Reporting; +using AET.ModVerify.Settings; +using Microsoft.Extensions.DependencyInjection; +using PG.Commons.Hashing; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Engine.Database; +using PG.StarWarsGame.Engine.DataTypes; +using PG.StarWarsGame.Engine.Language; +using PG.StarWarsGame.Files.MEG.Services.Builder.Normalization; +#if NETSTANDARD2_0 +using AnakinRaW.CommonUtilities.FileSystem; +#endif + +namespace AET.ModVerify.Verifiers; + +public class AudioFilesVerifier : GameVerifierBase +{ + private readonly PetroglyphDataEntryPathNormalizer _pathNormalizer; + private readonly ICrc32HashingService _hashingService; + private readonly IFileSystem _fileSystem; + private readonly IGameLanguageManager _languageManager; + + public AudioFilesVerifier(IGameDatabase gameDatabase, GameVerifySettings settings, IServiceProvider serviceProvider) : base(gameDatabase, settings, serviceProvider) + { + _pathNormalizer = new(serviceProvider); + _hashingService = serviceProvider.GetRequiredService(); + _fileSystem = serviceProvider.GetRequiredService(); + _languageManager = serviceProvider.GetRequiredService() + .GetLanguageManager(Repository.EngineType); + } + + public override string FriendlyName => "Verify Audio Files"; + + protected override void RunVerification(CancellationToken token) + { + var visitedSamples = new HashSet(); + var languagesToVerify = GetLanguagesToVerify().ToList(); + foreach (var sfxEvent in Database.SfxEvents.Entries) + { + foreach (var codedSample in sfxEvent.AllSamples) + { + VerifySample(codedSample, sfxEvent, languagesToVerify, visitedSamples); + } + } + } + + private void VerifySample(string sample, SfxEvent sfxEvent, IEnumerable languagesToVerify, HashSet visitedSamples) + { + Span sampleNameBuffer = stackalloc char[PGConstants.MaxPathLength]; + + var i = _pathNormalizer.Normalize(sample.AsSpan(), sampleNameBuffer); + var normalizedSampleName = sampleNameBuffer.Slice(0, i); + var crc = _hashingService.GetCrc32(normalizedSampleName, PGConstants.PGCrc32Encoding); + if (!visitedSamples.Add(crc)) + return; + + + if (normalizedSampleName.Length > PGConstants.MaxPathLength) + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.FilePathTooLong, + $"Sample name '{sample}' is too long.", + VerificationSeverity.Error, + sample)); + return; + } + + var normalizedSampleNameString = normalizedSampleName.ToString(); + + if (sfxEvent.IsLocalized) + { + foreach (var language in languagesToVerify) + { + var localizedSampleName = _languageManager.LocalizeFileName(normalizedSampleNameString, language, out var localized); + VerifySample(localizedSampleName, sfxEvent); + + if (!localized) + return; + } + } + else + { + VerifySample(normalizedSampleNameString, sfxEvent); + } + } + + private void VerifySample(string sample, SfxEvent sfxEvent) + { + using var sampleStream = Repository.TryOpenFile(sample); + if (sampleStream is null) + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.SampleNotFound, + $"Audio file '{sample}' could not be found.", + VerificationSeverity.Error, + sample)); + 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 '{sample}' has an invalid format '{format}'. Supported is {WaveFormats.PCM}", + VerificationSeverity.Error, + sample)); + } + + if (channels > 1 && !IsAmbient2D(sfxEvent)) + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.SampleNotMono, + $"Audio file '{sample}' is not mono audio.", + VerificationSeverity.Information, + sample)); + } + + if (sampleRate > 48_000) + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes. InvalidSampleRate, + $"Audio file '{sample}' has a too high sample rate of {sampleRate}. Maximum is 48.000Hz.", + VerificationSeverity.Error, + sample)); + } + + if (bitPerSecondPerChannel > 16) + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.InvalidBitsPerSeconds, + $"Audio file '{sample}' has an invalid bit size of {bitPerSecondPerChannel}. Supported are 16bit.", + VerificationSeverity.Error, + sample)); + } + } + + // 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 Database.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/Steps/DuplicateFinderStep.cs b/src/ModVerify/Verifiers/DuplicateNameFinder.cs similarity index 51% rename from src/ModVerify/Steps/DuplicateFinderStep.cs rename to src/ModVerify/Verifiers/DuplicateNameFinder.cs index f3473dd..14d2e67 100644 --- a/src/ModVerify/Steps/DuplicateFinderStep.cs +++ b/src/ModVerify/Verifiers/DuplicateNameFinder.cs @@ -1,23 +1,21 @@ using System; using System.Linq; using System.Threading; +using AET.ModVerify.Reporting; +using AET.ModVerify.Settings; using AnakinRaW.CommonUtilities.Collections; using PG.StarWarsGame.Engine.Database; using PG.StarWarsGame.Engine.DataTypes; -namespace AET.ModVerify.Steps; +namespace AET.ModVerify.Verifiers; -public sealed class DuplicateFinderStep( +public sealed class DuplicateNameFinder( IGameDatabase gameDatabase, GameVerifySettings settings, IServiceProvider serviceProvider) - : GameVerificationStep(gameDatabase, settings, serviceProvider) + : GameVerifierBase(gameDatabase, settings, serviceProvider) { - public const string DuplicateFound = "DUP00"; - - protected override string LogFileName => "Duplicates"; - - public override string Name => "Duplicate Definitions"; + public override string FriendlyName => "Duplicate Definitions"; protected override void RunVerification(CancellationToken token) { @@ -25,21 +23,29 @@ protected override void RunVerification(CancellationToken token) CheckDatabaseForDuplicates(Database.SfxEvents, "SFXEvent"); } - private void CheckDatabaseForDuplicates(IXmlDatabase database, string context) where T : XmlObject + private void CheckDatabaseForDuplicates(IXmlDatabase database, string databaseName) where T : XmlObject { foreach (var key in database.EntryKeys) { var entries = database.GetEntries(key); if (entries.Count > 1) - AddError(VerificationError.Create(DuplicateFound, CreateDuplicateErrorMessage(context, entries))); + { + var entryNames = entries.Select(x => x.Name); + AddError(VerificationError.Create( + this, + VerifierErrorCodes.DuplicateFound, + CreateDuplicateErrorMessage(databaseName, entries), + VerificationSeverity.Warning, + entryNames)); + } } } - private string CreateDuplicateErrorMessage(string context, ReadOnlyFrugalList entries) where T : XmlObject + private string CreateDuplicateErrorMessage(string databaseName, ReadOnlyFrugalList entries) where T : XmlObject { var firstEntry = entries.First(); - var message = $"{context} '{firstEntry.Name}' ({firstEntry.Crc32}) has duplicate definitions: "; + 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}] "; diff --git a/src/ModVerify/Verifiers/GameVerifierBase.cs b/src/ModVerify/Verifiers/GameVerifierBase.cs new file mode 100644 index 0000000..fff7dca --- /dev/null +++ b/src/ModVerify/Verifiers/GameVerifierBase.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Threading; +using AET.ModVerify.Reporting; +using AET.ModVerify.Settings; +using AnakinRaW.CommonUtilities.SimplePipeline.Steps; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine.Database; +using PG.StarWarsGame.Engine.Repositories; + +namespace AET.ModVerify.Verifiers; + +public abstract class GameVerifierBase( + IGameDatabase gameDatabase, + GameVerifySettings settings, + IServiceProvider serviceProvider) + : PipelineStep(serviceProvider), IGameVerifier +{ + + protected readonly IFileSystem FileSystem = serviceProvider.GetRequiredService(); + + private readonly HashSet _verifyErrors = new(); + + public IReadOnlyCollection VerifyErrors => _verifyErrors; + + protected GameVerifySettings Settings { get; } = settings; + + protected IGameDatabase Database { get; } = gameDatabase ?? throw new ArgumentNullException(nameof(gameDatabase)); + + protected IGameRepository Repository => gameDatabase.GameRepository; + + public virtual string FriendlyName => GetType().Name; + + public string Name => GetType().FullName; + + protected sealed override void RunCore(CancellationToken token) + { + Logger?.LogInformation($"Running verifier '{FriendlyName}'..."); + try + { + RunVerification(token); + } + finally + { + Logger?.LogInformation($"Finished verifier '{FriendlyName}'"); + } + } + + protected abstract void RunVerification(CancellationToken token); + + protected void AddError(VerificationError error) + { + _verifyErrors.Add(error); + if (Settings.AbortSettings.FailFast && error.Severity >= Settings.AbortSettings.MinimumAbortSeverity) + throw new GameVerificationException(error); + } + + protected void GuardedVerify(Action action, Predicate exceptionFilter, Action exceptionHandler) + { + try + { + action(); + } + catch (Exception e) when (exceptionFilter(e)) + { + exceptionHandler(e); + } + } +} \ No newline at end of file diff --git a/src/ModVerify/Steps/VerifyReferencedModelsStep.cs b/src/ModVerify/Verifiers/ReferencedModelsVerifier.cs similarity index 56% rename from src/ModVerify/Steps/VerifyReferencedModelsStep.cs rename to src/ModVerify/Verifiers/ReferencedModelsVerifier.cs index c2ff800..46fd7e3 100644 --- a/src/ModVerify/Steps/VerifyReferencedModelsStep.cs +++ b/src/ModVerify/Verifiers/ReferencedModelsVerifier.cs @@ -3,6 +3,8 @@ using System.IO; using System.Linq; using System.Threading; +using AET.ModVerify.Reporting; +using AET.ModVerify.Settings; using Microsoft.Extensions.DependencyInjection; using PG.Commons.Binary; using PG.Commons.Files; @@ -13,30 +15,21 @@ using PG.StarWarsGame.Files.ALO.Files.Particles; using PG.StarWarsGame.Files.ALO.Services; using PG.StarWarsGame.Files.ChunkFiles.Data; +using AnakinRaW.CommonUtilities.FileSystem; -namespace AET.ModVerify.Steps; +namespace AET.ModVerify.Verifiers; -public sealed class VerifyReferencedModelsStep( +public sealed class ReferencedModelsVerifier( IGameDatabase database, GameVerifySettings settings, IServiceProvider serviceProvider) - : GameVerificationStep(database, settings, serviceProvider) + : GameVerifierBase(database, settings, serviceProvider) { - public const string ModelNotFound = "ALO00"; - public const string ModelBroken = "ALO01"; - public const string ModelMissingTexture = "ALO02"; - public const string ModelMissingProxy = "ALO03"; - public const string ModelMissingShader = "ALO04"; - private const string ProxyAltIdentifier = "_ALT"; - private static readonly string[] TextureExtensions = ["dds", "tga"]; - private readonly IAloFileService _modelFileService = serviceProvider.GetRequiredService(); - protected override string LogFileName => "ModelRefs"; - - public override string Name => "Referenced Models"; + public override string FriendlyName => "Referenced Models"; protected override void RunVerification(CancellationToken token) { @@ -58,7 +51,13 @@ protected override void RunVerification(CancellationToken token) if (modelStream is null) { - var error = VerificationError.Create(ModelNotFound, $"Unable to find .ALO data: {model}", model); + var error = VerificationError.Create( + this, + VerifierErrorCodes.ModelNotFound, + $"Unable to find .ALO file '{model}'", + VerificationSeverity.Error, + model); + AddError(error); } else @@ -87,12 +86,42 @@ private void VerifyModelOrParticle(Stream modelStream, Queue workingQueu { var aloFile = modelStream.GetFilePath(); var message = $"{aloFile} is corrupted: {e.Message}"; - AddError(VerificationError.Create(ModelBroken, message, aloFile)); + AddError(VerificationError.Create(this, VerifierErrorCodes.ModelBroken, message, VerificationSeverity.Critical, aloFile)); } } - private void VerifyParticle(IAloParticleFile particle) + private void VerifyParticle(IAloParticleFile file) { + foreach (var texture in file.Content.Textures) + { + GuardedVerify(() => VerifyTextureExists(file, texture), + e => e is ArgumentException, + _ => + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.InvalidTexture, + $"Invalid texture file name" + + $" '{texture}' in particle '{file.FilePath}'", + VerificationSeverity.Error, + texture, + file.FilePath)); + }); + } + + var fileName = FileSystem.Path.GetFileNameWithoutExtension(file.FilePath.AsSpan()); + var name = file.Content.Name.AsSpan(); + + if (!fileName.Equals(name, StringComparison.OrdinalIgnoreCase)) + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.InvalidParticleName, + $"The particle name '{file.Content.Name}' does not match file name '{fileName.ToString()}'", + VerificationSeverity.Error, + file.FilePath)); + } + } private void VerifyModel(IAloModelFile file, Queue workingQueue) @@ -101,14 +130,32 @@ private void VerifyModel(IAloModelFile file, Queue workingQueue) { GuardedVerify(() => VerifyTextureExists(file, texture), e => e is ArgumentException, - $"texture '{texture}'"); + _ => + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.InvalidTexture, + $"Invalid texture file name" + + $" '{texture}' in model '{file.FilePath}'", + VerificationSeverity.Error, + texture, file.FilePath)); + }); } foreach (var shader in file.Content.Shaders) { GuardedVerify(() => VerifyShaderExists(file, shader), e => e is ArgumentException, - $"shader '{shader}'"); + _ => + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.InvalidShader, + $"Invalid texture file name" + + $" '{shader}' in model '{file.FilePath}'", + VerificationSeverity.Error, + shader, file.FilePath)); + }); } @@ -116,7 +163,16 @@ private void VerifyModel(IAloModelFile file, Queue workingQueue) { GuardedVerify(() => VerifyProxyExists(file, proxy, workingQueue), e => e is ArgumentException, - $"proxy '{proxy}'"); + _ => + { + AddError(VerificationError.Create( + this, + VerifierErrorCodes.InvalidProxy, + $"Invalid proxy file name" + + $" '{proxy}' in model '{file.FilePath}'", + VerificationSeverity.Error, + proxy, file.FilePath)); + }); } } @@ -124,12 +180,11 @@ private void VerifyTextureExists(IPetroglyphFileHolder - .Create(ModelMissingTexture, message, (model.FilePath, texture)); + var error = VerificationError.Create(this, VerifierErrorCodes.ModelMissingTexture, message, VerificationSeverity.Error, model.FilePath, texture); AddError(error); } } @@ -141,8 +196,7 @@ private void VerifyProxyExists(IPetroglyphFileHolder model, string proxy, Queue< if (!Repository.FileExists(BuildModelPath(particle))) { var message = $"{model.FilePath} references missing proxy particle: {particle}"; - var error = VerificationError<(string Model, string Proxy)> - .Create(ModelMissingProxy, message, (model.FilePath, particle)); + var error = VerificationError.Create(this, VerifierErrorCodes.ModelMissingProxy, message, VerificationSeverity.Error, model.FilePath, particle); AddError(error); } else @@ -162,8 +216,7 @@ private void VerifyShaderExists(IPetroglyphFileHolder data, string shader) if (!Repository.EffectsRepository.FileExists(shader)) { var message = $"{data.FilePath} references missing shader effect: {shader}"; - var error = VerificationError<(string, string)> - .Create(ModelMissingShader, message, (data.FilePath, shader)); + var error = VerificationError.Create(this, VerifierErrorCodes.ModelMissingShader, message, VerificationSeverity.Error, data.FilePath, shader); AddError(error); } } diff --git a/src/ModVerify/Verifiers/VerifierErrorCodes.cs b/src/ModVerify/Verifiers/VerifierErrorCodes.cs new file mode 100644 index 0000000..cb991fe --- /dev/null +++ b/src/ModVerify/Verifiers/VerifierErrorCodes.cs @@ -0,0 +1,25 @@ +namespace AET.ModVerify.Verifiers; + +public static class VerifierErrorCodes +{ + public const string GenericExceptionErrorCode = "MV00"; + + public const string DuplicateFound = "DUP00"; + + public const string SampleNotFound = "WAV00"; + public const string FilePathTooLong = "WAV01"; + public const string SampleNotPCM = "WAV02"; + public const string SampleNotMono = "WAV03"; + public const string InvalidSampleRate = "WAV04"; + public const string InvalidBitsPerSeconds = "WAV05"; + + public const string ModelNotFound = "ALO00"; + public const string ModelBroken = "ALO01"; + public const string ModelMissingTexture = "ALO02"; + public const string ModelMissingProxy = "ALO03"; + public const string ModelMissingShader = "ALO04"; + public const string InvalidTexture = "ALO05"; + public const string InvalidShader = "ALO06"; + public const string InvalidProxy = "ALO07"; + public const string InvalidParticleName = "ALO08"; +} \ No newline at end of file diff --git a/src/ModVerify/VerifyGamePipeline.cs b/src/ModVerify/VerifyGamePipeline.cs index b71204f..4ef01a6 100644 --- a/src/ModVerify/VerifyGamePipeline.cs +++ b/src/ModVerify/VerifyGamePipeline.cs @@ -3,7 +3,9 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using AET.ModVerify.Steps; +using AET.ModVerify.Reporting; +using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers; using AnakinRaW.CommonUtilities.SimplePipeline; using AnakinRaW.CommonUtilities.SimplePipeline.Runners; using Microsoft.Extensions.DependencyInjection; @@ -15,7 +17,7 @@ namespace AET.ModVerify; public abstract class VerifyGamePipeline : Pipeline { - private readonly List _verificationSteps = new(); + private readonly List _verificationSteps = new(); private readonly GameEngineType _targetType; private readonly GameLocations _gameLocations; private readonly ParallelRunner _verifyRunner; @@ -29,10 +31,10 @@ protected VerifyGamePipeline(GameEngineType targetType, GameLocations gameLocati _gameLocations = gameLocations ?? throw new ArgumentNullException(nameof(gameLocations)); Settings = settings ?? throw new ArgumentNullException(nameof(settings)); - if (settings.ParallelWorkers is < 0 or > 64) + if (settings.ParallelVerifiers is < 0 or > 64) throw new ArgumentException("Settings has invalid parallel worker number.", nameof(settings)); - _verifyRunner = new ParallelRunner(settings.ParallelWorkers, serviceProvider); + _verifyRunner = new ParallelRunner(settings.ParallelVerifiers, serviceProvider); } @@ -69,20 +71,14 @@ protected sealed override async Task RunCoreAsync(CancellationToken token) Logger?.LogInformation("Finished Verifying"); } - var stepsWithVerificationErrors = _verificationSteps.Where(x => x.VerifyErrors.Any()).ToList(); - var failedSteps = new List(); - foreach (var verificationStep in _verificationSteps) - { - if (verificationStep.VerifyErrors.Any()) - { - failedSteps.Add(verificationStep); - Logger?.LogWarning($"Verifier '{verificationStep.Name}' reported errors!"); - } - } + Logger?.LogInformation("Reporting Errors..."); - if (Settings.ThrowBehavior == VerifyThrowBehavior.FinalThrow && failedSteps.Count > 0) - throw new GameVerificationException(stepsWithVerificationErrors); + var reportBroker = new VerificationReportBroker(Settings.GlobalReportSettings, ServiceProvider); + var errors = reportBroker.Report(_verificationSteps); + if (Settings.AbortSettings.ThrowsGameVerificationException && + errors.Any(x => x.Severity >= Settings.AbortSettings.MinimumAbortSeverity)) + throw new GameVerificationException(errors); } finally { @@ -90,5 +86,5 @@ protected sealed override async Task RunCoreAsync(CancellationToken token) } } - protected abstract IEnumerable CreateVerificationSteps(IGameDatabase database); + protected abstract IEnumerable CreateVerificationSteps(IGameDatabase database); } \ No newline at end of file diff --git a/src/ModVerify/VerifyThrowBehavior.cs b/src/ModVerify/VerifyThrowBehavior.cs deleted file mode 100644 index d1e9192..0000000 --- a/src/ModVerify/VerifyThrowBehavior.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace AET.ModVerify; - -public enum VerifyThrowBehavior -{ - None, - FinalThrow, - FailFast -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabase.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabase.cs index c23903a..f32f347 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabase.cs @@ -1,4 +1,6 @@ -using PG.StarWarsGame.Engine.DataTypes; +using System.Collections.Generic; +using PG.StarWarsGame.Engine.DataTypes; +using PG.StarWarsGame.Engine.Language; using PG.StarWarsGame.Engine.Repositories; namespace PG.StarWarsGame.Engine.Database; @@ -12,4 +14,6 @@ internal class GameDatabase : IGameDatabase public required IXmlDatabase GameObjects { get; init; } public required IXmlDatabase SfxEvents { get; init; } + + public IEnumerable InstalledLanguages { get; init; } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabaseService.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabaseService.cs index 9386d0c..2c1885f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabaseService.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabaseService.cs @@ -11,7 +11,7 @@ internal class GameDatabaseService(IServiceProvider serviceProvider) : IGameData { public async Task CreateDatabaseAsync( GameEngineType targetEngineType, - GameLocations locations, + GameLocations locations, CancellationToken cancellationToken = default) { var repoFactory = serviceProvider.GetRequiredService(); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabase.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabase.cs index 7716027..15dd8a1 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabase.cs @@ -1,15 +1,19 @@ -using PG.StarWarsGame.Engine.DataTypes; +using System.Collections.Generic; +using PG.StarWarsGame.Engine.DataTypes; +using PG.StarWarsGame.Engine.Language; using PG.StarWarsGame.Engine.Repositories; namespace PG.StarWarsGame.Engine.Database; public interface IGameDatabase { - public IGameRepository GameRepository { get; } + IGameRepository GameRepository { get; } - public GameConstants GameConstants { get; } + GameConstants GameConstants { get; } - public IXmlDatabase GameObjects { get; } + IXmlDatabase GameObjects { get; } - public IXmlDatabase SfxEvents { get; } + IXmlDatabase SfxEvents { get; } + + IEnumerable InstalledLanguages { get; } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabaseService.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabaseService.cs index 21b5971..366a7f7 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabaseService.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabaseService.cs @@ -5,5 +5,8 @@ namespace PG.StarWarsGame.Engine.Database; public interface IGameDatabaseService { - Task CreateDatabaseAsync(GameEngineType targetEngineType, GameLocations locations, CancellationToken cancellationToken = default); + Task CreateDatabaseAsync( + GameEngineType targetEngineType, + GameLocations locations, + CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs index 0bb21db..1d3cd26 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs @@ -12,7 +12,7 @@ namespace PG.StarWarsGame.Engine.Database.Initialization; -internal class GameDatabaseCreationPipeline(GameRepository repository, IServiceProvider serviceProvider) +internal class GameDatabaseCreationPipeline(GameRepository repository, IServiceProvider serviceProvider) : Pipeline(serviceProvider) { private ParseSingletonXmlStep _parseGameConstants = null!; @@ -106,6 +106,9 @@ protected override async Task RunCoreAsync(CancellationToken token) ThrowIfAnyStepsFailed(_parseXmlRunner.Steps); token.ThrowIfCancellationRequested(); + + + var installedLanguages = repository.InitializeInstalledSfxMegFiles(); repository.Seal(); @@ -116,6 +119,7 @@ protected override async Task RunCoreAsync(CancellationToken token) GameConstants = _parseGameConstants.Database, GameObjects = _parseGameObjects.Database, SfxEvents = _parseSfxEvents.Database, + InstalledLanguages = installedLanguages }; } finally diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/EawGameLanguageManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/EawGameLanguageManager.cs new file mode 100644 index 0000000..2c720b4 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/EawGameLanguageManager.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace PG.StarWarsGame.Engine.Language; + +internal sealed class EawGameLanguageManager(IServiceProvider serviceProvider) : GameLanguageManager(serviceProvider) +{ + public override IEnumerable SupportedLanguages { get; } = new HashSet + { + LanguageType.English, LanguageType.German, LanguageType.French, + LanguageType.Spanish, LanguageType.Italian, LanguageType.Japanese, + LanguageType.Korean, LanguageType.Chinese, LanguageType.Russian, + LanguageType.Polish, LanguageType.Thai + }; +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/FocGameLanguageManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/FocGameLanguageManager.cs new file mode 100644 index 0000000..3636c40 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/FocGameLanguageManager.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; + +namespace PG.StarWarsGame.Engine.Language; + +internal sealed class FocGameLanguageManager(IServiceProvider serviceProvider) : GameLanguageManager(serviceProvider) +{ + public override IEnumerable SupportedLanguages { get; } = new HashSet + { + LanguageType.English, LanguageType.German, LanguageType.French, + LanguageType.Spanish, LanguageType.Italian, LanguageType.Russian, + LanguageType.Polish + }; + + public override LanguageType GetLanguagesFromUser() + { + var language = base.GetLanguagesFromUser(); + return language is LanguageType.Thai or LanguageType.Chinese or LanguageType.Japanese or LanguageType.Korean + ? LanguageType.English + : language; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManager.cs index 851cace..ec4d6b2 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManager.cs @@ -1,13 +1,14 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; using PG.Commons.Services; -using PG.StarWarsGame.Engine.Utilities; namespace PG.StarWarsGame.Engine.Language; -// TODO: Manager for each game -internal sealed class GameLanguageManager(IServiceProvider serviceProvider) : ServiceBase(serviceProvider), IGameLanguageManager +internal abstract class GameLanguageManager(IServiceProvider serviceProvider) : ServiceBase(serviceProvider), IGameLanguageManager { private static readonly IDictionary LanguageToFileSuffixMapMp3 = new Dictionary @@ -41,23 +42,7 @@ internal sealed class GameLanguageManager(IServiceProvider serviceProvider) : Se { LanguageType.Thai, "_THA.WAV" }, }; - public IReadOnlyCollection FocSupportedLanguages { get; } = - new HashSet - { - LanguageType.English, LanguageType.German, LanguageType.French, - LanguageType.Spanish, LanguageType.Italian, LanguageType.Russian, - LanguageType.Polish - }; - - public IReadOnlyCollection EawSupportedLanguages { get; } = - new HashSet - { - LanguageType.English, LanguageType.German, LanguageType.French, - LanguageType.Spanish, LanguageType.Italian, LanguageType.Japanese, - LanguageType.Korean, LanguageType.Chinese, LanguageType.Russian, - LanguageType.Polish, LanguageType.Thai - }; - + public abstract IEnumerable SupportedLanguages { get; } public bool TryGetLanguage(string languageName, out LanguageType language) { @@ -65,21 +50,19 @@ public bool TryGetLanguage(string languageName, out LanguageType language) return Enum.TryParse(languageName, true, out language); } - public bool IsFileNameLocalizable(string fileName, bool requiredEnglishName) + public bool IsFileNameLocalizable(ReadOnlySpan fileName, bool requiredEnglishName) { - var fileSpan = fileName.AsSpan(); - if (requiredEnglishName) { - if (fileSpan.EndsWith("_ENG.WAV".AsSpan(), StringComparison.OrdinalIgnoreCase)) + if (fileName.EndsWith("_ENG.WAV".AsSpan(), StringComparison.OrdinalIgnoreCase)) return true; - if (fileSpan.EndsWith("_ENG.MP3".AsSpan(), StringComparison.OrdinalIgnoreCase)) + if (fileName.EndsWith("_ENG.MP3".AsSpan(), StringComparison.OrdinalIgnoreCase)) return true; return false; } - var isWav = fileSpan.EndsWith(".WAV".AsSpan(), StringComparison.OrdinalIgnoreCase); - var isMp3 = fileSpan.EndsWith(".MP3".AsSpan(), StringComparison.OrdinalIgnoreCase); + var isWav = fileName.EndsWith(".WAV".AsSpan(), StringComparison.OrdinalIgnoreCase); + var isMp3 = fileName.EndsWith(".MP3".AsSpan(), StringComparison.OrdinalIgnoreCase); ICollection? checkList = null; if (isWav) @@ -92,27 +75,83 @@ public bool IsFileNameLocalizable(string fileName, bool requiredEnglishName) foreach (var toCheck in checkList) { - if (fileSpan.EndsWith(toCheck.AsSpan(), StringComparison.OrdinalIgnoreCase)) + if (fileName.EndsWith(toCheck.AsSpan(), StringComparison.OrdinalIgnoreCase)) return true; } return false; } + public virtual LanguageType GetLanguagesFromUser() + { + CultureInfo culture; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var langId = GetUserDefaultUILanguage(); + culture = new CultureInfo(langId); + } + else + culture = CultureInfo.CurrentCulture; + + var rootCulture = GetParentCultureRecursive(culture); + + var cultureName = rootCulture.EnglishName; + + if (TryGetLanguage(cultureName, out var language)) + return language; + return LanguageType.English; + } + + private CultureInfo GetParentCultureRecursive(CultureInfo culture) + { + var parent = culture.Parent; + if (parent.Equals(CultureInfo.InvariantCulture)) + return culture; + return GetParentCultureRecursive(parent); + } + public string LocalizeFileName(string fileName, LanguageType language, out bool localized) { if (fileName.Length > PGConstants.MaxPathLength) throw new ArgumentOutOfRangeException(nameof(fileName), "fileName is too long"); - localized = true; - // The game assumes that all localized audio files are referenced by using their english name. // Thus, PG takes this shortcut if (language == LanguageType.English) + { + localized = true; + return fileName; + } + + + Span localizedName = stackalloc char[fileName.Length]; + var length = LocalizeFileName(fileName.AsSpan(), language, localizedName, out localized); + if (!localized) return fileName; - var fileSpan = fileName.AsSpan(); + Debug.Assert(localizedName.Length == length); + return localizedName.ToString(); + } + + + public int LocalizeFileName(ReadOnlySpan fileName, LanguageType language, Span destination, out bool localized) + { + if (fileName.Length > PGConstants.MaxPathLength) + throw new ArgumentOutOfRangeException(nameof(fileName), "fileName is too long"); + + if (destination.Length < fileName.Length) + throw new ArgumentException("destination is too short", nameof(destination)); + + localized = true; + + // The game assumes that all localized audio files are referenced by using their english name. + // Thus, PG takes this shortcut + if (language == LanguageType.English) + { + fileName.CopyTo(destination); + return fileName.Length; + } var isWav = false; var isMp3 = false; @@ -120,14 +159,14 @@ public string LocalizeFileName(string fileName, LanguageType language, out bool // The game only localizes file names iff they have the english suffix // NB: Also note that the engine does *not* check whether the filename actually ends with this suffix // but instead only take the first occurrence. This means that a file name like 'test_eng.wav_ger.wav' will trick the algorithm. - var engSuffixIndex = fileSpan.IndexOf("_ENG.WAV".AsSpan(), StringComparison.OrdinalIgnoreCase); + var engSuffixIndex = fileName.IndexOf("_ENG.WAV".AsSpan(), StringComparison.OrdinalIgnoreCase); if (engSuffixIndex != -1) isWav = true; if (!isWav) { - engSuffixIndex = fileSpan.IndexOf("_ENG.MP3".AsSpan(), StringComparison.OrdinalIgnoreCase); - if (engSuffixIndex != -1) + engSuffixIndex = fileName.IndexOf("_ENG.MP3".AsSpan(), StringComparison.OrdinalIgnoreCase); + if (engSuffixIndex != -1) isMp3 = true; } @@ -135,12 +174,13 @@ public string LocalizeFileName(string fileName, LanguageType language, out bool if (engSuffixIndex == -1) { localized = false; - Logger.LogWarning($"Unable to localize '{fileName}'"); - return fileName; + Logger.LogWarning($"Unable to localize '{fileName.ToString()}'"); + fileName.CopyTo(destination); + return fileName.Length; } - var withoutSuffix = fileSpan.Slice(0, engSuffixIndex); - + var withoutSuffix = fileName.Slice(0, engSuffixIndex); + ReadOnlySpan newLocalizedSuffix; if (isWav) newLocalizedSuffix = LanguageToFileSuffixMapWav[language].AsSpan(); @@ -149,11 +189,13 @@ public string LocalizeFileName(string fileName, LanguageType language, out bool else throw new InvalidOperationException(); - var sb = new ValueStringBuilder(stackalloc char[PGConstants.MaxPathLength]); - - sb.Append(withoutSuffix); - sb.Append(newLocalizedSuffix); + withoutSuffix.CopyTo(destination); + newLocalizedSuffix.CopyTo(destination.Slice(withoutSuffix.Length, newLocalizedSuffix.Length)); - return sb.ToString(); + return fileName.Length; } + + + [DllImport("Kernel32.dll", CharSet = CharSet.Auto)] + static extern ushort GetUserDefaultUILanguage(); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManagerProvider.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManagerProvider.cs new file mode 100644 index 0000000..b6b00e8 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/GameLanguageManagerProvider.cs @@ -0,0 +1,19 @@ +using System; + +namespace PG.StarWarsGame.Engine.Language; + +internal class GameLanguageManagerProvider(IServiceProvider serviceProvider) : IGameLanguageManagerProvider +{ + private readonly Lazy _eawLanguageManager = new(() => new EawGameLanguageManager(serviceProvider)); + private readonly Lazy _focLanguageManager = new(() => new FocGameLanguageManager(serviceProvider)); + + public IGameLanguageManager GetLanguageManager(GameEngineType engine) + { + return engine switch + { + GameEngineType.Eaw => _eawLanguageManager.Value, + GameEngineType.Foc => _focLanguageManager.Value, + _ => throw new InvalidOperationException($"Engine '{engine}' not supported!") + }; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/IGameLanguageManager.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/IGameLanguageManager.cs index d335112..b0e03da 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/IGameLanguageManager.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/IGameLanguageManager.cs @@ -1,16 +1,19 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace PG.StarWarsGame.Engine.Language; public interface IGameLanguageManager -{ - IReadOnlyCollection FocSupportedLanguages { get; } - - IReadOnlyCollection EawSupportedLanguages { get; } +{ + IEnumerable SupportedLanguages { get; } bool TryGetLanguage(string languageName, out LanguageType language); string LocalizeFileName(string fileName, LanguageType language, out bool localized); - bool IsFileNameLocalizable(string fileName, bool requireEnglishName); + int LocalizeFileName(ReadOnlySpan fileName, LanguageType language, Span destination, out bool localized); + + bool IsFileNameLocalizable(ReadOnlySpan fileName, bool requireEnglishName); + + LanguageType GetLanguagesFromUser(); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/IGameLanguageManagerProvider.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/IGameLanguageManagerProvider.cs new file mode 100644 index 0000000..c47d0a4 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Language/IGameLanguageManagerProvider.cs @@ -0,0 +1,6 @@ +namespace PG.StarWarsGame.Engine.Language; + +public interface IGameLanguageManagerProvider +{ + IGameLanguageManager GetLanguageManager(GameEngineType engine); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj index ddd8497..705e571 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj @@ -16,6 +16,11 @@ snupkg true + + + + + @@ -36,7 +41,4 @@ - - - \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs index 4b02a65..def842e 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs @@ -11,7 +11,7 @@ public static class PetroglyphEngineServiceContribution public static void ContributeServices(IServiceCollection serviceCollection) { serviceCollection.AddSingleton(sp => new GameRepositoryFactory(sp)); - serviceCollection.AddSingleton(sp => new GameLanguageManager(sp)); + serviceCollection.AddSingleton(sp => new GameLanguageManagerProvider(sp)); serviceCollection.AddSingleton(sp => new PetroglyphXmlFileParserFactory(sp)); serviceCollection.AddSingleton(sp => new GameDatabaseService(sp)); } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/AudioRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/AudioRepository.cs deleted file mode 100644 index 134a732..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/AudioRepository.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace PG.StarWarsGame.Engine.Repositories; - -public class AudioRepository -{ - -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/EffectsRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/EffectsRepository.cs index 933c373..42e73ce 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/EffectsRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/EffectsRepository.cs @@ -1,15 +1,11 @@ using System; -using System.IO; -using System.IO.Abstractions; +using System.Diagnostics.CodeAnalysis; using System.Linq; -using Microsoft.Extensions.DependencyInjection; namespace PG.StarWarsGame.Engine.Repositories; -public class EffectsRepository(IGameRepository baseRepository, IServiceProvider serviceProvider) : IRepository +public class EffectsRepository(IGameRepository baseRepository, IServiceProvider serviceProvider) : MultiPassRepository(baseRepository, serviceProvider) { - private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); - private static readonly string[] LookupPaths = [ "Data\\Art\\Shaders", @@ -19,60 +15,27 @@ public class EffectsRepository(IGameRepository baseRepository, IServiceProvider private static readonly string[] ShaderExtensions = [".fx", ".fxo", ".fxh"]; - public Stream OpenFile(string filePath, bool megFileOnly = false) + [return:MaybeNull] + protected override T MultiPassAction(string inputPath, Func fileAction) { - var shaderStream = TryOpenFile(filePath, megFileOnly); - if (shaderStream is null) - throw new FileNotFoundException($"Unable to find game data: {filePath}"); - return shaderStream; - } - - public bool FileExists(string filePath, bool megFileOnly = false) - { - var result = false; - ShaderFileAction(filePath, shaderPath => - { - if (!baseRepository.FileExists(shaderPath, megFileOnly)) - return false; - - result = true; - return true; - - }); - return result; - } - - public Stream? TryOpenFile(string filePath, bool megFileOnly = false) - { - Stream? shaderStream = null; - ShaderFileAction(filePath, shaderPath => - { - var stream = baseRepository.TryOpenFile(shaderPath, megFileOnly); - if (stream is null) - return false; - shaderStream = stream; - return true; - }); - - return shaderStream; - } - - private void ShaderFileAction(string filePath, Predicate action) - { - var currExt = _fileSystem.Path.GetExtension(filePath); + var currExt = FileSystem.Path.GetExtension(inputPath); if (!ShaderExtensions.Contains(currExt, StringComparer.OrdinalIgnoreCase)) - throw new ArgumentException("Invalid data extension for shader. Must be .fx, .fxh or .fxo", nameof(filePath)); + throw new ArgumentException("Invalid data extension for shader. Must be .fx, .fxh or .fxo", nameof(inputPath)); foreach (var directory in LookupPaths) { - var lookupPath = _fileSystem.Path.Combine(directory, filePath); + var lookupPath = FileSystem.Path.Combine(directory, inputPath); foreach (var ext in ShaderExtensions) { - lookupPath = _fileSystem.Path.ChangeExtension(lookupPath, ext); - if (action(lookupPath)) - return; + lookupPath = FileSystem.Path.ChangeExtension(lookupPath, ext); + + var actionResult = fileAction(lookupPath); + if (actionResult.success) + return actionResult.result; } } + + return default; } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs index 30d648d..84f931b 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging; using PG.Commons.Hashing; using PG.Commons.Services; +using PG.StarWarsGame.Engine.Language; using PG.StarWarsGame.Engine.Utilities; using PG.StarWarsGame.Engine.Xml; using PG.StarWarsGame.Files.MEG.Data.Archives; @@ -29,6 +30,7 @@ internal abstract class GameRepository : ServiceBase, IGameRepository private readonly PetroglyphDataEntryPathNormalizer _megPathNormalizer; private readonly ICrc32HashingService _crc32HashingService; private readonly IVirtualMegArchiveBuilder _virtualMegBuilder; + private readonly IGameLanguageManagerProvider _languageManagerProvider; protected readonly string GameDirectory; @@ -40,7 +42,10 @@ internal abstract class GameRepository : ServiceBase, IGameRepository public abstract GameEngineType EngineType { get; } public IRepository EffectsRepository { get; } - + public IRepository TextureRepository { get; } + + + private readonly List _loadedMegFiles = new(); protected IVirtualMegArchive? MasterMegArchive { get; private set; } protected GameRepository(GameLocations gameLocations, IServiceProvider serviceProvider) : base(serviceProvider) @@ -53,6 +58,7 @@ protected GameRepository(GameLocations gameLocations, IServiceProvider servicePr _virtualMegBuilder = serviceProvider.GetRequiredService(); _crc32HashingService = serviceProvider.GetRequiredService(); _megPathNormalizer = new PetroglyphDataEntryPathNormalizer(serviceProvider); + _languageManagerProvider = serviceProvider.GetRequiredService(); foreach (var mod in gameLocations.ModPaths) { @@ -76,6 +82,7 @@ protected GameRepository(GameLocations gameLocations, IServiceProvider servicePr } EffectsRepository = new EffectsRepository(this, serviceProvider); + TextureRepository = new TextureRepository(this, serviceProvider); } @@ -92,6 +99,8 @@ public void AddMegFiles(IList megFiles) new MegDataEntryReference(new MegDataEntryLocationReference(megFile, entry)))); MasterMegArchive = _virtualMegBuilder.BuildFrom(MasterMegArchive.ToList().Concat(newLocations), true); } + + _loadedMegFiles.AddRange(megFiles.Select(x => x.FilePath)); } public void AddMegFile(string megFile) @@ -194,6 +203,67 @@ public IEnumerable FindFiles(string searchPattern, bool megFileOnly = fa return files; } + public bool IsLanguageInstalled(LanguageType language) + { + // A language is considered to be installed if its Text, Speech and localized 2d file exists in the current game + var languageFiles = new LanguageFiles(language); + + if (!FileExists(languageFiles.MasterTextDatFilePath)) + return false; + + if (!FileExists(languageFiles.Sfx2dMegFilePath)) + return false; + + + foreach (var loadedMegFile in _loadedMegFiles) + { + var file = FileSystem.Path.GetFileName(loadedMegFile.AsSpan()); + var speechFileName = languageFiles.SpeechMegFileName.AsSpan(); + + if (file.Equals(speechFileName, StringComparison.OrdinalIgnoreCase)) + return true; + } + + return false; + } + + public IEnumerable InitializeInstalledSfxMegFiles() + { + ThrowIfSealed(); + + var firstFallback = FallbackPaths.FirstOrDefault(); + if (firstFallback is not null) + { + AddMegFile(FileSystem.Path.Combine(firstFallback, "DATA\\AUDIO\\SFX\\SFX2D_NON_LOCALIZED.MEG")); + AddMegFile(FileSystem.Path.Combine(firstFallback, "DATA\\AUDIO\\SFX\\SFX3D_NON_LOCALIZED.MEG")); + } + + AddMegFile("DATA\\AUDIO\\SFX\\SFX2D_NON_LOCALIZED.MEG"); + AddMegFile("DATA\\AUDIO\\SFX\\SFX3D_NON_LOCALIZED.MEG"); + + + var languageManager = _languageManagerProvider.GetLanguageManager(EngineType); + var languages = new List(); + foreach (var language in languageManager.SupportedLanguages) + { + if (!IsLanguageInstalled(language)) + continue; + languages.Add(language); + + var languageFiles = new LanguageFiles(language); + + if (firstFallback is not null) + AddMegFile(FileSystem.Path.Combine(firstFallback, languageFiles.Sfx2dMegFilePath)); + + AddMegFile(languageFiles.Sfx2dMegFilePath); + } + + if (languages.Count == 0) + Logger.LogWarning("Unable to initialize any language."); + + return languages; + } + protected IList LoadMegArchivesFromXml(string lookupPath) { var megFilesXmlPath = FileSystem.Path.Combine(lookupPath, "Data\\MegaFiles.xml"); @@ -303,4 +373,24 @@ protected readonly struct ActionResult(bool shallReturn, T? result) [StructLayout(LayoutKind.Explicit)] private readonly struct EmptyStruct; + + private sealed class LanguageFiles + { + public LanguageType Language { get; } + + public string MasterTextDatFilePath { get; } + + public string Sfx2dMegFilePath { get; } + + public string SpeechMegFileName { get; } + + public LanguageFiles(LanguageType language) + { + Language = language; + var languageString = language.ToString().ToUpperInvariant(); + MasterTextDatFilePath = $"DATA\\TEXT\\MasterTextFile_{languageString}.DAT"; + Sfx2dMegFilePath = $"DATA\\AUDIO\\SFX\\SFX2D_{languageString}.MEG"; + SpeechMegFileName = $"{languageString}SPEECH.MEG"; + } + } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepository.cs index 689b1e9..d197536 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepository.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using PG.StarWarsGame.Engine.Language; namespace PG.StarWarsGame.Engine.Repositories; @@ -8,7 +9,11 @@ public interface IGameRepository : IRepository IRepository EffectsRepository { get; } + IRepository TextureRepository { get; } + bool FileExists(string filePath, string[] extensions, bool megFileOnly = false); IEnumerable FindFiles(string searchPattern, bool megFileOnly = false); + + bool IsLanguageInstalled(LanguageType languageType); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/MultiPassRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/MultiPassRepository.cs new file mode 100644 index 0000000..c4238bb --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/MultiPassRepository.cs @@ -0,0 +1,46 @@ +using System; +using System.IO; +using System.IO.Abstractions; +using Microsoft.Extensions.DependencyInjection; + +namespace PG.StarWarsGame.Engine.Repositories; + +public abstract class MultiPassRepository(IGameRepository baseRepository, IServiceProvider serviceProvider) : IRepository +{ + protected readonly IFileSystem FileSystem = serviceProvider.GetRequiredService(); + + public Stream OpenFile(string filePath, bool megFileOnly = false) + { + var fileStream = TryOpenFile(filePath, megFileOnly); + if (fileStream is null) + throw new FileNotFoundException($"Unable to find game file: {filePath}"); + return fileStream; + } + + public bool FileExists(string filePath, bool megFileOnly = false) + { + return MultiPassAction(filePath, actualPath => baseRepository.FileExists(actualPath, megFileOnly)); + } + + public Stream? TryOpenFile(string filePath, bool megFileOnly = false) + { + return MultiPassAction(filePath, path => + { + var stream = baseRepository.TryOpenFile(path, megFileOnly); + if (stream is null) + return (false, null); + return (true, stream); + }); + } + + protected abstract T? MultiPassAction(string inputPath, Func fileAction); + + protected bool MultiPassAction(string inputPath, Predicate fileAction) + { + return MultiPassAction(inputPath, path => + { + var result = fileAction(path); + return (result, result); + }); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/TextureRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/TextureRepository.cs new file mode 100644 index 0000000..e5253e0 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/TextureRepository.cs @@ -0,0 +1,64 @@ +using System; + +namespace PG.StarWarsGame.Engine.Repositories; + +public class TextureRepository(IGameRepository baseRepository, IServiceProvider serviceProvider) : MultiPassRepository(baseRepository, serviceProvider) +{ + protected override T? MultiPassAction(string inputPath, Func fileAction) where T : default + { + if (FindTexture(inputPath, fileAction, out var result)) + return result; + + var ddsFilePath = ChangeExtensionTo(inputPath, ".dds"); + + if (FindTexture(ddsFilePath, fileAction, out result)) + return result; + + return default; + } + + private bool FindTexture(string inputPath, Func fileAction, out T? result) + { + result = default; + + var actionResult = fileAction(inputPath); + if (actionResult.success) + { + result = actionResult.result; + return true; + } + + var newInput = inputPath; + + // Only PG knows why they only search for backslash and not also forward slash, + // when in fact in other methods, they handle both. + var separatorIndex = inputPath.LastIndexOf('\\'); + if (separatorIndex != -1 && separatorIndex + 1 < inputPath.Length) + newInput = inputPath.Substring(separatorIndex + 1); + + var pathWithFolders = FileSystem.Path.Combine("DATA\\ART\\TEXTURES", newInput); + actionResult = fileAction(pathWithFolders); + + if (actionResult.success) + { + result = actionResult.result; + return true; + } + + + return false; + } + + + private static string ChangeExtensionTo(string input, string extension) + { + // We cannot use Path.ChangeExtension as the PG implementation supports some strange things + // like that a string "c:\\file.tga\\" ending with a directory separator. The PG result will be + // "c:\\file.dds" while Path.ChangeExtension would return "c:\\file.tga\\.dds" + + // Also, while there are many cases, where method breaks (such as "c:/test.abc/path.dds"), + // it's the way how the engine works... + var firstPart = input.Split('.')[0]; + return firstPart + extension; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/ParticleReaderV1.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/ParticleReaderV1.cs index a2436c2..1f487f7 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/ParticleReaderV1.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/ParticleReaderV1.cs @@ -1,6 +1,11 @@ -using System.IO; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using PG.Commons.Binary; using PG.StarWarsGame.Files.ALO.Data; using PG.StarWarsGame.Files.ALO.Services; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; namespace PG.StarWarsGame.Files.ALO.Binary.Reader; @@ -8,6 +13,116 @@ internal class ParticleReaderV1(AloLoadOptions loadOptions, Stream stream) : Alo { public override AlamoParticle Read() { - return new AlamoParticle(); + var textures = new HashSet(StringComparer.OrdinalIgnoreCase); + + string? name = null; + + var rootChunk = ChunkReader.ReadChunk(); + + if (rootChunk.Type != (int)ChunkType.Particle) + throw new NotSupportedException("This reader only support V1 particles."); + + var actualSize = 0; + + do + { + var chunk = ChunkReader.ReadChunk(ref actualSize); + + switch (chunk.Type) + { + case (int)ChunkType.Name: + ReadName(chunk.Size, out name); + break; + case (int)ChunkType.Emitters: + ReadEmitters(chunk.Size, textures); + break; + default: + ChunkReader.Skip(chunk.Size); + break; + } + + actualSize += chunk.Size; + + } while (actualSize < rootChunk.Size); + + + + if (actualSize != rootChunk.Size) + throw new BinaryCorruptedException(); + + if (string.IsNullOrEmpty(name)) + throw new BinaryCorruptedException("The particle does not contain a name."); + + return new AlamoParticle + { + Name = name!, + Textures = textures, + }; + } + + private void ReadEmitters(int size, HashSet textures) + { + var actualSize = 0; + + do + { + var chunk = ChunkReader.ReadChunk(ref actualSize); + + if (chunk.Type != (int)ChunkType.Emitter) + throw new BinaryCorruptedException("Unable to read particle"); + + ReadEmitter(chunk.Size, textures); + + actualSize += chunk.Size; + + + } while (actualSize < size); + + if (size != actualSize) + throw new BinaryCorruptedException("Unable to read particle."); + } + + private void ReadEmitter(int chunkSize, HashSet textures) + { + var actualSize = 0; + + do + { + var chunk = ChunkReader.ReadChunk(ref actualSize); + + if (chunk.Type == (int)ChunkType.Properties) + { + var shader = ChunkReader.ReadDword(); + ChunkReader.Skip(chunk.Size - sizeof(uint)); + } + else if (chunk.Type == (int)ChunkType.ColorTextureName) + { + var texture = ChunkReader.ReadString(chunk.Size, Encoding.ASCII, true); + textures.Add(texture); + } + else if (chunk.Type == (int)ChunkType.BumpTextureName) + { + var bump = ChunkReader.ReadString(chunk.Size, Encoding.ASCII, true); + textures.Add(bump); + } + else + { + ChunkReader.Skip(chunk.Size); + } + + actualSize += chunk.Size; + + } while (actualSize < chunkSize); + + if (actualSize != chunkSize) + throw new BinaryCorruptedException("Unable to read particle"); + } + + private void ReadName(int size, out string name) + { + var actualSize = 0; + name = ChunkReader.ReadString(size, Encoding.ASCII, true, ref actualSize); + if (size != actualSize) + throw new BinaryCorruptedException("Unable to read alo particle."); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/ParticleReaderV2.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/ParticleReaderV2.cs index d311417..fc79903 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/ParticleReaderV2.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/ParticleReaderV2.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using PG.StarWarsGame.Files.ALO.Data; using PG.StarWarsGame.Files.ALO.Services; @@ -8,6 +9,6 @@ internal class ParticleReaderV2(AloLoadOptions loadOptions, Stream stream) : Alo { public override AlamoParticle Read() { - return new AlamoParticle(); + throw new NotImplementedException(); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Data/AlamoParticle.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Data/AlamoParticle.cs index 6fadab9..b2d94cb 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Data/AlamoParticle.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Data/AlamoParticle.cs @@ -1,7 +1,13 @@ -namespace PG.StarWarsGame.Files.ALO.Data; +using System.Collections.Generic; + +namespace PG.StarWarsGame.Files.ALO.Data; public class AlamoParticle : IAloDataContent { + public required string Name { get; init; } + + public ISet Textures { get; init; } + public void Dispose() { } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkType.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkType.cs index 145a0f7..9b34a66 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkType.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkType.cs @@ -3,6 +3,12 @@ public enum ChunkType { Unknown, + Name = 0x0, + Id = 0x1, + Persistance = 0x2, + Properties = 0x2, + ColorTextureName = 0x3, + BumpTextureName = 0x45, Skeleton = 0x200, BoneCount = 0x201, Bone = 0x202, @@ -10,14 +16,16 @@ public enum ChunkType Mesh = 0x400, MeshName = 0x401, MeshInformation = 0x402, - Light = 0x1300, Connections = 0x600, ConnectionCounts = 0x601, ObjectConnection = 0x602, ProxyConnection = 0x603, Dazzle = 0x604, + Emitter = 0x700, + Emitters = 0x800, Particle = 0x900, Animation = 0x1000, + Light = 0x1300, ParticleUaW = 0x1500, SubMeshData = 0x00010000, SubMeshMaterialInformation = 0x00010100, diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkReader.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkReader.cs index 4097792..60105e5 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkReader.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkReader.cs @@ -61,6 +61,11 @@ public uint ReadDword(ref int readSize) return value; } + public uint ReadDword() + { + return _binaryReader.ReadUInt32(); + } + public void Skip(int bytesToSkip, ref int readBytes) { _binaryReader.BaseStream.Seek(bytesToSkip, SeekOrigin.Current); @@ -79,13 +84,27 @@ public string ReadString(int size, Encoding encoding, bool zeroTerminated, ref i return value; } + public string ReadString(int size, Encoding encoding, bool zeroTerminated) + { + var value = _binaryReader.ReadString(size, encoding, zeroTerminated); + return value; + } + public ChunkMetadata? TryReadChunk() + { + var _ = 0; + return TryReadChunk(ref _); + } + + public ChunkMetadata? TryReadChunk(ref int size) { if (_binaryReader.BaseStream.Position == _binaryReader.BaseStream.Length) return null; try { - return ReadChunk(); + var chunk = ReadChunk(); + size += 8; + return chunk; } catch (EndOfStreamException e) { From 738b2a079ab56713dbe5f8fbc26400df4223c5f4 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sun, 21 Jul 2024 15:19:46 +0200 Subject: [PATCH 22/25] reorganize app --- src/ModVerify.CliApp/GameFinderResult.cs | 2 +- src/ModVerify.CliApp/GameFinderService.cs | 152 ++++++++++++++ src/ModVerify.CliApp/ModFinderService.cs | 98 --------- src/ModVerify.CliApp/ModOrGameSelector.cs | 141 +++++++++++++ src/ModVerify.CliApp/ModSelectionResult.cs | 10 + src/ModVerify.CliApp/ModVerify.CliApp.csproj | 10 + src/ModVerify.CliApp/ModVerifyApp.cs | 36 ++++ src/ModVerify.CliApp/ModVerifyOptions.cs | 30 +++ src/ModVerify.CliApp/ModVerifyPipeline.cs | 24 +++ src/ModVerify.CliApp/Program.cs | 198 +++++++----------- .../Properties/launchSettings.json | 11 + src/ModVerify.CliApp/VerifyGameSetupData.cs | 13 ++ src/ModVerify/VerifyGamePipeline.cs | 23 +- .../Database/IGameDatabaseService.cs | 3 +- .../GameDatabaseCreationPipeline.cs | 1 - 15 files changed, 524 insertions(+), 228 deletions(-) create mode 100644 src/ModVerify.CliApp/GameFinderService.cs delete mode 100644 src/ModVerify.CliApp/ModFinderService.cs create mode 100644 src/ModVerify.CliApp/ModOrGameSelector.cs create mode 100644 src/ModVerify.CliApp/ModSelectionResult.cs create mode 100644 src/ModVerify.CliApp/ModVerifyApp.cs create mode 100644 src/ModVerify.CliApp/ModVerifyOptions.cs create mode 100644 src/ModVerify.CliApp/ModVerifyPipeline.cs create mode 100644 src/ModVerify.CliApp/Properties/launchSettings.json create mode 100644 src/ModVerify.CliApp/VerifyGameSetupData.cs diff --git a/src/ModVerify.CliApp/GameFinderResult.cs b/src/ModVerify.CliApp/GameFinderResult.cs index a98ccd0..5d6cc94 100644 --- a/src/ModVerify.CliApp/GameFinderResult.cs +++ b/src/ModVerify.CliApp/GameFinderResult.cs @@ -2,4 +2,4 @@ namespace ModVerify.CliApp; -public readonly record struct GameFinderResult(IGame Game, IGame FallbackGame); \ No newline at end of file +internal record GameFinderResult(IGame Game, IGame? FallbackGame); \ No newline at end of file diff --git a/src/ModVerify.CliApp/GameFinderService.cs b/src/ModVerify.CliApp/GameFinderService.cs new file mode 100644 index 0000000..4b9f23a --- /dev/null +++ b/src/ModVerify.CliApp/GameFinderService.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Infrastructure.Clients.Steam; +using PG.StarWarsGame.Infrastructure.Games; +using PG.StarWarsGame.Infrastructure.Mods; +using PG.StarWarsGame.Infrastructure.Services; +using PG.StarWarsGame.Infrastructure.Services.Dependencies; +using PG.StarWarsGame.Infrastructure.Services.Detection; + +namespace ModVerify.CliApp; + +internal class GameFinderService +{ + private readonly IServiceProvider _serviceProvider; + private readonly IFileSystem _fileSystem; + private readonly ILogger? _logger; + private readonly IGameFactory _gameFactory; + private readonly IModFactory _modFactory; + + public GameFinderService(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + _fileSystem = _serviceProvider.GetRequiredService(); + _gameFactory = _serviceProvider.GetRequiredService(); + _modFactory = _serviceProvider.GetRequiredService(); + _logger = _serviceProvider.GetService()?.CreateLogger(GetType()); + } + + public GameFinderResult FindGames() + { + var detectors = new List + { + new DirectoryGameDetector(_fileSystem.DirectoryInfo.New(Environment.CurrentDirectory), _serviceProvider), + new SteamPetroglyphStarWarsGameDetector(_serviceProvider), + }; + + return FindGames(detectors); + } + + public GameFinderResult FindGamesFromPath(string path) + { + // There are three common situations: + // 1. path points to the actual game directory + // 2. path points to a local mod in game/Mods/ModDir + // 3. path points to a workshop mod + var givenDirectory = _fileSystem.DirectoryInfo.New(path); + var possibleGameDir = givenDirectory.Parent?.Parent; + var possibleSteamAppsFolder = givenDirectory.Parent?.Parent?.Parent?.Parent?.Parent; + + var detectors = new List + { + new DirectoryGameDetector(givenDirectory, _serviceProvider) + }; + + if (possibleGameDir is not null) + detectors.Add(new DirectoryGameDetector(possibleGameDir, _serviceProvider)); + + if (possibleSteamAppsFolder is not null && possibleSteamAppsFolder.Name == "steamapps" && uint.TryParse(givenDirectory.Name, out _)) + detectors.Add(new SteamPetroglyphStarWarsGameDetector(_serviceProvider)); + + return FindGames(detectors); + } + + private bool TryDetectGame(GameType gameType, IList detectors, out GameDetectionResult result) + { + var gd = new CompositeGameDetector(detectors, _serviceProvider, true); + result = gd.Detect(new GameDetectorOptions(gameType)); + + if (result.Error is not null) + { + _logger?.LogTrace($"Unable to find game installation: {result.Error.Message}", result.Error); + return false; + } + if (result.GameLocation is null) + return false; + + return true; + } + + + private void SetupMods(IGame game) + { + var modFinder = _serviceProvider.GetRequiredService(); + var modRefs = modFinder.FindMods(game); + + var mods = new List(); + + foreach (var modReference in modRefs) + { + var mod = _modFactory.FromReference(game, modReference); + mods.AddRange(mod); + } + + foreach (var mod in mods) + game.AddMod(mod); + + // Mods need to be added to the game first, before resolving their dependencies. + foreach (var mod in mods) + { + var resolver = _serviceProvider.GetRequiredService(); + mod.ResolveDependencies(resolver, + new DependencyResolverOptions { CheckForCycle = true, ResolveCompleteChain = true }); + } + } + + private GameFinderResult FindGames(IList detectors) + { + // FoC needs to be tried first + if (!TryDetectGame(GameType.Foc, detectors, out var result)) + { + _logger?.LogTrace("Unable to find FoC installation. Trying again with EaW..."); + if (!TryDetectGame(GameType.EaW, detectors, out result)) + throw new GameException("Unable to find game installation: Wrong install path?"); + } + + if (result.GameLocation is null) + throw new GameException("Unable to find game installation: Wrong install path?"); + + _logger?.LogTrace($"Found game installation: {result.GameIdentity} at {result.GameLocation.FullName}"); + + var game = _gameFactory.CreateGame(result); + + SetupMods(game); + + + IGame? fallbackGame = null; + // If the game is Foc we want to set up Eaw as well as the fallbackGame + if (game.Type == GameType.Foc) + { + var fallbackDetectors = new List(); + + if (game.Platform == GamePlatform.SteamGold) + fallbackDetectors.Add(new SteamPetroglyphStarWarsGameDetector(_serviceProvider)); + else + throw new NotImplementedException("Searching fallback game for non-Steam games is currently is not yet implemented."); + + if (!TryDetectGame(GameType.EaW, fallbackDetectors, out var fallbackResult) || fallbackResult.GameLocation is null) + throw new GameException("Unable to find fallback game installation: Wrong install path?"); + + _logger?.LogTrace($"Found fallback game installation: {fallbackResult.GameIdentity} at {fallbackResult.GameLocation.FullName}"); + + fallbackGame = _gameFactory.CreateGame(fallbackResult); + + SetupMods(fallbackGame); + } + + return new GameFinderResult(game, fallbackGame); + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModFinderService.cs b/src/ModVerify.CliApp/ModFinderService.cs deleted file mode 100644 index 71c96f6..0000000 --- a/src/ModVerify.CliApp/ModFinderService.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO.Abstractions; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using PG.StarWarsGame.Infrastructure.Clients.Steam; -using PG.StarWarsGame.Infrastructure.Games; -using PG.StarWarsGame.Infrastructure.Mods; -using PG.StarWarsGame.Infrastructure.Services; -using PG.StarWarsGame.Infrastructure.Services.Dependencies; -using PG.StarWarsGame.Infrastructure.Services.Detection; - -namespace ModVerify.CliApp; - -internal class ModFinderService -{ - private readonly IServiceProvider _serviceProvider; - private readonly IFileSystem _fileSystem; - private readonly ILogger? _logger; - private readonly IGameFactory _gameFactory; - private readonly IModFactory _modFactory; - - public ModFinderService(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - _fileSystem = _serviceProvider.GetRequiredService(); - _gameFactory = _serviceProvider.GetRequiredService(); - _modFactory = _serviceProvider.GetRequiredService(); - _logger = _serviceProvider.GetService()?.CreateLogger(GetType()); - } - - public GameFinderResult FindAndAddModInDirectory(string path) - { - var currentDirectory = _fileSystem.DirectoryInfo.New(path); - - // Assuming the currentDir is inside a Mod's directory, we need to go up two level (Game/Mods/ModDir) - var potentialGameDirectory = currentDirectory.Parent?.Parent; - if (potentialGameDirectory is null) - throw new GameException("Unable to find game installation: Wrong install path?"); - - var gd = new CompositeGameDetector(new List - { - new DirectoryGameDetector(potentialGameDirectory, _serviceProvider), - new SteamPetroglyphStarWarsGameDetector(_serviceProvider) - }, _serviceProvider, true); - - var focDetectionResult = gd.Detect(new GameDetectorOptions(GameType.Foc)); - - if (focDetectionResult.Error is not null) - throw new GameException($"Unable to find game installation: {focDetectionResult.Error.Message}", focDetectionResult.Error); - - if (focDetectionResult.GameLocation is null) - throw new GameException("Unable to find game installation: Wrong install path?"); - - _logger?.LogDebug($"Found game {focDetectionResult.GameIdentity} at '{focDetectionResult.GameLocation.FullName}'"); - - var foc = _gameFactory.CreateGame(focDetectionResult); - - var modFinder = _serviceProvider.GetRequiredService(); - var modRefs = modFinder.FindMods(foc); - - var mods = new List(); - - foreach (var modReference in modRefs) - { - var mod = _modFactory.FromReference(foc, modReference); - mods.AddRange(mod); - } - - foreach (var mod in mods) - foc.AddMod(mod); - - // Mods need to be added to the game first, before resolving their dependencies. - foreach (var mod in mods) - { - var resolver = _serviceProvider.GetRequiredService(); - mod.ResolveDependencies(resolver, - new DependencyResolverOptions { CheckForCycle = true, ResolveCompleteChain = true }); - } - - - var eawDetectionResult = gd.Detect(new GameDetectorOptions(GameType.EaW)); - if (eawDetectionResult.GameLocation is null) - throw new GameException("Unable to find Empire at War installation."); - if (eawDetectionResult.Error is not null) - throw new GameException($"Unable to find game installation: {eawDetectionResult.Error.Message}", eawDetectionResult.Error); - _logger?.LogDebug($"Found game {eawDetectionResult.GameIdentity} at '{eawDetectionResult.GameLocation.FullName}'"); - - var eaw = _gameFactory.CreateGame(eawDetectionResult); - - return new GameFinderResult(foc, eaw); - } - - public GameFinderResult FindAndAddModInCurrentDirectory() - { - return FindAndAddModInDirectory(Environment.CurrentDirectory); - } -} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModOrGameSelector.cs b/src/ModVerify.CliApp/ModOrGameSelector.cs new file mode 100644 index 0000000..7659c38 --- /dev/null +++ b/src/ModVerify.CliApp/ModOrGameSelector.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using EawModinfo.Spec; +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Infrastructure; +using PG.StarWarsGame.Infrastructure.Games; +using PG.StarWarsGame.Infrastructure.Mods; + +namespace ModVerify.CliApp; + +internal class ModOrGameSelector(IServiceProvider serviceProvider) +{ + private readonly GameFinderService _gameFinderService = new(serviceProvider); + private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); + + public ModSelectionResult SelectModOrGame(string? searchPath) + { + if (string.IsNullOrEmpty(searchPath)) + return SelectFromConsoleInput(); + return SelectFromPath(searchPath!); + } + + private ModSelectionResult SelectFromPath(string searchPath) + { + var fullSearchPath = _fileSystem.Path.GetFullPath(searchPath); + var gameResult = _gameFinderService.FindGamesFromPath(fullSearchPath); + + IPhysicalPlayableObject? gameOrMod = null; + + if (gameResult.Game.Directory.FullName.Equals(fullSearchPath, StringComparison.OrdinalIgnoreCase)) + gameOrMod = gameResult.Game; + else + { + foreach (var mod in gameResult.Game.Mods) + { + if (mod is IPhysicalMod physicalMod) + { + if (physicalMod.Directory.FullName.Equals(fullSearchPath, StringComparison.OrdinalIgnoreCase)) + { + gameOrMod = physicalMod; + break; + } + } + } + } + + if (gameOrMod is null) + throw new GameException($"Unable to a game or mod matching the path '{fullSearchPath}'."); + + return new ModSelectionResult + { + FallbackGame = gameResult.FallbackGame, + ModOrGame = gameOrMod + }; + } + + private ModSelectionResult SelectFromConsoleInput() + { + var gameResult = _gameFinderService.FindGames(); + + var list = new List(); + + var game = gameResult.Game; + list.Add(gameResult.Game); + + Console.WriteLine($"0: {game.Name}"); + + var counter = 1; + + var workshopMods = new HashSet(new ModEqualityComparer(false, false)); + + foreach (var mod in game.Mods) + { + var isSteam = mod.Type == ModType.Workshops; + if (isSteam) + { + workshopMods.Add(mod); + continue; + } + Console.WriteLine($"{counter++}:\t{mod.Name}"); + list.Add(mod); + } + + if (gameResult.FallbackGame is not null) + { + var fallbackGame = gameResult.FallbackGame; + list.Add(fallbackGame); + Console.WriteLine($"{counter++}: {fallbackGame.Name}"); + + foreach (var mod in fallbackGame.Mods) + { + var isSteam = mod.Type == ModType.Workshops; + if (isSteam) + { + workshopMods.Add(mod); + continue; + } + Console.WriteLine($"{counter++}:\t{mod.Name}"); + list.Add(mod); + } + } + + Console.WriteLine("Workshop Items:"); + foreach (var mod in workshopMods) + { + Console.WriteLine($"{counter++}:\t{mod.Name}"); + list.Add(mod); + } + + + IPlayableObject? selectedObject = null; + + do + { + Console.Write("Select a game or mod to verify: "); + var numberString = Console.ReadLine(); + + if (!int.TryParse(numberString, out var number)) + continue; + if (number < list.Count) + selectedObject = list[number]; + } while (selectedObject is null); + + if (selectedObject is not IPhysicalPlayableObject physicalPlayableObject) + throw new InvalidOperationException(); + + var coercedFallbackGame = gameResult.FallbackGame; + if (selectedObject.Equals(gameResult.FallbackGame)) + coercedFallbackGame = null; + else if (selectedObject.Game.Equals(gameResult.FallbackGame)) + coercedFallbackGame = null; + + + return new ModSelectionResult + { + ModOrGame = physicalPlayableObject, + FallbackGame = coercedFallbackGame + }; + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModSelectionResult.cs b/src/ModVerify.CliApp/ModSelectionResult.cs new file mode 100644 index 0000000..dee1c45 --- /dev/null +++ b/src/ModVerify.CliApp/ModSelectionResult.cs @@ -0,0 +1,10 @@ +using PG.StarWarsGame.Infrastructure; +using PG.StarWarsGame.Infrastructure.Games; + +namespace ModVerify.CliApp; + +internal record ModSelectionResult +{ + public IPhysicalPlayableObject ModOrGame { get; init; } + public IGame? FallbackGame { get; init; } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModVerify.CliApp.csproj b/src/ModVerify.CliApp/ModVerify.CliApp.csproj index 4100a37..5d511d2 100644 --- a/src/ModVerify.CliApp/ModVerify.CliApp.csproj +++ b/src/ModVerify.CliApp/ModVerify.CliApp.csproj @@ -36,6 +36,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -43,6 +47,10 @@ + + + + @@ -51,4 +59,6 @@ + + diff --git a/src/ModVerify.CliApp/ModVerifyApp.cs b/src/ModVerify.CliApp/ModVerifyApp.cs new file mode 100644 index 0000000..be1b8dd --- /dev/null +++ b/src/ModVerify.CliApp/ModVerifyApp.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Threading.Tasks; +using AET.ModVerify; +using AET.ModVerify.Reporting; +using AET.ModVerify.Settings; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace ModVerify.CliApp; + +internal class ModVerifyApp(GameVerifySettings settings, IServiceProvider services) +{ + private readonly ILogger? _logger = services.GetService()?.CreateLogger(typeof(ModVerifyApp)); + private readonly IFileSystem _fileSystem = services.GetRequiredService(); + + private IReadOnlyCollection Errors { get; set; } = Array.Empty(); + + public async Task RunVerification(VerifyGameSetupData gameSetupData) + { + var verifyPipeline = BuildPipeline(gameSetupData); + _logger?.LogInformation($"Verifying {gameSetupData.VerifyObject.Name}..."); + await verifyPipeline.RunAsync(); + _logger?.LogInformation("Finished Verifying"); + } + + private VerifyGamePipeline BuildPipeline(VerifyGameSetupData setupData) + { + return new ModVerifyPipeline(setupData.EngineType, setupData.GameLocations, settings, services); + } + + public async Task WriteBaseline(string baselineFile) + { + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModVerifyOptions.cs b/src/ModVerify.CliApp/ModVerifyOptions.cs new file mode 100644 index 0000000..0622bf6 --- /dev/null +++ b/src/ModVerify.CliApp/ModVerifyOptions.cs @@ -0,0 +1,30 @@ +using CommandLine; + +namespace ModVerify.CliApp; + +internal class ModVerifyOptions +{ + [Option('p', "path", Required = false, + HelpText = "The path to a mod directory to verify. If not path is specified, " + + "the app search for mods and asks the user to select a game or mod.")] + public string? Path { get; set; } + + [Option('v', "verbose", Required = false, HelpText = "Set output to verbose messages.")] + public bool Verbose { get; set; } + + [Option("baseline", Required = false, HelpText = "Path to a JSON baseline file.")] + public string? Baseline { get; set; } + + [Option("suppressions", Required = false, HelpText = "Path to a JSON suppression file.")] + public string? Suppressions { get; set; } + + [Option("createBaseLine", Required = false, + HelpText = "When a path is specified, the tools creates a new baseline file. " + + "An existing file will be overwritten. " + + "Previous baselines are merged into the new baseline.")] + public string? NewBaselineFile { get; set; } + + [Option("fallbackPath", Required = false, + HelpText = "Additional fallback path, which may contain assets that shall be included when doing the verification.")] + public string? AdditionalFallbackPath { get; set; } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModVerifyPipeline.cs b/src/ModVerify.CliApp/ModVerifyPipeline.cs new file mode 100644 index 0000000..03fbfe3 --- /dev/null +++ b/src/ModVerify.CliApp/ModVerifyPipeline.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using AET.ModVerify; +using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers; +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Engine.Database; + +namespace ModVerify.CliApp; + +internal class ModVerifyPipeline( + GameEngineType targetType, + GameLocations gameLocations, + GameVerifySettings settings, + IServiceProvider serviceProvider) + : VerifyGamePipeline(targetType, gameLocations, settings, serviceProvider) +{ + protected override IEnumerable CreateVerificationSteps(IGameDatabase database) + { + var verifyProvider = ServiceProvider.GetRequiredService(); + return verifyProvider.GetAllDefaultVerifiers(database, Settings); + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/Program.cs b/src/ModVerify.CliApp/Program.cs index 2af2dc2..ad81084 100644 --- a/src/ModVerify.CliApp/Program.cs +++ b/src/ModVerify.CliApp/Program.cs @@ -9,27 +9,24 @@ using AET.ModVerify.Reporting; using AET.ModVerify.Reporting.Reporters; using AET.ModVerify.Settings; -using AET.ModVerify.Verifiers; using AET.SteamAbstraction; using AnakinRaW.CommonUtilities.FileSystem; using AnakinRaW.CommonUtilities.Hashing; using AnakinRaW.CommonUtilities.Registry; using AnakinRaW.CommonUtilities.Registry.Windows; using CommandLine; -using EawModinfo.Spec; +using CommandLine.Text; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Console; using PG.Commons.Extensibility; using PG.StarWarsGame.Engine; -using PG.StarWarsGame.Engine.Database; using PG.StarWarsGame.Files.ALO; using PG.StarWarsGame.Files.DAT.Services.Builder; using PG.StarWarsGame.Files.MEG.Data.Archives; using PG.StarWarsGame.Files.XML; using PG.StarWarsGame.Infrastructure; using PG.StarWarsGame.Infrastructure.Clients; -using PG.StarWarsGame.Infrastructure.Games; using PG.StarWarsGame.Infrastructure.Mods; using PG.StarWarsGame.Infrastructure.Services.Dependencies; using Serilog; @@ -41,111 +38,92 @@ namespace ModVerify.CliApp; internal class Program { private const string EngineParserNamespace = "PG.StarWarsGame.Engine.Xml.Parsers"; - private const string ParserNamespace = "PG.StarWarsGame.Engine.Xml.Parsers"; + private const string ParserNamespace = "PG.StarWarsGame.Files.XML.Parsers"; - private static IServiceProvider _services = null!; - private static CliOptions _options; - - private static async Task Main(string[] args) + private static async Task Main(string[] args) { - _options = Parser.Default.ParseArguments(args).WithParsed(o => { }).Value; - _services = CreateAppServices(); - - GameFinderResult gameFinderResult; - IPlayableObject? selectedObject = null; + var result = 0; + + var parseResult = Parser.Default.ParseArguments(args); - if (!string.IsNullOrEmpty(_options.Path)) + ModVerifyOptions? verifyOptions = null!; + await parseResult.WithParsedAsync(o => { - var fs = _services.GetService(); - if (!fs!.Directory.Exists(_options.Path)) - throw new DirectoryNotFoundException($"No directory found at {_options.Path}"); - - gameFinderResult = new ModFinderService(_services).FindAndAddModInDirectory(_options.Path); - var selectedPath = fs.Path.GetFullPath(_options.Path).ToUpper(); - selectedObject = - (from mod1 in gameFinderResult.Game.Mods.OfType() - let modPath = fs.Path.GetFullPath(mod1.Directory.FullName) - where selectedPath.Equals(modPath) - select mod1).FirstOrDefault(); - if (selectedObject == null) throw new Exception($"The selected directory {_options.Path} is not a mod."); - } - else + verifyOptions = o; + return Task.CompletedTask; + }).ConfigureAwait(false); + + await parseResult.WithNotParsedAsync(e => { - gameFinderResult = new ModFinderService(_services).FindAndAddModInCurrentDirectory(); - var game = gameFinderResult.Game; - Console.WriteLine($"0: {game.Name}"); + Console.WriteLine(HelpText.AutoBuild(parseResult).ToString()); + result = 0xA0; + return Task.CompletedTask; + }).ConfigureAwait(false); - var list = new List { game }; + if (verifyOptions is null) + { + if (result != 0) + return result; + throw new InvalidOperationException("Mod verify was executed with the wrong arguments."); + } - var counter = 1; - foreach (var mod in game.Mods) - { - var isSteam = mod.Type == ModType.Workshops; - var line = $"{counter++}: {mod.Name}"; - if (isSteam) - line += "*"; - Console.WriteLine(line); - list.Add(mod); - } - - do - { - Console.Write("Select a game or mod to verify: "); - var numberString = Console.ReadLine(); + var services = CreateAppServices(verifyOptions.Verbose); + var settings = BuildSettings(verifyOptions, services); + var gameSetupData = CreateGameSetupData(verifyOptions, services); - if (!int.TryParse(numberString, out var number)) continue; - if (number < list.Count) - selectedObject = list[number]; - } while (selectedObject is null); - } + var verifier = new ModVerifyApp(settings, services); + await verifier.RunVerification(gameSetupData).ConfigureAwait(false); - Console.WriteLine($"Verifying {selectedObject.Name}..."); - var verifyPipeline = BuildPipeline(selectedObject, gameFinderResult.FallbackGame); + if (verifyOptions.NewBaselineFile is not null) + await verifier.WriteBaseline(verifyOptions.NewBaselineFile).ConfigureAwait(false); - try - { - await verifyPipeline.RunAsync(); - } - catch (GameVerificationException e) - { - Console.WriteLine(e.Message); - } + return 0; } - private static VerifyGamePipeline BuildPipeline(IPlayableObject playableObject, IGame fallbackGame) + private static VerifyGameSetupData CreateGameSetupData(ModVerifyOptions options, IServiceProvider services) { + var selectionResult = new ModOrGameSelector(services).SelectModOrGame(options.Path); + IList mods = Array.Empty(); - if (playableObject is IMod mod) + if (selectionResult.ModOrGame is IMod mod) { - var traverser = _services.GetRequiredService(); + var traverser = services.GetRequiredService(); mods = traverser.Traverse(mod) .Select(x => x.Mod) .OfType().Select(x => x.Directory.FullName) .ToList(); } - var gameLocations = new GameLocations( - mods, - playableObject.Game.Directory.FullName, - fallbackGame.Directory.FullName); + var fallbackPaths = new List(); + if (selectionResult.FallbackGame is not null) + fallbackPaths.Add(selectionResult.FallbackGame.Directory.FullName); - var settings = BuildSettings(); + if (!string.IsNullOrEmpty(options.AdditionalFallbackPath)) + fallbackPaths.Add(options.AdditionalFallbackPath); + var gameLocations = new GameLocations( + mods, + selectionResult.ModOrGame.Game.Directory.FullName, + fallbackPaths); - return new ModVerifyPipeline(GameEngineType.Foc, gameLocations, settings, _services); + return new VerifyGameSetupData + { + EngineType = GameEngineType.Foc, + GameLocations = gameLocations, + VerifyObject = selectionResult.ModOrGame, + }; } - - private static GameVerifySettings BuildSettings() + private static GameVerifySettings BuildSettings(ModVerifyOptions options, IServiceProvider services) { var settings = GameVerifySettings.Default; var reportSettings = settings.GlobalReportSettings; - if (_options.Baseline is not null) + if (options.Baseline is not null) { - using var fs = _services.GetRequiredService().FileStream - .New(_options.Baseline, FileMode.Open, FileAccess.Read); + using var fs = services.GetRequiredService().FileStream + .New(options.Baseline, FileMode.Open, FileAccess.Read); var baseline = VerificationBaseline.FromJson(fs); reportSettings = reportSettings with @@ -154,10 +132,10 @@ private static GameVerifySettings BuildSettings() }; } - if (_options.Suppressions is not null) + if (options.Suppressions is not null) { - using var fs = _services.GetRequiredService().FileStream - .New(_options.Suppressions, FileMode.Open, FileAccess.Read); + using var fs = services.GetRequiredService().FileStream + .New(options.Suppressions, FileMode.Open, FileAccess.Read); var baseline = SuppressionList.FromJson(fs); reportSettings = reportSettings with @@ -173,7 +151,7 @@ private static GameVerifySettings BuildSettings() } - private static IServiceProvider CreateAppServices() + private static IServiceProvider CreateAppServices(bool verbose) { var fileSystem = new FileSystem(); var serviceCollection = new ServiceCollection(); @@ -182,7 +160,7 @@ private static IServiceProvider CreateAppServices() serviceCollection.AddSingleton(sp => new HashingService(sp)); serviceCollection.AddSingleton(fileSystem); - serviceCollection.AddLogging(builder => ConfigureLogging(builder, fileSystem)); + serviceCollection.AddLogging(builder => ConfigureLogging(builder, fileSystem, verbose)); SteamAbstractionLayer.InitializeServices(serviceCollection); PetroglyphGameClients.InitializeServices(serviceCollection); @@ -203,7 +181,7 @@ private static IServiceProvider CreateAppServices() return serviceCollection.BuildServiceProvider(); } - private static void ConfigureLogging(ILoggingBuilder loggingBuilder, IFileSystem fileSystem) + private static void ConfigureLogging(ILoggingBuilder loggingBuilder, IFileSystem fileSystem, bool verbose) { loggingBuilder.ClearProviders(); @@ -213,16 +191,26 @@ private static void ConfigureLogging(ILoggingBuilder loggingBuilder, IFileSystem logLevel = LogLevel.Debug; loggingBuilder.AddDebug(); #else - if (_options.Verbose) + if (verbose) { logLevel = LogLevel.Debug; loggingBuilder.AddDebug(); } #endif - loggingBuilder.AddConsole(); loggingBuilder.SetMinimumLevel(logLevel); SetupXmlParseLogging(loggingBuilder, fileSystem); + + loggingBuilder.AddFilter((category, level) => + { + if (level < logLevel) + return false; + if (string.IsNullOrEmpty(category)) + return false; + if (category.StartsWith(EngineParserNamespace) || category.StartsWith(ParserNamespace)) + return false; + return true; + }).AddConsole(); } private static void SetupXmlParseLogging(ILoggingBuilder loggingBuilder, IFileSystem fileSystem) @@ -239,50 +227,10 @@ private static void SetupXmlParseLogging(ILoggingBuilder loggingBuilder, IFileSy .CreateLogger(); loggingBuilder.AddSerilog(logger); - - loggingBuilder.AddFilter((category, _) => - { - if (string.IsNullOrEmpty(category)) - return false; - if (category.StartsWith(EngineParserNamespace) || category.StartsWith(ParserNamespace)) - return false; - - return true; - }); } private static bool IsXmlParserLogging(LogEvent logEvent) { return Matching.FromSource(ParserNamespace)(logEvent) || Matching.FromSource(EngineParserNamespace)(logEvent); } - - - internal class CliOptions - { - [Option('v', "verbose", Required = false, HelpText = "Set output to verbose messages.")] - public bool Verbose { get; set; } - - [Option('p', "path", Required = false, HelpText = "The path to a mod directory to verify.")] - public string Path { get; set; } - - [Option("baseline", Required = false, HelpText = "Path to a JSON baseline file.")] - public string? Baseline { get; set; } - - [Option("suppressions", Required = false, HelpText = "Path to a JSON suppression file.")] - public string? Suppressions { get; set; } - } -} - -internal class ModVerifyPipeline( - GameEngineType targetType, - GameLocations gameLocations, - GameVerifySettings settings, - IServiceProvider serviceProvider) - : VerifyGamePipeline(targetType, gameLocations, settings, serviceProvider) -{ - protected override IEnumerable CreateVerificationSteps(IGameDatabase database) - { - var verifyProvider = ServiceProvider.GetRequiredService(); - return verifyProvider.GetAllDefaultVerifiers(database, Settings); - } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/Properties/launchSettings.json b/src/ModVerify.CliApp/Properties/launchSettings.json new file mode 100644 index 0000000..e0ea1a3 --- /dev/null +++ b/src/ModVerify.CliApp/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "No-Arguments": { + "commandName": "Project" + }, + "From-path": { + "commandName": "Project", + "commandLineArgs": "-p \"C:\\Program Files (x86)\\Steam\\steamapps\\common\\Star Wars Empire at War\\corruption\\Mods\\Republic_at_War\"" + } + } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/VerifyGameSetupData.cs b/src/ModVerify.CliApp/VerifyGameSetupData.cs new file mode 100644 index 0000000..621e0c2 --- /dev/null +++ b/src/ModVerify.CliApp/VerifyGameSetupData.cs @@ -0,0 +1,13 @@ +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Infrastructure; + +namespace ModVerify.CliApp; + +internal class VerifyGameSetupData +{ + public required IPhysicalPlayableObject VerifyObject { get; init; } + + public required GameEngineType EngineType { get; init; } + + public required GameLocations GameLocations { get; init; } +} \ No newline at end of file diff --git a/src/ModVerify/VerifyGamePipeline.cs b/src/ModVerify/VerifyGamePipeline.cs index 4ef01a6..2423f30 100644 --- a/src/ModVerify/VerifyGamePipeline.cs +++ b/src/ModVerify/VerifyGamePipeline.cs @@ -24,6 +24,8 @@ public abstract class VerifyGamePipeline : Pipeline protected GameVerifySettings Settings { get; } + public IReadOnlyCollection Errors { get; private set; } = Array.Empty(); + protected VerifyGamePipeline(GameEngineType targetType, GameLocations gameLocations, GameVerifySettings settings, IServiceProvider serviceProvider) : base(serviceProvider) { @@ -46,12 +48,21 @@ protected sealed override Task PrepareCoreAsync() protected sealed override async Task RunCoreAsync(CancellationToken token) { - Logger?.LogInformation("Verifying game..."); + Logger?.LogInformation("Verifying..."); try { var databaseService = ServiceProvider.GetRequiredService(); - var database = await databaseService.CreateDatabaseAsync(_targetType, _gameLocations, token); + IGameDatabase database; + try + { + //databaseService.XmlParseError += OnXmlParseError; + database = await databaseService.CreateDatabaseAsync(_targetType, _gameLocations, token); + } + finally + { + //databaseService.XmlParseError -= OnXmlParseError; + } foreach (var gameVerificationStep in CreateVerificationSteps(database)) { @@ -76,6 +87,9 @@ protected sealed override async Task RunCoreAsync(CancellationToken token) var reportBroker = new VerificationReportBroker(Settings.GlobalReportSettings, ServiceProvider); var errors = reportBroker.Report(_verificationSteps); + + Errors = errors; + if (Settings.AbortSettings.ThrowsGameVerificationException && errors.Any(x => x.Severity >= Settings.AbortSettings.MinimumAbortSeverity)) throw new GameVerificationException(errors); @@ -86,5 +100,10 @@ protected sealed override async Task RunCoreAsync(CancellationToken token) } } + private void OnXmlParseError(object sender, EventArgs e) + { + + } + protected abstract IEnumerable CreateVerificationSteps(IGameDatabase database); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabaseService.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabaseService.cs index 366a7f7..15c0c6f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabaseService.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabaseService.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System; +using System.Threading; using System.Threading.Tasks; namespace PG.StarWarsGame.Engine.Database; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs index 1d3cd26..6916ee6 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs @@ -30,7 +30,6 @@ protected override Task PrepareCoreAsync() foreach (var xmlParserStep in CreateXmlParserSteps()) _parseXmlRunner.AddStep(xmlParserStep); - return Task.FromResult(true); } From d2f27bb8d5679963fefa5656240a8eeb20dca210 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sun, 21 Jul 2024 15:23:33 +0200 Subject: [PATCH 23/25] update sub --- PetroglyphTools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PetroglyphTools b/PetroglyphTools index 2710513..3efe081 160000 --- a/PetroglyphTools +++ b/PetroglyphTools @@ -1 +1 @@ -Subproject commit 271051320757451afeec76cf21d236e85f26f016 +Subproject commit 3efe081031d1ab331c5af917be07454a943a2d4c From a677fb9ac8df0466d64eda37cd065f77a5efa87c Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Mon, 22 Jul 2024 09:48:31 +0200 Subject: [PATCH 24/25] start xml reporting --- .../GameDatabaseCreationPipeline.cs | 7 +- .../Xml/Parsers/Data/GameObjectParser.cs | 1 - .../Xml/Parsers/Data/SfxEventParser.cs | 9 ++- .../Xml/Parsers/File/SfxEventFileParser.cs | 40 ++++++++-- .../Xml/Parsers/XmlObjectParser.cs | 4 +- .../ParseErrorEventArgs.cs | 74 +++++++++++++++++++ .../Parsers/IPetroglyphXmlParser.cs | 9 ++- .../Parsers/PetroglyphXmlFileParser.cs | 3 + .../Parsers/PetroglyphXmlParser.cs | 7 ++ .../CommaSeparatedStringKeyValueListParser.cs | 1 + .../Primitives/PetroglyphXmlByteParser.cs | 12 ++- .../Primitives/PetroglyphXmlFloatParser.cs | 13 +++- .../Primitives/PetroglyphXmlIntegerParser.cs | 15 +++- .../PetroglyphXmlLooseStringListParser.cs | 15 +++- .../PetroglyphXmlMax100ByteParser.cs | 17 ++++- .../PetroglyphXmlUnsignedIntegerParser.cs | 14 +++- .../XmlLocationInfo.cs | 4 +- 17 files changed, 206 insertions(+), 39 deletions(-) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/ParseErrorEventArgs.cs diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs index 6916ee6..5da090e 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs @@ -19,14 +19,15 @@ internal class GameDatabaseCreationPipeline(GameRepository repository, IServiceP private ParseXmlDatabaseFromContainerStep _parseGameObjects = null!; private ParseXmlDatabaseFromContainerStep _parseSfxEvents = null!; - private ParallelRunner _parseXmlRunner = null!; + // We cannot use parallel processing here, in order to avoid races of the error event + private StepRunner _parseXmlRunner = null!; public GameDatabase GameDatabase { get; private set; } = null!; protected override Task PrepareCoreAsync() { - _parseXmlRunner = new ParallelRunner(4, ServiceProvider); + _parseXmlRunner = new StepRunner(ServiceProvider); foreach (var xmlParserStep in CreateXmlParserSteps()) _parseXmlRunner.AddStep(xmlParserStep); @@ -35,6 +36,8 @@ protected override Task PrepareCoreAsync() private IEnumerable CreateXmlParserSteps() { + // TODO: Use same load order as the engine! + yield return _parseGameConstants = new ParseSingletonXmlStep("GameConstants", "DATA\\XML\\GAMECONSTANTS.XML", repository, ServiceProvider); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs index 99f5917..f4aaf36 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs @@ -4,7 +4,6 @@ using PG.StarWarsGame.Engine.DataTypes; using PG.StarWarsGame.Files.XML; using PG.StarWarsGame.Files.XML.Parsers; -using PG.StarWarsGame.Files.XML.Parsers.Primitives; namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs index b767564..fd33158 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs @@ -1,6 +1,5 @@ using System; using System.Xml.Linq; -using Microsoft.Extensions.Logging; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.DataTypes; using PG.StarWarsGame.Engine.Xml.Tags; @@ -74,7 +73,7 @@ public override SfxEvent Parse(XElement element, out Crc32 nameCrc) return new SfxEvent(name, nameCrc, properties, XmlLocationInfo.FromElement(element)); } - protected override bool OnParsed(string tag, object value, ValueListDictionary properties, string? elementName) + protected override bool OnParsed(XElement element, string tag, object value, ValueListDictionary properties, string? outerElementName) { if (tag == SfxEventXmlTags.UsePreset) { @@ -83,7 +82,11 @@ protected override bool OnParsed(string tag, object value, ValueListDictionary parsedElements) { var parser = new SfxEventParser(parsedElements, ServiceProvider); - foreach (var xElement in element.Elements()) + + try { - var sfxEvent = parser.Parse(xElement, out var nameCrc); - if (nameCrc == default) - Logger?.LogWarning($"SFXEvent has no name at location '{XmlLocationInfo.FromElement(xElement)}'"); - parsedElements.Add(nameCrc, sfxEvent); + parser.ParseError += OnInnerParseError; + if (!element.HasElements) + { + OnParseError(ParseErrorEventArgs.FromEmptyRoot(XmlLocationInfo.FromElement(element).XmlFile, element)); + return; + } + + foreach (var xElement in element.Elements()) + { + var sfxEvent = parser.Parse(xElement, out var nameCrc); + if (nameCrc == default) + { + var location = XmlLocationInfo.FromElement(xElement); + OnParseError(new ParseErrorEventArgs(location.XmlFile, xElement, XmlParseErrorKind.MissingAttribute, + $"SFXEvent has no name at location '{location}'")); + } + parsedElements.Add(nameCrc, sfxEvent); + } } + finally + { + parser.ParseError -= OnInnerParseError; + } + } + + protected override void OnParseError(ParseErrorEventArgs e) + { + Logger?.LogWarning($"Error while parsing {e.File}: {e.Message}"); + base.OnParseError(e); + } + + private void OnInnerParseError(object sender, ParseErrorEventArgs e) + { + OnParseError(e); } public override SfxEvent Parse(XElement element) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs index f2eab56..b65b0a2 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs @@ -38,13 +38,13 @@ public abstract class XmlObjectParser(IReadOnlyValueListDictionary var value = parser.Parse(elm); - if (OnParsed(tagName, value, xmlProperties, name)) + if (OnParsed(elm, tagName, value, xmlProperties, name)) xmlProperties.Add(tagName, value); } return xmlProperties; } - protected virtual bool OnParsed(string tag, object value, ValueListDictionary properties, string? elementName) + protected virtual bool OnParsed(XElement element, string tag, object value, ValueListDictionary properties, string? outerElementName) { return true; } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ParseErrorEventArgs.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ParseErrorEventArgs.cs new file mode 100644 index 0000000..be59de3 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ParseErrorEventArgs.cs @@ -0,0 +1,74 @@ +using System; +using System.Xml.Linq; +using AnakinRaW.CommonUtilities; + +namespace PG.StarWarsGame.Files.XML; + +public class ParseErrorEventArgs : EventArgs +{ + public string File { get; } + + public XElement? Element { get; } + + public XmlParseErrorKind ErrorKind { get; } + + public string Message { get; } + + public ParseErrorEventArgs(string file, XElement? element, XmlParseErrorKind errorKind, string message) + { + ThrowHelper.ThrowIfNullOrEmpty(file); + File = file; + Element = element; + ErrorKind = errorKind; + Message = message; + } + + public static ParseErrorEventArgs FromMissingFile(string file) + { + ThrowHelper.ThrowIfNullOrEmpty(file); + return new ParseErrorEventArgs(file, null, XmlParseErrorKind.MissingFile, $"XML file '{file}' not found."); + } + + public static ParseErrorEventArgs FromEmptyRoot(string file, XElement element) + { + ThrowHelper.ThrowIfNullOrEmpty(file); + return new ParseErrorEventArgs(file, element, XmlParseErrorKind.EmptyRoot, $"XML file '{file}' has an empty root node."); + } +} + +public enum XmlParseErrorKind +{ + /// + /// The error not specified any further. + /// + Unknown, + /// + /// The XML file could not be found. + /// + MissingFile, + /// + /// The root node of an XML file is empty. + /// + EmptyRoot, + /// + /// A tag's value is syntactically correct, but the semantics of value are not valid. For example, + /// when the input is '-1' but an uint type is expected. + /// + InvalidValue, + /// + /// A tag's value is has an invalid syntax. + /// + MalformedValue, + /// + /// The value is too long + /// + TooLongData, + /// + /// The data is missing an XML attribute. Usually this is the Name attribute. + /// + MissingAttribute, + /// + /// The data points to a non-existing reference. + /// + MissingReference, +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlParser.cs index cd8a607..dffbd69 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlParser.cs @@ -1,13 +1,16 @@ -using System.Xml.Linq; +using System; +using System.Xml.Linq; namespace PG.StarWarsGame.Files.XML.Parsers; public interface IPetroglyphXmlParser { - public object Parse(XElement element); + event EventHandler ParseError; + + object Parse(XElement element); } public interface IPetroglyphXmlParser : IPetroglyphXmlParser { - public new T Parse(XElement element); + new T Parse(XElement element); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs index 871ff6f..3798aef 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs @@ -29,6 +29,9 @@ public void ParseFile(Stream xmlStream, IValueListDictionary parsedEnt private XElement? GetRootElement(Stream xmlStream) { var fileName = xmlStream.GetFilePath(); + if (string.IsNullOrEmpty(fileName)) + throw new InvalidOperationException("Unable to parse XML from unnamed stream. Either parse from a file or MEG stream."); + var xmlReader = XmlReader.Create(xmlStream, new XmlReaderSettings(), fileName); var options = LoadOptions.SetBaseUri; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlParser.cs index 3ef8976..a3a536c 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlParser.cs @@ -8,6 +8,8 @@ namespace PG.StarWarsGame.Files.XML.Parsers; public abstract class PetroglyphXmlParser : IPetroglyphXmlParser { + public event EventHandler? ParseError; + protected IServiceProvider ServiceProvider { get; } protected ILogger? Logger { get; } @@ -23,6 +25,11 @@ protected PetroglyphXmlParser(IServiceProvider serviceProvider) public abstract T Parse(XElement element); + protected virtual void OnParseError(ParseErrorEventArgs e) + { + ParseError?.Invoke(this, e); + } + object IPetroglyphXmlParser.Parse(XElement element) { return Parse(element); 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 5e15971..f5ba6b0 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/CommaSeparatedStringKeyValueListParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/CommaSeparatedStringKeyValueListParser.cs @@ -7,6 +7,7 @@ namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; // 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 : PetroglyphXmlPrimitiveElementParser> { internal CommaSeparatedStringKeyValueListParser(IServiceProvider serviceProvider) : base(serviceProvider) 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 0da16a3..a6f5d7b 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlByteParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlByteParser.cs @@ -8,7 +8,6 @@ public sealed class PetroglyphXmlByteParser : PetroglyphXmlPrimitiveElementParse { internal PetroglyphXmlByteParser(IServiceProvider serviceProvider) : base(serviceProvider) { - } public override byte Parse(XElement element) @@ -18,10 +17,17 @@ public override byte Parse(XElement element) var asByte = (byte)intValue; if (intValue != asByte) { - var location = XmlLocationInfo.FromElement(element); - Logger?.LogWarning($"Expected a byte value (0 - 255) but got value '{intValue}' at {location}"); + var location = XmlLocationInfo.FromElement(element); + OnParseError(new ParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.InvalidValue, + $"Expected a byte value (0 - 255) but got value '{intValue}' at {location}")); } return asByte; } + + protected override void OnParseError(ParseErrorEventArgs e) + { + Logger?.LogWarning(e.Message); + base.OnParseError(e); + } } \ 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 bbb2a8f..8e201d6 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlFloatParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlFloatParser.cs @@ -1,7 +1,7 @@ -using Microsoft.Extensions.Logging; -using System; +using System; using System.Globalization; using System.Xml.Linq; +using Microsoft.Extensions.Logging; namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; @@ -17,9 +17,16 @@ public override float Parse(XElement element) if (!double.TryParse(element.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var doubleValue)) { var location = XmlLocationInfo.FromElement(element); - Logger?.LogWarning($"Expected double but got value '{element.Value}' at {location}"); + OnParseError(new ParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.MalformedValue, + $"Expected double but got value '{element.Value}' at {location}")); return 0.0f; } return (float)doubleValue; } + + protected override void OnParseError(ParseErrorEventArgs e) + { + Logger?.LogWarning(e.Message); + base.OnParseError(e); + } } \ No newline at end of file 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 ab67620..383c2ce 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlIntegerParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlIntegerParser.cs @@ -1,6 +1,6 @@ -using System; +using Microsoft.Extensions.Logging; +using System; using System.Xml.Linq; -using Microsoft.Extensions.Logging; namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; @@ -18,11 +18,18 @@ public override int Parse(XElement element) if (!int.TryParse(element.Value, out var i)) { - var location = XmlLocationInfo.FromElement(element); - Logger?.LogWarning($"Expected integer but got '{element.Value}' at {location}"); + var location = XmlLocationInfo.FromElement(element); + OnParseError(new ParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.MalformedValue, + $"Expected integer but got '{element.Value}' at {location}")); return 0; } return i; } + + protected override void OnParseError(ParseErrorEventArgs e) + { + Logger?.LogWarning(e.Message); + base.OnParseError(e); + } } \ 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 be8cbf7..4cbdfed 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlLooseStringListParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlLooseStringListParser.cs @@ -1,7 +1,7 @@ -using System; +using Microsoft.Extensions.Logging; +using System; using System.Collections.Generic; using System.Xml.Linq; -using Microsoft.Extensions.Logging; namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; @@ -30,7 +30,10 @@ public override IList Parse(XElement element) if (trimmedValued.Length > 0x2000) { - Logger?.LogWarning($"Input value is too long '{trimmedValued.Length}' at {XmlLocationInfo.FromElement(element)}"); + var location = XmlLocationInfo.FromElement(element); + OnParseError(new ParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.TooLongData, + $"Input value is too long '{trimmedValued.Length}' at {XmlLocationInfo.FromElement(element)}")); + return Array.Empty(); } @@ -38,4 +41,10 @@ public override IList Parse(XElement element) return entries; } + + protected override void OnParseError(ParseErrorEventArgs e) + { + Logger?.LogWarning(e.Message); + base.OnParseError(e); + } } \ 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 index 7c767b3..61aa4ec 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlMax100ByteParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlMax100ByteParser.cs @@ -1,6 +1,6 @@ -using System; +using Microsoft.Extensions.Logging; +using System; using System.Xml.Linq; -using Microsoft.Extensions.Logging; namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; @@ -22,16 +22,25 @@ public override byte Parse(XElement element) if (intValue != asByte) { var location = XmlLocationInfo.FromElement(element); - Logger?.LogWarning($"Expected a byte value (0 - 255) but got value '{intValue}' at {location}"); + + OnParseError(new ParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.InvalidValue, + $"Expected a byte value (0 - 255) but got value '{intValue}' at {location}")); } // Add additional check, cause the PG implementation is broken, but we need to stay "bug-compatible". if (asByte > 100) { var location = XmlLocationInfo.FromElement(element); - Logger?.LogWarning($"Expected a byte value (0 - 100) but got value '{asByte}' at {location}"); + OnParseError(new ParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.InvalidValue, + $"Expected a byte value (0 - 100) but got value '{asByte}' at {location}")); } return asByte; } + + protected override void OnParseError(ParseErrorEventArgs e) + { + Logger?.LogWarning(e.Message); + base.OnParseError(e); + } } \ 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 7faf6a7..689471d 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlUnsignedIntegerParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlUnsignedIntegerParser.cs @@ -1,6 +1,6 @@ -using System; +using Microsoft.Extensions.Logging; +using System; using System.Xml.Linq; -using Microsoft.Extensions.Logging; namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; @@ -18,9 +18,17 @@ public override uint Parse(XElement element) if (intValue != asUint) { var location = XmlLocationInfo.FromElement(element); - Logger?.LogWarning($"Expected unsigned integer but got '{intValue}' at {location}"); + + OnParseError(new ParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.InvalidValue, + $"Expected unsigned integer but got '{intValue}' at {location}")); } return asUint; } + + protected override void OnParseError(ParseErrorEventArgs e) + { + Logger?.LogWarning(e.Message); + base.OnParseError(e); + } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XmlLocationInfo.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XmlLocationInfo.cs index bcafd6f..67d7e78 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XmlLocationInfo.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XmlLocationInfo.cs @@ -7,11 +7,10 @@ public readonly struct XmlLocationInfo(string xmlFile, int? line) { public bool HasLocation => !string.IsNullOrEmpty(XmlFile) && Line is not null; - public string? XmlFile { get; } = xmlFile; + public string XmlFile { get; } = xmlFile; public int? Line { get; } = line; - public static XmlLocationInfo FromElement(XElement element) { if (element.Document is null) @@ -21,7 +20,6 @@ public static XmlLocationInfo FromElement(XElement element) return new XmlLocationInfo(element.Document.BaseUri, null); } - public override string ToString() { if (string.IsNullOrEmpty(XmlFile)) From 52f68c00c13255b95629fb91137bdf88289e9d0f Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Wed, 24 Jul 2024 13:00:10 +0200 Subject: [PATCH 25/25] update mod verify --- .github/workflows/release.yml | 4 +- README.md | 78 ++++++++- src/ModVerify.CliApp/ModVerify.CliApp.csproj | 5 + src/ModVerify.CliApp/ModVerifyApp.cs | 78 +++++++-- src/ModVerify.CliApp/ModVerifyAppSettings.cs | 21 +++ src/ModVerify.CliApp/ModVerifyOptions.cs | 19 ++- src/ModVerify.CliApp/Program.cs | 156 +++++++----------- .../Properties/launchSettings.json | 5 +- src/ModVerify.CliApp/SettingsBuilder.cs | 76 +++++++++ .../GlobalVerificationReportSettings.cs | 4 +- .../Reporting/VerificationBaseline.cs | 6 + src/ModVerify/Settings/GameVerifySettings.cs | 4 +- src/ModVerify/Verifiers/GameVerifierBase.cs | 16 ++ .../Verifiers/ReferencedModelsVerifier.cs | 52 +++--- src/ModVerify/Verifiers/VerifierErrorCodes.cs | 10 ++ .../Verifiers/XmlParseErrorCollector.cs | 88 ++++++++++ src/ModVerify/VerifyGamePipeline.cs | 36 ++-- .../Database/GameDatabaseService.cs | 35 +++- .../Database/IGameDatabaseService.cs | 3 +- .../GameDatabaseCreationPipeline.cs | 17 +- .../ParseXmlDatabaseFromContainerStep.cs | 27 ++- .../Initialization/ParseXmlDatabaseStep.cs | 7 +- .../PetroglyphEngineServiceContribution.cs | 3 +- .../Repositories/EffectsRepository.cs | 1 + .../Repositories/FocGameRepository.cs | 4 +- .../Repositories/GameRepository.cs | 51 +++++- .../Repositories/GameRepositoryFactory.cs | 6 +- .../Repositories/IGameRepository.cs | 5 + .../Repositories/IGameRepositoryFactory.cs | 7 +- .../Xml/IPetroglyphXmlFileParserFactory.cs | 5 +- .../Xml/Parsers/Data/GameConstantsParser.cs | 4 +- .../Xml/Parsers/Data/GameObjectParser.cs | 6 +- .../Xml/Parsers/Data/SfxEventParser.cs | 8 +- .../Parsers/File/GameObjectFileFileParser.cs | 9 +- .../Xml/Parsers/File/SfxEventFileParser.cs | 50 +++--- .../Xml/Parsers/XmlObjectParser.cs | 5 +- .../Xml/PetroglyphXmlParserFactory.cs | 15 +- .../IPrimitiveXmlErrorParserProvider.cs | 3 + .../IPrimitiveXmlParserErrorListener.cs | 3 + .../ErrorHandling/IXmlParserErrorListener.cs | 8 + .../ErrorHandling/IXmlParserErrorProvider.cs | 6 + .../PrimitiveXmlParserErrorBroker.cs | 13 ++ .../ErrorHandling/XmlErrorEventHandler.cs | 5 + .../ErrorHandling/XmlParseErrorEventArgs.cs | 37 +++++ .../ErrorHandling/XmlParseErrorKind.cs | 42 +++++ ....StarWarsGame.Files.XML.csproj.DotSettings | 6 + .../ParseErrorEventArgs.cs | 74 --------- .../Parsers/IPetroglyphXmlParser.cs | 5 +- .../Parsers/PetroglyphXmlElementParser.cs | 5 +- .../Parsers/PetroglyphXmlFileParser.cs | 58 ++++++- .../Parsers/PetroglyphXmlParser.cs | 10 +- .../PetroglyphXmlPrimitiveElementParser.cs | 4 +- .../CommaSeparatedStringKeyValueListParser.cs | 3 +- .../Primitives/PetroglyphXmlBooleanParser.cs | 3 +- .../Primitives/PetroglyphXmlByteParser.cs | 7 +- .../Primitives/PetroglyphXmlFloatParser.cs | 7 +- .../Primitives/PetroglyphXmlIntegerParser.cs | 7 +- .../PetroglyphXmlLooseStringListParser.cs | 7 +- .../PetroglyphXmlMax100ByteParser.cs | 9 +- .../Primitives/PetroglyphXmlStringParser.cs | 3 +- .../PetroglyphXmlUnsignedIntegerParser.cs | 7 +- .../Primitives/PrimitiveParserProvider.cs | 58 +++++-- .../Primitives/XmlFileContainerParser.cs | 4 +- .../XmlLocationInfo.cs | 4 +- .../XmlServiceContribution.cs | 3 + 65 files changed, 955 insertions(+), 372 deletions(-) create mode 100644 src/ModVerify.CliApp/ModVerifyAppSettings.cs create mode 100644 src/ModVerify.CliApp/SettingsBuilder.cs create mode 100644 src/ModVerify/Verifiers/XmlParseErrorCollector.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IPrimitiveXmlErrorParserProvider.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IPrimitiveXmlParserErrorListener.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IXmlParserErrorListener.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IXmlParserErrorProvider.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/PrimitiveXmlParserErrorBroker.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlErrorEventHandler.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlParseErrorEventArgs.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlParseErrorKind.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/PG.StarWarsGame.Files.XML.csproj.DotSettings delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.XML/ParseErrorEventArgs.cs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 41accc4..1c7a7be 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,6 +55,8 @@ jobs: # Change into the artifacts directory to avoid including the directory itself in the zip archive working-directory: ./releases/net8.0 run: zip -r ../ModVerify-Net8.zip . + - name: Rename .NET Framework executable + run: mv ./releases/net48/ModVerify.CliApp.exe ./releases/net48/ModVerify.exe - uses: dotnet/nbgv@v0.4.2 id: nbgv - name: Create GitHub release @@ -65,5 +67,5 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} generate_release_notes: true files: | - ./releases/net48/ModVerify.CliApp.exe + ./releases/net48/ModVerify.exe ./releases/ModVerify-Net8.zip \ No newline at end of file diff --git a/README.md b/README.md index bb3e92a..3555a77 100644 --- a/README.md +++ b/README.md @@ -1 +1,77 @@ -# ModVerify \ No newline at end of file +# ModVerify: A Mod Verification Tool + +ModVerify is a command-line tool designed to verify mods for the game Star Wars: Empire at War and its expansion Forces of Corruption. + +## Table of Contents + +- [Installation](#installation) +- [Usage](#usage) +- [Options](#options) +- [Available Checks](#available-checks) + +## Installation + +Download the latest release from the [releases page](https://github.com/AlamoEngine-Tools/ModVerify/releases). There are two versions of the application available. + +1. `ModVerify.exe` is the default version. Use this if you simply want to verify your mods. This version only works on Windows. +2. `ModVerify-NetX.zip` is the cross-platform app. It works on Windows and Linux and is most likely the version you want to use to include it in some CI/CD scenarios. + +You can place the files anywhere on your system, eg. your Desktop. There is no need to place it inside a mod's directory. + +*Note: Both versions have the exact same feature set. They just target a different .NET runtime. Linux and CI/CD support is not fully tested yet. Current priority is on the Windows-only version.* + +## Usage + +Simply run the executable file `ModVerify.exe`. + +When given no specific argument through the command line, the app will ask you which game or mod you want to verify. When the tool is done, it will write the verification results into new files next to the executable. + +A `.JSON` file lists all found issues. Into seperate `.txt` files the same errors get grouped by a category of the finding. The text files may be easier to read, while the json file is more useful for 3rd party tool processing. + +## Options + +You can also run the tool with command line arguments to adjust the tool to your needs. + +To see all available options, open the command line and type: + +```bash +ModVerify.exe --help +``` + +Here is a list of the most relevant options: + +### `--path` +Specifies a path that shall be analyzed. **There will be no user input required when using this option** + +### `--output` +Specified the output path where analysis result shall be written to. + +### `--baseline` +Specifies a baseline file that shall be used to filter out known errors. You can download the [FoC baseline](focBaseline.json) which includes all errors produced by the vanilla game. + +### `--createBaseline` +If you want to create your own baseline, add this option with a file path such as `myModBaseline.json`. + +### Example +This is an example run configuration that analyzes a specific mod, uses a the FoC basline and writes the output into a dedicated directory: + +```bash +ModVerify.exe --path "C:\My Games\FoC\Mods\MyMod" --output "C:\My Games\FoC\Mods\MyMod\verifyResults" --baseline focBaseline.json +``` + + +## Available Checks + +The following verifiers are currently implemented: + +### For SFX Events: +- Checks whether coded preset exists +- Checks the referenced samples for validity (bit rate, sample size, channels, etc.) +- Duplicates + + +### For GameObjects +- Checks the referenced models for validity (textures, particles and shaders) +- Duplicates + + diff --git a/src/ModVerify.CliApp/ModVerify.CliApp.csproj b/src/ModVerify.CliApp/ModVerify.CliApp.csproj index 5d511d2..22a7164 100644 --- a/src/ModVerify.CliApp/ModVerify.CliApp.csproj +++ b/src/ModVerify.CliApp/ModVerify.CliApp.csproj @@ -18,6 +18,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -36,6 +40,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/ModVerify.CliApp/ModVerifyApp.cs b/src/ModVerify.CliApp/ModVerifyApp.cs index be1b8dd..0d6929b 100644 --- a/src/ModVerify.CliApp/ModVerifyApp.cs +++ b/src/ModVerify.CliApp/ModVerifyApp.cs @@ -1,36 +1,90 @@ using System; using System.Collections.Generic; +using System.IO; using System.IO.Abstractions; +using System.Linq; using System.Threading.Tasks; using AET.ModVerify; using AET.ModVerify.Reporting; -using AET.ModVerify.Settings; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Infrastructure.Mods; +using PG.StarWarsGame.Infrastructure.Services.Dependencies; namespace ModVerify.CliApp; -internal class ModVerifyApp(GameVerifySettings settings, IServiceProvider services) +internal class ModVerifyApp(ModVerifyAppSettings settings, IServiceProvider services) { private readonly ILogger? _logger = services.GetService()?.CreateLogger(typeof(ModVerifyApp)); private readonly IFileSystem _fileSystem = services.GetRequiredService(); - private IReadOnlyCollection Errors { get; set; } = Array.Empty(); - - public async Task RunVerification(VerifyGameSetupData gameSetupData) + public async Task RunApplication() { - var verifyPipeline = BuildPipeline(gameSetupData); - _logger?.LogInformation($"Verifying {gameSetupData.VerifyObject.Name}..."); - await verifyPipeline.RunAsync(); - _logger?.LogInformation("Finished Verifying"); + var returnCode = 0; + + var gameSetupData = CreateGameSetupData(settings, services); + var verifyPipeline = new ModVerifyPipeline(gameSetupData.EngineType, gameSetupData.GameLocations, settings.GameVerifySettigns, services); + + try + { + _logger?.LogInformation($"Verifying {gameSetupData.VerifyObject.Name}..."); + await verifyPipeline.RunAsync().ConfigureAwait(false); + _logger?.LogInformation("Finished Verifying"); + } + catch (GameVerificationException e) + { + returnCode = e.HResult; + } + + if (settings.CreateNewBaseline) + await WriteBaseline(verifyPipeline.Errors, settings.NewBaselinePath).ConfigureAwait(false); + + return returnCode; } - private VerifyGamePipeline BuildPipeline(VerifyGameSetupData setupData) + private async Task WriteBaseline(IEnumerable errors, string baselineFile) { - return new ModVerifyPipeline(setupData.EngineType, setupData.GameLocations, settings, services); + var fullPath = _fileSystem.Path.GetFullPath(baselineFile); +#if NET + await +#endif + using var fs = _fileSystem.FileStream.New(fullPath, FileMode.Create, FileAccess.Write, FileShare.None); + var baseline = new VerificationBaseline(errors); + await baseline.ToJsonAsync(fs); } - public async Task WriteBaseline(string baselineFile) + private static VerifyGameSetupData CreateGameSetupData(ModVerifyAppSettings options, IServiceProvider services) { + var selectionResult = new ModOrGameSelector(services).SelectModOrGame(options.PathToVerify); + + IList mods = Array.Empty(); + if (selectionResult.ModOrGame is IMod mod) + { + var traverser = services.GetRequiredService(); + mods = traverser.Traverse(mod) + .Select(x => x.Mod) + .OfType().Select(x => x.Directory.FullName) + .ToList(); + } + + var fallbackPaths = new List(); + if (selectionResult.FallbackGame is not null) + fallbackPaths.Add(selectionResult.FallbackGame.Directory.FullName); + + if (!string.IsNullOrEmpty(options.AdditionalFallbackPath)) + fallbackPaths.Add(options.AdditionalFallbackPath); + + var gameLocations = new GameLocations( + mods, + selectionResult.ModOrGame.Game.Directory.FullName, + fallbackPaths); + + return new VerifyGameSetupData + { + EngineType = GameEngineType.Foc, + GameLocations = gameLocations, + VerifyObject = selectionResult.ModOrGame, + }; } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModVerifyAppSettings.cs b/src/ModVerify.CliApp/ModVerifyAppSettings.cs new file mode 100644 index 0000000..3e7092c --- /dev/null +++ b/src/ModVerify.CliApp/ModVerifyAppSettings.cs @@ -0,0 +1,21 @@ +using System; +using AET.ModVerify.Settings; +using System.Diagnostics.CodeAnalysis; + +namespace ModVerify.CliApp; + +public record ModVerifyAppSettings +{ + public GameVerifySettings GameVerifySettigns { get; init; } + + public string? PathToVerify { get; init; } = null; + + public string Output { get; init; } = Environment.CurrentDirectory; + + public string? AdditionalFallbackPath { get; init; } = null; + + [MemberNotNullWhen(true, nameof(NewBaselinePath))] + public bool CreateNewBaseline => !string.IsNullOrEmpty(NewBaselinePath); + + public string? NewBaselinePath { get; init; } +} \ No newline at end of file diff --git a/src/ModVerify.CliApp/ModVerifyOptions.cs b/src/ModVerify.CliApp/ModVerifyOptions.cs index 0622bf6..323b78d 100644 --- a/src/ModVerify.CliApp/ModVerifyOptions.cs +++ b/src/ModVerify.CliApp/ModVerifyOptions.cs @@ -1,4 +1,5 @@ -using CommandLine; +using AET.ModVerify.Reporting; +using CommandLine; namespace ModVerify.CliApp; @@ -6,9 +7,12 @@ internal class ModVerifyOptions { [Option('p', "path", Required = false, HelpText = "The path to a mod directory to verify. If not path is specified, " + - "the app search for mods and asks the user to select a game or mod.")] + "the app searches for game installations and asks the user to select a game or mod.")] public string? Path { get; set; } + [Option('o', "output", Required = false, HelpText = "directory where result files shall be stored to.")] + public string? Output { get; set; } + [Option('v', "verbose", Required = false, HelpText = "Set output to verbose messages.")] public bool Verbose { get; set; } @@ -18,7 +22,16 @@ internal class ModVerifyOptions [Option("suppressions", Required = false, HelpText = "Path to a JSON suppression file.")] public string? Suppressions { get; set; } - [Option("createBaseLine", Required = false, + [Option("minFailSeverity", Required = false, Default = null, + HelpText = "When set, the application return with an error, if any finding has at least the specified severity value.")] + public VerificationSeverity? MinimumFailureSeverity { get; set; } + + + [Option("failFast", Required = false, Default = false, + HelpText = "When set, the application will abort on the first failure. The option also recognized the 'MinimumFailureSeverity' setting.")] + public bool FailFast { get; set; } + + [Option("createBaseline", Required = false, HelpText = "When a path is specified, the tools creates a new baseline file. " + "An existing file will be overwritten. " + "Previous baselines are merged into the new baseline.")] diff --git a/src/ModVerify.CliApp/Program.cs b/src/ModVerify.CliApp/Program.cs index ad81084..1a99252 100644 --- a/src/ModVerify.CliApp/Program.cs +++ b/src/ModVerify.CliApp/Program.cs @@ -1,21 +1,17 @@ using System; -using System.Collections.Generic; -using System.IO; using System.IO.Abstractions; -using System.Linq; using System.Runtime.CompilerServices; using System.Threading.Tasks; using AET.ModVerify; -using AET.ModVerify.Reporting; using AET.ModVerify.Reporting.Reporters; -using AET.ModVerify.Settings; +using AET.ModVerify.Reporting.Reporters.JSON; +using AET.ModVerify.Reporting.Reporters.Text; using AET.SteamAbstraction; using AnakinRaW.CommonUtilities.FileSystem; using AnakinRaW.CommonUtilities.Hashing; using AnakinRaW.CommonUtilities.Registry; using AnakinRaW.CommonUtilities.Registry.Windows; using CommandLine; -using CommandLine.Text; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Console; @@ -27,8 +23,6 @@ using PG.StarWarsGame.Files.XML; using PG.StarWarsGame.Infrastructure; using PG.StarWarsGame.Infrastructure.Clients; -using PG.StarWarsGame.Infrastructure.Mods; -using PG.StarWarsGame.Infrastructure.Services.Dependencies; using Serilog; using Serilog.Events; using Serilog.Filters; @@ -55,7 +49,6 @@ await parseResult.WithParsedAsync(o => await parseResult.WithNotParsedAsync(e => { - Console.WriteLine(HelpText.AutoBuild(parseResult).ToString()); result = 0xA0; return Task.CompletedTask; }).ConfigureAwait(false); @@ -67,100 +60,45 @@ await parseResult.WithNotParsedAsync(e => throw new InvalidOperationException("Mod verify was executed with the wrong arguments."); } - var services = CreateAppServices(verifyOptions.Verbose); - var settings = BuildSettings(verifyOptions, services); - var gameSetupData = CreateGameSetupData(verifyOptions, services); - - var verifier = new ModVerifyApp(settings, services); - await verifier.RunVerification(gameSetupData).ConfigureAwait(false); - - if (verifyOptions.NewBaselineFile is not null) - await verifier.WriteBaseline(verifyOptions.NewBaselineFile).ConfigureAwait(false); - - return 0; - } - - private static VerifyGameSetupData CreateGameSetupData(ModVerifyOptions options, IServiceProvider services) - { - var selectionResult = new ModOrGameSelector(services).SelectModOrGame(options.Path); - - IList mods = Array.Empty(); - if (selectionResult.ModOrGame is IMod mod) + var coreServiceCollection = CreateCoreServices(verifyOptions.Verbose); + var coreServices = coreServiceCollection.BuildServiceProvider(); + var logger = coreServices.GetService()?.CreateLogger(typeof(Program)); + try { - var traverser = services.GetRequiredService(); - mods = traverser.Traverse(mod) - .Select(x => x.Mod) - .OfType().Select(x => x.Directory.FullName) - .ToList(); - } + var settings = new SettingsBuilder(coreServices) + .BuildSettings(verifyOptions); - var fallbackPaths = new List(); - if (selectionResult.FallbackGame is not null) - fallbackPaths.Add(selectionResult.FallbackGame.Directory.FullName); + var services = CreateAppServices(coreServiceCollection, settings); - if (!string.IsNullOrEmpty(options.AdditionalFallbackPath)) - fallbackPaths.Add(options.AdditionalFallbackPath); - - var gameLocations = new GameLocations( - mods, - selectionResult.ModOrGame.Game.Directory.FullName, - fallbackPaths); + var verifier = new ModVerifyApp(settings, services); - return new VerifyGameSetupData + return await verifier.RunApplication().ConfigureAwait(false); + } + catch (Exception e) { - EngineType = GameEngineType.Foc, - GameLocations = gameLocations, - VerifyObject = selectionResult.ModOrGame, - }; + logger?.LogCritical(e, e.Message); + Console.WriteLine(e.Message); + return e.HResult; + } } - - private static GameVerifySettings BuildSettings(ModVerifyOptions options, IServiceProvider services) + + private static IServiceCollection CreateCoreServices(bool verboseLogging) { - var settings = GameVerifySettings.Default; - - var reportSettings = settings.GlobalReportSettings; + var fileSystem = new FileSystem(); + var serviceCollection = new ServiceCollection(); - if (options.Baseline is not null) - { - using var fs = services.GetRequiredService().FileStream - .New(options.Baseline, FileMode.Open, FileAccess.Read); - var baseline = VerificationBaseline.FromJson(fs); - - reportSettings = reportSettings with - { - Baseline = baseline - }; - } + serviceCollection.AddSingleton(new WindowsRegistry()); + serviceCollection.AddSingleton(fileSystem); - if (options.Suppressions is not null) - { - using var fs = services.GetRequiredService().FileStream - .New(options.Suppressions, FileMode.Open, FileAccess.Read); - var baseline = SuppressionList.FromJson(fs); - - reportSettings = reportSettings with - { - Suppressions = baseline - }; - } + serviceCollection.AddLogging(builder => ConfigureLogging(builder, fileSystem, verboseLogging)); - return settings with - { - GlobalReportSettings = reportSettings - }; + return serviceCollection; } - private static IServiceProvider CreateAppServices(bool verbose) + private static IServiceProvider CreateAppServices(IServiceCollection serviceCollection, ModVerifyAppSettings settings) { - var fileSystem = new FileSystem(); - var serviceCollection = new ServiceCollection(); - - serviceCollection.AddSingleton(new WindowsRegistry()); serviceCollection.AddSingleton(sp => new HashingService(sp)); - serviceCollection.AddSingleton(fileSystem); - - serviceCollection.AddLogging(builder => ConfigureLogging(builder, fileSystem, verbose)); SteamAbstractionLayer.InitializeServices(serviceCollection); PetroglyphGameClients.InitializeServices(serviceCollection); @@ -173,14 +111,31 @@ private static IServiceProvider CreateAppServices(bool verbose) XmlServiceContribution.ContributeServices(serviceCollection); PetroglyphEngineServiceContribution.ContributeServices(serviceCollection); + ModVerifyServiceContribution.ContributeServices(serviceCollection); - serviceCollection.RegisterConsoleReporter(); - serviceCollection.RegisterJsonReporter(); - serviceCollection.RegisterTextFileReporter(); + SetupReporting(serviceCollection, settings); + return serviceCollection.BuildServiceProvider(); } + private static void SetupReporting(IServiceCollection serviceCollection, ModVerifyAppSettings settings) + { + serviceCollection.RegisterConsoleReporter(); + + serviceCollection.RegisterJsonReporter(new JsonReporterSettings + { + OutputDirectory = settings.Output, + MinimumReportSeverity = settings.GameVerifySettigns.GlobalReportSettings.MinimumReportSeverity + }); + + serviceCollection.RegisterTextFileReporter(new TextFileReporterSettings + { + OutputDirectory = settings.Output, + MinimumReportSeverity = settings.GameVerifySettigns.GlobalReportSettings.MinimumReportSeverity + }); + } + private static void ConfigureLogging(ILoggingBuilder loggingBuilder, IFileSystem fileSystem, bool verbose) { loggingBuilder.ClearProviders(); @@ -198,8 +153,8 @@ private static void ConfigureLogging(ILoggingBuilder loggingBuilder, IFileSystem } #endif loggingBuilder.SetMinimumLevel(logLevel); - - SetupXmlParseLogging(loggingBuilder, fileSystem); + + SetupFileLogging(loggingBuilder, fileSystem); loggingBuilder.AddFilter((category, level) => { @@ -212,18 +167,19 @@ private static void ConfigureLogging(ILoggingBuilder loggingBuilder, IFileSystem return true; }).AddConsole(); } - - private static void SetupXmlParseLogging(ILoggingBuilder loggingBuilder, IFileSystem fileSystem) + + private static void SetupFileLogging(ILoggingBuilder loggingBuilder, IFileSystem fileSystem) { - const string xmlParseLogFileName = "XmlParseLog.txt"; + var logPath = fileSystem.Path.Combine(fileSystem.Path.GetTempPath(), "ModVerify_log.txt"); - fileSystem.File.TryDeleteWithRetry(xmlParseLogFileName); var logger = new LoggerConfiguration() .Enrich.FromLogContext() - .MinimumLevel.Warning() - .Filter.ByIncludingOnly(IsXmlParserLogging) - .WriteTo.File(xmlParseLogFileName, outputTemplate: "[{Level:u3}] [{SourceContext}] {Message}{NewLine}{Exception}") + .MinimumLevel.Verbose() + .Filter.ByExcluding(IsXmlParserLogging) + .WriteTo.RollingFile( + logPath, + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{SourceContext}] {Message}{NewLine}{Exception}") .CreateLogger(); loggingBuilder.AddSerilog(logger); diff --git a/src/ModVerify.CliApp/Properties/launchSettings.json b/src/ModVerify.CliApp/Properties/launchSettings.json index e0ea1a3..2bbebc6 100644 --- a/src/ModVerify.CliApp/Properties/launchSettings.json +++ b/src/ModVerify.CliApp/Properties/launchSettings.json @@ -1,11 +1,12 @@ { "profiles": { "No-Arguments": { - "commandName": "Project" + "commandName": "Project", + "commandLineArgs": "-o verifyResults --minFailSeverity Information --baseline test.json" }, "From-path": { "commandName": "Project", - "commandLineArgs": "-p \"C:\\Program Files (x86)\\Steam\\steamapps\\common\\Star Wars Empire at War\\corruption\\Mods\\Republic_at_War\"" + "commandLineArgs": "-p \"C:\\Program Files (x86)\\Steam\\steamapps\\common\\Star Wars Empire at War\\corruption\\Mods\\Republic_at_War\" --baseline test.json" } } } \ No newline at end of file diff --git a/src/ModVerify.CliApp/SettingsBuilder.cs b/src/ModVerify.CliApp/SettingsBuilder.cs new file mode 100644 index 0000000..3821bb8 --- /dev/null +++ b/src/ModVerify.CliApp/SettingsBuilder.cs @@ -0,0 +1,76 @@ +using System; +using System.IO; +using System.IO.Abstractions; +using AET.ModVerify.Reporting; +using AET.ModVerify.Settings; +using Microsoft.Extensions.DependencyInjection; + +namespace ModVerify.CliApp; + +internal class SettingsBuilder(IServiceProvider services) +{ + private readonly IFileSystem _fileSystem = services.GetRequiredService(); + + public ModVerifyAppSettings BuildSettings(ModVerifyOptions options) + { + var output = Environment.CurrentDirectory; + if (!string.IsNullOrEmpty(options.Output)) + output = _fileSystem.Path.GetFullPath(_fileSystem.Path.Combine(Environment.CurrentDirectory, options.Output)); + + + return new ModVerifyAppSettings + { + GameVerifySettigns = BuildGameVerifySettings(options), + PathToVerify = options.Path, + Output = output, + AdditionalFallbackPath = options.AdditionalFallbackPath, + NewBaselinePath = options.NewBaselineFile + }; + } + + private GameVerifySettings BuildGameVerifySettings(ModVerifyOptions options) + { + var settings = GameVerifySettings.Default; + + return settings with + { + GlobalReportSettings = BuilderGlobalReportSettings(options), + AbortSettings = BuildAbortSettings(options) + }; + } + + private VerificationAbortSettings BuildAbortSettings(ModVerifyOptions options) + { + return new VerificationAbortSettings + { + FailFast = options.FailFast, + MinimumAbortSeverity = options.MinimumFailureSeverity ?? VerificationSeverity.Information, + ThrowsGameVerificationException = options.MinimumFailureSeverity.HasValue || options.FailFast + }; + } + + private GlobalVerificationReportSettings BuilderGlobalReportSettings(ModVerifyOptions options) + { + var baseline = VerificationBaseline.Empty; + var suppressions = SuppressionList.Empty; + + if (options.Baseline is not null) + { + using var fs = _fileSystem.FileStream.New(options.Baseline, FileMode.Open, FileAccess.Read); + baseline = VerificationBaseline.FromJson(fs); + } + + if (options.Suppressions is not null) + { + using var fs = _fileSystem.FileStream.New(options.Suppressions, FileMode.Open, FileAccess.Read); + suppressions = SuppressionList.FromJson(fs); + } + + return new GlobalVerificationReportSettings + { + Baseline = baseline, + Suppressions = suppressions, + MinimumReportSeverity = VerificationSeverity.Information, + }; + } +} \ No newline at end of file diff --git a/src/ModVerify/Reporting/GlobalVerificationReportSettings.cs b/src/ModVerify/Reporting/GlobalVerificationReportSettings.cs index a94225e..3982d4d 100644 --- a/src/ModVerify/Reporting/GlobalVerificationReportSettings.cs +++ b/src/ModVerify/Reporting/GlobalVerificationReportSettings.cs @@ -1,4 +1,6 @@ -namespace AET.ModVerify.Reporting; +using System; + +namespace AET.ModVerify.Reporting; public record GlobalVerificationReportSettings : VerificationReportSettings { diff --git a/src/ModVerify/Reporting/VerificationBaseline.cs b/src/ModVerify/Reporting/VerificationBaseline.cs index 3305b6f..05bdcbd 100644 --- a/src/ModVerify/Reporting/VerificationBaseline.cs +++ b/src/ModVerify/Reporting/VerificationBaseline.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Text.Json; +using System.Threading.Tasks; using AET.ModVerify.Reporting.Json; namespace AET.ModVerify.Reporting; @@ -32,6 +33,11 @@ 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); + } + public static VerificationBaseline FromJson(Stream stream) { var baselineJson = JsonSerializer.Deserialize(stream, JsonSerializerOptions.Default); diff --git a/src/ModVerify/Settings/GameVerifySettings.cs b/src/ModVerify/Settings/GameVerifySettings.cs index cd95a09..488bfe4 100644 --- a/src/ModVerify/Settings/GameVerifySettings.cs +++ b/src/ModVerify/Settings/GameVerifySettings.cs @@ -4,8 +4,6 @@ namespace AET.ModVerify.Settings; public record GameVerifySettings { - public int ParallelVerifiers { get; init; } = 4; - public static readonly GameVerifySettings Default = new() { AbortSettings = new(), @@ -13,6 +11,8 @@ public record GameVerifySettings LocalizationOption = VerifyLocalizationOption.English }; + public int ParallelVerifiers { get; init; } = 4; + public VerificationAbortSettings AbortSettings { get; init; } public GlobalVerificationReportSettings GlobalReportSettings { get; init; } diff --git a/src/ModVerify/Verifiers/GameVerifierBase.cs b/src/ModVerify/Verifiers/GameVerifierBase.cs index fff7dca..a1f10cd 100644 --- a/src/ModVerify/Verifiers/GameVerifierBase.cs +++ b/src/ModVerify/Verifiers/GameVerifierBase.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO.Abstractions; using System.Threading; using AET.ModVerify.Reporting; using AET.ModVerify.Settings; +using AnakinRaW.CommonUtilities.FileSystem; using AnakinRaW.CommonUtilities.SimplePipeline.Steps; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -68,4 +70,18 @@ protected void GuardedVerify(Action action, Predicate exceptionFilter exceptionHandler(e); } } + + protected string GetGameStrippedPath(string path) + { + if (!FileSystem.Path.IsPathFullyQualified(path)) + return path; + + if (path.Length <= Repository.Path.Length) + return path; + + if (path.StartsWith(Repository.Path, StringComparison.OrdinalIgnoreCase)) + return path.Substring(Repository.Path.Length); + + return path; + } } \ No newline at end of file diff --git a/src/ModVerify/Verifiers/ReferencedModelsVerifier.cs b/src/ModVerify/Verifiers/ReferencedModelsVerifier.cs index 46fd7e3..8a6e7ba 100644 --- a/src/ModVerify/Verifiers/ReferencedModelsVerifier.cs +++ b/src/ModVerify/Verifiers/ReferencedModelsVerifier.cs @@ -16,6 +16,7 @@ using PG.StarWarsGame.Files.ALO.Services; using PG.StarWarsGame.Files.ChunkFiles.Data; using AnakinRaW.CommonUtilities.FileSystem; +using System.Reflection; namespace AET.ModVerify.Verifiers; @@ -84,7 +85,7 @@ private void VerifyModelOrParticle(Stream modelStream, Queue workingQueu } catch (BinaryCorruptedException e) { - var aloFile = modelStream.GetFilePath(); + var aloFile = GetGameStrippedPath(modelStream.GetFilePath()); var message = $"{aloFile} is corrupted: {e.Message}"; AddError(VerificationError.Create(this, VerifierErrorCodes.ModelBroken, message, VerificationSeverity.Critical, aloFile)); } @@ -98,14 +99,15 @@ private void VerifyParticle(IAloParticleFile file) e => e is ArgumentException, _ => { + var modelFilePath = GetGameStrippedPath(file.FilePath); AddError(VerificationError.Create( this, VerifierErrorCodes.InvalidTexture, $"Invalid texture file name" + - $" '{texture}' in particle '{file.FilePath}'", + $" '{texture}' in particle 'modelFilePath'", VerificationSeverity.Error, - texture, - file.FilePath)); + texture, + modelFilePath)); }); } @@ -114,12 +116,13 @@ private void VerifyParticle(IAloParticleFile file) if (!fileName.Equals(name, StringComparison.OrdinalIgnoreCase)) { + var modelFilePath = GetGameStrippedPath(file.FilePath); AddError(VerificationError.Create( this, VerifierErrorCodes.InvalidParticleName, - $"The particle name '{file.Content.Name}' does not match file name '{fileName.ToString()}'", - VerificationSeverity.Error, - file.FilePath)); + $"The particle name '{file.Content.Name}' does not match file name '{modelFilePath}'", + VerificationSeverity.Error, + modelFilePath)); } } @@ -132,13 +135,13 @@ private void VerifyModel(IAloModelFile file, Queue workingQueue) e => e is ArgumentException, _ => { + var modelFilePath = GetGameStrippedPath(file.FilePath); AddError(VerificationError.Create( this, VerifierErrorCodes.InvalidTexture, - $"Invalid texture file name" + - $" '{texture}' in model '{file.FilePath}'", + $"Invalid texture file name '{texture}' in model '{modelFilePath}'", VerificationSeverity.Error, - texture, file.FilePath)); + texture, modelFilePath)); }); } @@ -148,13 +151,13 @@ private void VerifyModel(IAloModelFile file, Queue workingQueue) e => e is ArgumentException, _ => { + var modelFilePath = GetGameStrippedPath(file.FilePath); AddError(VerificationError.Create( this, VerifierErrorCodes.InvalidShader, - $"Invalid texture file name" + - $" '{shader}' in model '{file.FilePath}'", + $"Invalid texture file name '{shader}' in model '{modelFilePath}'", VerificationSeverity.Error, - shader, file.FilePath)); + shader, modelFilePath)); }); } @@ -165,13 +168,13 @@ private void VerifyModel(IAloModelFile file, Queue workingQueue) e => e is ArgumentException, _ => { + var modelFilePath = GetGameStrippedPath(file.FilePath); AddError(VerificationError.Create( this, VerifierErrorCodes.InvalidProxy, - $"Invalid proxy file name" + - $" '{proxy}' in model '{file.FilePath}'", + $"Invalid proxy file name '{proxy}' in model '{modelFilePath}'", VerificationSeverity.Error, - proxy, file.FilePath)); + proxy, modelFilePath)); }); } } @@ -183,8 +186,9 @@ private void VerifyTextureExists(IPetroglyphFileHolder xmlErrors, + IGameDatabase gameDatabase, + GameVerifySettings settings, + IServiceProvider serviceProvider) : + GameVerifierBase(gameDatabase, settings, serviceProvider) +{ + public override string FriendlyName => "XML Parsing Errors"; + + protected override void RunVerification(CancellationToken token) + { + foreach (var xmlError in xmlErrors) + AddError(ConvertXmlToVerificationError(xmlError)); + } + + private VerificationError ConvertXmlToVerificationError(XmlParseErrorEventArgs xmlError) + { + var id = GetIdFromError(xmlError.ErrorKind); + var severity = GetSeverityFromError(xmlError.ErrorKind); + + var assets = new List + { + GetGameStrippedPath(xmlError.File.ToUpperInvariant()) + }; + + var xmlElement = xmlError.Element; + + if (xmlElement is not null) + { + assets.Add(xmlElement.Name.LocalName); + + var parent = xmlElement.Parent; + + if (parent != null) + { + var parentName = parent.Attribute("Name"); + assets.Add(parentName != null ? $"parentName='{parentName.Value}'" : $"parentTag='{parent.Name.LocalName}'"); + } + + } + + return VerificationError.Create(this, id, xmlError.Message, severity, assets); + + } + + private VerificationSeverity GetSeverityFromError(XmlParseErrorKind xmlErrorErrorKind) + { + return xmlErrorErrorKind switch + { + XmlParseErrorKind.EmptyRoot => VerificationSeverity.Critical, + XmlParseErrorKind.MissingFile => VerificationSeverity.Error, + XmlParseErrorKind.InvalidValue => VerificationSeverity.Information, + XmlParseErrorKind.MalformedValue => VerificationSeverity.Warning, + XmlParseErrorKind.MissingAttribute => VerificationSeverity.Error, + XmlParseErrorKind.MissingReference => VerificationSeverity.Error, + XmlParseErrorKind.TooLongData => VerificationSeverity.Warning, + XmlParseErrorKind.DataBeforeHeader => VerificationSeverity.Information, + _ => VerificationSeverity.Warning + }; + } + + private string GetIdFromError(XmlParseErrorKind xmlErrorErrorKind) + { + return xmlErrorErrorKind switch + { + XmlParseErrorKind.EmptyRoot => VerifierErrorCodes.EmptyXmlRoot, + XmlParseErrorKind.MissingFile => VerifierErrorCodes.MissingXmlFile, + XmlParseErrorKind.InvalidValue => VerifierErrorCodes.InvalidXmlValue, + XmlParseErrorKind.MalformedValue => VerifierErrorCodes.MalformedXmlValue, + XmlParseErrorKind.MissingAttribute => VerifierErrorCodes.MissingXmlAttribute, + XmlParseErrorKind.MissingReference => VerifierErrorCodes.MissingXmlReference, + XmlParseErrorKind.TooLongData => VerifierErrorCodes.XmlValueTooLong, + XmlParseErrorKind.Unknown => VerifierErrorCodes.GenericXmlError, + XmlParseErrorKind.DataBeforeHeader => VerifierErrorCodes.XmlDataBeforeHeader, + _ => throw new ArgumentOutOfRangeException(nameof(xmlErrorErrorKind), xmlErrorErrorKind, null) + }; + } +} \ No newline at end of file diff --git a/src/ModVerify/VerifyGamePipeline.cs b/src/ModVerify/VerifyGamePipeline.cs index 2423f30..994253e 100644 --- a/src/ModVerify/VerifyGamePipeline.cs +++ b/src/ModVerify/VerifyGamePipeline.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -12,6 +13,8 @@ using Microsoft.Extensions.Logging; using PG.StarWarsGame.Engine; using PG.StarWarsGame.Engine.Database; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.Parsers; namespace AET.ModVerify; @@ -21,11 +24,13 @@ public abstract class VerifyGamePipeline : Pipeline private readonly GameEngineType _targetType; private readonly GameLocations _gameLocations; private readonly ParallelRunner _verifyRunner; - + protected GameVerifySettings Settings { get; } public IReadOnlyCollection Errors { get; private set; } = Array.Empty(); + private readonly ConcurrentBag _xmlParseErrors = new(); + protected VerifyGamePipeline(GameEngineType targetType, GameLocations gameLocations, GameVerifySettings settings, IServiceProvider serviceProvider) : base(serviceProvider) { @@ -56,19 +61,20 @@ protected sealed override async Task RunCoreAsync(CancellationToken token) IGameDatabase database; try { - //databaseService.XmlParseError += OnXmlParseError; + databaseService.XmlParseError += OnXmlParseError; database = await databaseService.CreateDatabaseAsync(_targetType, _gameLocations, token); } finally { - //databaseService.XmlParseError -= OnXmlParseError; + databaseService.XmlParseError -= OnXmlParseError; + databaseService.Dispose(); } - foreach (var gameVerificationStep in CreateVerificationSteps(database)) - { - _verifyRunner.AddStep(gameVerificationStep); - _verificationSteps.Add(gameVerificationStep); - } + + AddStep(new XmlParseErrorCollector(_xmlParseErrors, database, Settings, ServiceProvider)); + + foreach (var gameVerificationStep in CreateVerificationSteps(database)) + AddStep(gameVerificationStep); try { @@ -83,7 +89,7 @@ protected sealed override async Task RunCoreAsync(CancellationToken token) } - Logger?.LogInformation("Reporting Errors..."); + Logger?.LogInformation("Reporting Errors"); var reportBroker = new VerificationReportBroker(Settings.GlobalReportSettings, ServiceProvider); var errors = reportBroker.Report(_verificationSteps); @@ -100,10 +106,16 @@ protected sealed override async Task RunCoreAsync(CancellationToken token) } } - private void OnXmlParseError(object sender, EventArgs e) - { + protected abstract IEnumerable CreateVerificationSteps(IGameDatabase database); + private void AddStep(GameVerifierBase verifier) + { + _verifyRunner.AddStep(verifier); + _verificationSteps.Add(verifier); } - protected abstract IEnumerable CreateVerificationSteps(IGameDatabase database); + private void OnXmlParseError(IPetroglyphXmlParser sender, XmlParseErrorEventArgs e) + { + _xmlParseErrors.Add(e); + } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabaseService.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabaseService.cs index 2c1885f..fc1f5ca 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabaseService.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/GameDatabaseService.cs @@ -1,24 +1,51 @@ using System; using System.Threading; using System.Threading.Tasks; +using AnakinRaW.CommonUtilities; using Microsoft.Extensions.DependencyInjection; using PG.StarWarsGame.Engine.Database.Initialization; using PG.StarWarsGame.Engine.Repositories; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.Parsers; namespace PG.StarWarsGame.Engine.Database; -internal class GameDatabaseService(IServiceProvider serviceProvider) : IGameDatabaseService +internal class GameDatabaseService : DisposableObject, IXmlParserErrorListener, IGameDatabaseService { + public event XmlErrorEventHandler? XmlParseError; + + private readonly IServiceProvider _serviceProvider; + private IPrimitiveXmlErrorParserProvider _primitiveXmlParserErrorProvider; + + public GameDatabaseService(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + _primitiveXmlParserErrorProvider = serviceProvider.GetRequiredService(); + _primitiveXmlParserErrorProvider.XmlParseError += OnXmlParseError; + } + public async Task CreateDatabaseAsync( GameEngineType targetEngineType, GameLocations locations, CancellationToken cancellationToken = default) { - var repoFactory = serviceProvider.GetRequiredService(); - var repository = repoFactory.Create(targetEngineType, locations); + var repoFactory = _serviceProvider.GetRequiredService(); + var repository = repoFactory.Create(targetEngineType, locations, this); - var pipeline = new GameDatabaseCreationPipeline(repository, serviceProvider); + var pipeline = new GameDatabaseCreationPipeline(repository, this, _serviceProvider); await pipeline.RunAsync(cancellationToken); return pipeline.GameDatabase; } + + protected override void DisposeManagedResources() + { + base.DisposeManagedResources(); + _primitiveXmlParserErrorProvider.XmlParseError -= OnXmlParseError; + _primitiveXmlParserErrorProvider = null!; + } + + public void OnXmlParseError(IPetroglyphXmlParser parser, XmlParseErrorEventArgs error) + { + XmlParseError?.Invoke(parser, error); + } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabaseService.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabaseService.cs index 15c0c6f..575c521 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabaseService.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/IGameDatabaseService.cs @@ -1,10 +1,11 @@ using System; using System.Threading; using System.Threading.Tasks; +using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Engine.Database; -public interface IGameDatabaseService +public interface IGameDatabaseService : IXmlParserErrorProvider, IDisposable { Task CreateDatabaseAsync( GameEngineType targetEngineType, diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs index 5da090e..f5aff84 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/GameDatabaseCreationPipeline.cs @@ -9,25 +9,24 @@ using Microsoft.Extensions.Logging; using PG.StarWarsGame.Engine.DataTypes; using PG.StarWarsGame.Engine.Repositories; +using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Engine.Database.Initialization; -internal class GameDatabaseCreationPipeline(GameRepository repository, IServiceProvider serviceProvider) +internal class GameDatabaseCreationPipeline(GameRepository repository, IXmlParserErrorListener xmlParserErrorListener, IServiceProvider serviceProvider) : Pipeline(serviceProvider) { private ParseSingletonXmlStep _parseGameConstants = null!; private ParseXmlDatabaseFromContainerStep _parseGameObjects = null!; private ParseXmlDatabaseFromContainerStep _parseSfxEvents = null!; - // We cannot use parallel processing here, in order to avoid races of the error event private StepRunner _parseXmlRunner = null!; public GameDatabase GameDatabase { get; private set; } = null!; - protected override Task PrepareCoreAsync() { - _parseXmlRunner = new StepRunner(ServiceProvider); + _parseXmlRunner = new ParallelRunner(4, ServiceProvider); foreach (var xmlParserStep in CreateXmlParserSteps()) _parseXmlRunner.AddStep(xmlParserStep); @@ -39,13 +38,13 @@ private IEnumerable CreateXmlParserSteps() // TODO: Use same load order as the engine! yield return _parseGameConstants = new ParseSingletonXmlStep("GameConstants", - "DATA\\XML\\GAMECONSTANTS.XML", repository, ServiceProvider); + "DATA\\XML\\GAMECONSTANTS.XML", repository, xmlParserErrorListener, ServiceProvider); yield return _parseGameObjects = new ParseXmlDatabaseFromContainerStep("GameObjects", - "DATA\\XML\\GAMEOBJECTFILES.XML", repository, ServiceProvider); + "DATA\\XML\\GAMEOBJECTFILES.XML", repository, xmlParserErrorListener, ServiceProvider); yield return _parseSfxEvents = new ParseXmlDatabaseFromContainerStep("SFXEvents", - "DATA\\XML\\SFXEventFiles.XML", repository, ServiceProvider); + "DATA\\XML\\SFXEventFiles.XML", repository, xmlParserErrorListener, ServiceProvider); // GUIDialogs.xml // LensFlares.xml @@ -130,8 +129,8 @@ protected override async Task RunCoreAsync(CancellationToken token) } } - private sealed class ParseSingletonXmlStep(string name, string xmlFile, IGameRepository repository, IServiceProvider serviceProvider) - : ParseXmlDatabaseStep(xmlFile, repository, serviceProvider) where T : class + private sealed class ParseSingletonXmlStep(string name, string xmlFile, IGameRepository repository, IXmlParserErrorListener? listener, IServiceProvider serviceProvider) + : ParseXmlDatabaseStep(xmlFile, repository, listener, serviceProvider) where T : class { protected override string Name => name; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/ParseXmlDatabaseFromContainerStep.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/ParseXmlDatabaseFromContainerStep.cs index a29c226..5dcde76 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/ParseXmlDatabaseFromContainerStep.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/ParseXmlDatabaseFromContainerStep.cs @@ -1,6 +1,7 @@ using System; using System.IO.Abstractions; using System.Linq; +using System.Xml; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using PG.Commons.Hashing; @@ -8,6 +9,7 @@ using PG.StarWarsGame.Engine.Repositories; using PG.StarWarsGame.Engine.Xml; using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Engine.Database.Initialization; @@ -15,6 +17,7 @@ internal class ParseXmlDatabaseFromContainerStep( string name, string xmlFile, IGameRepository repository, + IXmlParserErrorListener? listener, IServiceProvider serviceProvider) : CreateDatabaseStep>(repository, serviceProvider) where T : XmlObject @@ -28,7 +31,7 @@ internal class ParseXmlDatabaseFromContainerStep( protected sealed override IXmlDatabase CreateDatabase() { using var containerStream = GameRepository.OpenFile(xmlFile); - var containerParser = FileParserFactory.GetFileParser(); + var containerParser = FileParserFactory.GetFileParser(listener); Logger?.LogDebug($"Parsing container data '{xmlFile}'"); var container = containerParser.ParseFile(containerStream); @@ -38,11 +41,27 @@ protected sealed override IXmlDatabase CreateDatabase() foreach (var file in xmlFiles) { - using var fileStream = GameRepository.OpenFile(file); + using var fileStream = GameRepository.TryOpenFile(file); + + var parser = FileParserFactory.GetFileParser(listener); + + if (fileStream is null) + { + listener?.OnXmlParseError(parser, XmlParseErrorEventArgs.FromMissingFile(file)); + Logger?.LogWarning($"Could not find XML file '{file}'"); + continue; + } - var parser = FileParserFactory.GetFileParser(); Logger?.LogDebug($"Parsing File '{file}'"); - parser.ParseFile(fileStream, parsedEntries); + + try + { + parser.ParseFile(fileStream, parsedEntries); + } + catch (XmlException e) + { + listener?.OnXmlParseError(parser, new XmlParseErrorEventArgs(file, null, XmlParseErrorKind.Unknown, e.Message)); + } } return new XmlDatabase(parsedEntries, Services); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/ParseXmlDatabaseStep.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/ParseXmlDatabaseStep.cs index 4bed127..8050f02 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/ParseXmlDatabaseStep.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Database/Initialization/ParseXmlDatabaseStep.cs @@ -4,19 +4,22 @@ using Microsoft.Extensions.Logging; using PG.StarWarsGame.Engine.Repositories; using PG.StarWarsGame.Engine.Xml; +using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Engine.Database.Initialization; internal abstract class ParseXmlDatabaseStep( IList xmlFiles, IGameRepository repository, + IXmlParserErrorListener? listener, IServiceProvider serviceProvider) : CreateDatabaseStep(repository, serviceProvider) where T : class { protected readonly IPetroglyphXmlFileParserFactory FileParserFactory = serviceProvider.GetRequiredService(); - protected ParseXmlDatabaseStep(string xmlFile, IGameRepository repository, IServiceProvider serviceProvider) : this([xmlFile], repository, serviceProvider) + protected ParseXmlDatabaseStep(string xmlFile, IGameRepository repository, IXmlParserErrorListener? listener, IServiceProvider serviceProvider) + : this([xmlFile], repository, listener, serviceProvider) { } @@ -27,7 +30,7 @@ protected sealed override T CreateDatabase() { using var fileStream = GameRepository.OpenFile(xmlFile); - var parser = FileParserFactory.GetFileParser(); + var parser = FileParserFactory.GetFileParser(listener); Logger?.LogDebug($"Parsing File '{xmlFile}'"); var parsedData = parser.ParseFile(fileStream)!; parsedDatabaseEntries.Add(parsedData); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs index def842e..ae4269b 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PetroglyphEngineServiceContribution.cs @@ -13,6 +13,7 @@ public static void ContributeServices(IServiceCollection serviceCollection) serviceCollection.AddSingleton(sp => new GameRepositoryFactory(sp)); serviceCollection.AddSingleton(sp => new GameLanguageManagerProvider(sp)); serviceCollection.AddSingleton(sp => new PetroglyphXmlFileParserFactory(sp)); - serviceCollection.AddSingleton(sp => new GameDatabaseService(sp)); + + serviceCollection.AddTransient(sp => new GameDatabaseService(sp)); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/EffectsRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/EffectsRepository.cs index 42e73ce..4421c88 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/EffectsRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/EffectsRepository.cs @@ -13,6 +13,7 @@ public class EffectsRepository(IGameRepository baseRepository, IServiceProvider "Data\\Art\\Shaders\\Engine", ]; + // The engine does not support ".fxh" as a shader lookup, but as there might be som pre-compiling going on, this should be OK. private static readonly string[] ShaderExtensions = [".fx", ".fxo", ".fxh"]; [return:MaybeNull] diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/FocGameRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/FocGameRepository.cs index 3b269f0..a36382c 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/FocGameRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/FocGameRepository.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using PG.StarWarsGame.Files.MEG.Files; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.Parsers; namespace PG.StarWarsGame.Engine.Repositories; @@ -10,7 +12,7 @@ internal class FocGameRepository : GameRepository { public override GameEngineType EngineType => GameEngineType.Foc; - public FocGameRepository(GameLocations gameLocations, IServiceProvider serviceProvider) : base(gameLocations, serviceProvider) + public FocGameRepository(GameLocations gameLocations, IXmlParserErrorListener? listener, IServiceProvider serviceProvider) : base(gameLocations, listener, serviceProvider) { if (gameLocations == null) throw new ArgumentNullException(nameof(gameLocations)); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs index 84f931b..48c99f0 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepository.cs @@ -20,6 +20,7 @@ using PG.StarWarsGame.Files.MEG.Services; using PG.StarWarsGame.Files.MEG.Services.Builder.Normalization; using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Engine.Repositories; @@ -31,6 +32,7 @@ internal abstract class GameRepository : ServiceBase, IGameRepository private readonly ICrc32HashingService _crc32HashingService; private readonly IVirtualMegArchiveBuilder _virtualMegBuilder; private readonly IGameLanguageManagerProvider _languageManagerProvider; + private readonly IXmlParserErrorListener? _xmlParserErrorListener; protected readonly string GameDirectory; @@ -39,6 +41,7 @@ internal abstract class GameRepository : ServiceBase, IGameRepository private bool _sealed; + public string Path { get; } public abstract GameEngineType EngineType { get; } public IRepository EffectsRepository { get; } @@ -48,7 +51,7 @@ internal abstract class GameRepository : ServiceBase, IGameRepository private readonly List _loadedMegFiles = new(); protected IVirtualMegArchive? MasterMegArchive { get; private set; } - protected GameRepository(GameLocations gameLocations, IServiceProvider serviceProvider) : base(serviceProvider) + protected GameRepository(GameLocations gameLocations, IXmlParserErrorListener? errorListener, IServiceProvider serviceProvider) : base(serviceProvider) { if (gameLocations == null) throw new ArgumentNullException(nameof(gameLocations)); @@ -59,6 +62,7 @@ protected GameRepository(GameLocations gameLocations, IServiceProvider servicePr _crc32HashingService = serviceProvider.GetRequiredService(); _megPathNormalizer = new PetroglyphDataEntryPathNormalizer(serviceProvider); _languageManagerProvider = serviceProvider.GetRequiredService(); + _xmlParserErrorListener = errorListener; foreach (var mod in gameLocations.ModPaths) { @@ -83,6 +87,13 @@ protected GameRepository(GameLocations gameLocations, IServiceProvider servicePr EffectsRepository = new EffectsRepository(this, serviceProvider); TextureRepository = new TextureRepository(this, serviceProvider); + + + var path = ModPaths.Any() ? ModPaths.First() : GameDirectory; + if (!FileSystem.Path.HasTrailingDirectorySeparator(path)) + path += FileSystem.Path.DirectorySeparatorChar; + + Path = path; } @@ -231,15 +242,29 @@ public IEnumerable InitializeInstalledSfxMegFiles() { ThrowIfSealed(); + var megsToAdd = new List(); + var firstFallback = FallbackPaths.FirstOrDefault(); if (firstFallback is not null) { - AddMegFile(FileSystem.Path.Combine(firstFallback, "DATA\\AUDIO\\SFX\\SFX2D_NON_LOCALIZED.MEG")); - AddMegFile(FileSystem.Path.Combine(firstFallback, "DATA\\AUDIO\\SFX\\SFX3D_NON_LOCALIZED.MEG")); + var fallback2dNonLocalized = LoadMegArchive(FileSystem.Path.Combine(firstFallback, "DATA\\AUDIO\\SFX\\SFX2D_NON_LOCALIZED.MEG")); + var fallback3dNonLocalized = LoadMegArchive(FileSystem.Path.Combine(firstFallback, "DATA\\AUDIO\\SFX\\SFX3D_NON_LOCALIZED.MEG")); + + if (fallback2dNonLocalized is not null) + megsToAdd.Add(fallback2dNonLocalized); + + if (fallback3dNonLocalized is not null) + megsToAdd.Add(fallback3dNonLocalized); } - AddMegFile("DATA\\AUDIO\\SFX\\SFX2D_NON_LOCALIZED.MEG"); - AddMegFile("DATA\\AUDIO\\SFX\\SFX3D_NON_LOCALIZED.MEG"); + var nonLocalized2d = LoadMegArchive("DATA\\AUDIO\\SFX\\SFX2D_NON_LOCALIZED.MEG"); + var nonLocalized3d = LoadMegArchive("DATA\\AUDIO\\SFX\\SFX3D_NON_LOCALIZED.MEG"); + + if (nonLocalized2d is not null) + megsToAdd.Add(nonLocalized2d); + + if (nonLocalized3d is not null) + megsToAdd.Add(nonLocalized3d); var languageManager = _languageManagerProvider.GetLanguageManager(EngineType); @@ -252,15 +277,23 @@ public IEnumerable InitializeInstalledSfxMegFiles() var languageFiles = new LanguageFiles(language); - if (firstFallback is not null) - AddMegFile(FileSystem.Path.Combine(firstFallback, languageFiles.Sfx2dMegFilePath)); + if (firstFallback is not null) + { + var fallback2dLang = LoadMegArchive(FileSystem.Path.Combine(firstFallback, languageFiles.Sfx2dMegFilePath)); + if (fallback2dLang is not null) + megsToAdd.Add(fallback2dLang); + } - AddMegFile(languageFiles.Sfx2dMegFilePath); + var lang2d = LoadMegArchive(languageFiles.Sfx2dMegFilePath); + if (lang2d is not null) + megsToAdd.Add(lang2d); } if (languages.Count == 0) Logger.LogWarning("Unable to initialize any language."); + AddMegFiles(megsToAdd); + return languages; } @@ -278,7 +311,7 @@ protected IList LoadMegArchivesFromXml(string lookupPath) return Array.Empty(); } - var parser = fileParserFactory.GetFileParser(); + var parser = fileParserFactory.GetFileParser(_xmlParserErrorListener); var megaFilesXml = parser.ParseFile(xmlStream); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepositoryFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepositoryFactory.cs index 8c3af4d..fe64368 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepositoryFactory.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/GameRepositoryFactory.cs @@ -1,13 +1,15 @@ using System; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.Parsers; namespace PG.StarWarsGame.Engine.Repositories; internal sealed class GameRepositoryFactory(IServiceProvider serviceProvider) : IGameRepositoryFactory { - public GameRepository Create(GameEngineType engineType, GameLocations gameLocations) + public GameRepository Create(GameEngineType engineType, GameLocations gameLocations, IXmlParserErrorListener? xmlParserErrorListener) { if (engineType == GameEngineType.Eaw) throw new NotImplementedException("Empire at War is currently not supported."); - return new FocGameRepository(gameLocations, serviceProvider); + return new FocGameRepository(gameLocations, xmlParserErrorListener, serviceProvider); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepository.cs index d197536..62e6619 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepository.cs @@ -5,6 +5,11 @@ namespace PG.StarWarsGame.Engine.Repositories; public interface IGameRepository : IRepository { + /// + /// Gets the full qualified path of this repository with a trailing directory separator + /// + public string Path { get; } + public GameEngineType EngineType { get; } IRepository EffectsRepository { get; } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepositoryFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepositoryFactory.cs index c1461b0..e7f25e2 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepositoryFactory.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Repositories/IGameRepositoryFactory.cs @@ -1,6 +1,9 @@ -namespace PG.StarWarsGame.Engine.Repositories; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.Parsers; + +namespace PG.StarWarsGame.Engine.Repositories; internal interface IGameRepositoryFactory { - GameRepository Create(GameEngineType engineType, GameLocations gameLocations); + GameRepository Create(GameEngineType engineType, GameLocations gameLocations, IXmlParserErrorListener? xmlParserErrorListener); } \ 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 e70c889..36cb12c 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/IPetroglyphXmlFileParserFactory.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/IPetroglyphXmlFileParserFactory.cs @@ -1,8 +1,9 @@ -using PG.StarWarsGame.Files.XML.Parsers; +using PG.StarWarsGame.Files.XML.ErrorHandling; +using PG.StarWarsGame.Files.XML.Parsers; namespace PG.StarWarsGame.Engine.Xml; public interface IPetroglyphXmlFileParserFactory { - IPetroglyphXmlFileParser GetFileParser(); + IPetroglyphXmlFileParser GetFileParser(IXmlParserErrorListener? listener = null); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameConstantsParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameConstantsParser.cs index 23c5917..212bab1 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameConstantsParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameConstantsParser.cs @@ -3,11 +3,13 @@ using PG.Commons.Hashing; using PG.StarWarsGame.Engine.DataTypes; using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.ErrorHandling; using PG.StarWarsGame.Files.XML.Parsers; namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; -internal class GameConstantsParser(IServiceProvider serviceProvider) : PetroglyphXmlFileParser(serviceProvider) +internal class GameConstantsParser(IServiceProvider serviceProvider, IXmlParserErrorListener? listener = null) : + PetroglyphXmlFileParser(serviceProvider, listener) { public override GameConstants Parse(XElement element) { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs index f4aaf36..831cc55 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/GameObjectParser.cs @@ -3,14 +3,16 @@ using PG.Commons.Hashing; using PG.StarWarsGame.Engine.DataTypes; using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.ErrorHandling; using PG.StarWarsGame.Files.XML.Parsers; namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; public sealed class GameObjectParser( IReadOnlyValueListDictionary parsedElements, - IServiceProvider serviceProvider) - : XmlObjectParser(parsedElements, serviceProvider) + IServiceProvider serviceProvider, + IXmlParserErrorListener? listener = null) + : XmlObjectParser(parsedElements, serviceProvider, listener) { protected override IPetroglyphXmlElementParser? GetParser(string tag) { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs index fd33158..ffb7e18 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs @@ -4,14 +4,16 @@ using PG.StarWarsGame.Engine.DataTypes; 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( IReadOnlyValueListDictionary parsedElements, - IServiceProvider serviceProvider) - : XmlObjectParser(parsedElements, serviceProvider) + IServiceProvider serviceProvider, + IXmlParserErrorListener? listener = null) + : XmlObjectParser(parsedElements, serviceProvider, listener) { protected override IPetroglyphXmlElementParser? GetParser(string tag) { @@ -84,7 +86,7 @@ protected override bool OnParsed(XElement element, string tag, object value, Val else { var location = XmlLocationInfo.FromElement(element); - OnParseError(new ParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.MissingReference, + OnParseError(new XmlParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.MissingReference, $"Cannot to find preset '{presetName}' for SFXEvent '{outerElementName ?? "NONE"}'")); } } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileFileParser.cs index 238a26c..4eaedc7 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/GameObjectFileFileParser.cs @@ -4,16 +4,19 @@ using PG.StarWarsGame.Engine.DataTypes; using PG.StarWarsGame.Engine.Xml.Parsers.Data; 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 GameObjectFileFileParser(IServiceProvider serviceProvider) - : PetroglyphXmlFileParser(serviceProvider) +internal class GameObjectFileFileParser(IServiceProvider serviceProvider, IXmlParserErrorListener? listener = null) + : PetroglyphXmlFileParser(serviceProvider, listener) { + private readonly IXmlParserErrorListener? _listener = listener; + protected override void Parse(XElement element, IValueListDictionary parsedElements) { - var parser = new GameObjectParser(parsedElements, ServiceProvider); + var parser = new GameObjectParser(parsedElements, ServiceProvider, _listener); foreach (var xElement in element.Elements()) { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs index d10440f..7edc637 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs @@ -5,54 +5,46 @@ using PG.StarWarsGame.Engine.DataTypes; using PG.StarWarsGame.Engine.Xml.Parsers.Data; 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 SfxEventFileParser(IServiceProvider serviceProvider) - : PetroglyphXmlFileParser(serviceProvider) +internal class SfxEventFileParser(IServiceProvider serviceProvider, IXmlParserErrorListener? listener = null) + : PetroglyphXmlFileParser(serviceProvider, listener) { + private readonly IXmlParserErrorListener? _listener = listener; + protected override void Parse(XElement element, IValueListDictionary parsedElements) { - var parser = new SfxEventParser(parsedElements, ServiceProvider); + var parser = new SfxEventParser(parsedElements, ServiceProvider, _listener); - try + if (!element.HasElements) { - parser.ParseError += OnInnerParseError; - if (!element.HasElements) - { - OnParseError(ParseErrorEventArgs.FromEmptyRoot(XmlLocationInfo.FromElement(element).XmlFile, element)); - return; - } + OnParseError(XmlParseErrorEventArgs.FromEmptyRoot(XmlLocationInfo.FromElement(element).XmlFile, element)); + return; + } - foreach (var xElement in element.Elements()) + foreach (var xElement in element.Elements()) + { + var sfxEvent = parser.Parse(xElement, out var nameCrc); + if (nameCrc == default) { - var sfxEvent = parser.Parse(xElement, out var nameCrc); - if (nameCrc == default) - { - var location = XmlLocationInfo.FromElement(xElement); - OnParseError(new ParseErrorEventArgs(location.XmlFile, xElement, XmlParseErrorKind.MissingAttribute, - $"SFXEvent has no name at location '{location}'")); - } - parsedElements.Add(nameCrc, sfxEvent); + var location = XmlLocationInfo.FromElement(xElement); + OnParseError(new XmlParseErrorEventArgs(location.XmlFile, xElement, XmlParseErrorKind.MissingAttribute, + $"SFXEvent has no name at location '{location}'")); } + + parsedElements.Add(nameCrc, sfxEvent); } - finally - { - parser.ParseError -= OnInnerParseError; - } + } - protected override void OnParseError(ParseErrorEventArgs e) + protected override void OnParseError(XmlParseErrorEventArgs e) { Logger?.LogWarning($"Error while parsing {e.File}: {e.Message}"); base.OnParseError(e); } - - private void OnInnerParseError(object sender, ParseErrorEventArgs e) - { - OnParseError(e); - } public override SfxEvent Parse(XElement element) { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs index b65b0a2..5c3142c 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/XmlObjectParser.cs @@ -4,12 +4,13 @@ using PG.Commons.Hashing; using PG.StarWarsGame.Engine.DataTypes; using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.ErrorHandling; using PG.StarWarsGame.Files.XML.Parsers; namespace PG.StarWarsGame.Engine.Xml.Parsers; -public abstract class XmlObjectParser(IReadOnlyValueListDictionary parsedElements, IServiceProvider serviceProvider) - : PetroglyphXmlElementParser(serviceProvider) where T : XmlObject +public abstract class XmlObjectParser(IReadOnlyValueListDictionary parsedElements, IServiceProvider serviceProvider, IXmlParserErrorListener? listener = null) + : PetroglyphXmlElementParser(serviceProvider, listener) where T : XmlObject { protected IReadOnlyValueListDictionary ParsedElements { get; } = parsedElements ?? throw new ArgumentNullException(nameof(parsedElements)); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs index 0341eb6..7a283e5 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/PetroglyphXmlParserFactory.cs @@ -3,6 +3,7 @@ using PG.StarWarsGame.Engine.Xml.Parsers.Data; using PG.StarWarsGame.Engine.Xml.Parsers.File; using PG.StarWarsGame.Files.XML; +using PG.StarWarsGame.Files.XML.ErrorHandling; using PG.StarWarsGame.Files.XML.Parsers; using PG.StarWarsGame.Files.XML.Parsers.Primitives; @@ -10,24 +11,24 @@ namespace PG.StarWarsGame.Engine.Xml; internal sealed class PetroglyphXmlFileParserFactory(IServiceProvider serviceProvider) : IPetroglyphXmlFileParserFactory { - public IPetroglyphXmlFileParser GetFileParser() + public IPetroglyphXmlFileParser GetFileParser(IXmlParserErrorListener? listener = null) { - return (IPetroglyphXmlFileParser)GetFileParser(typeof(T)); + return (IPetroglyphXmlFileParser)GetFileParser(typeof(T), listener); } - private IPetroglyphXmlFileParser GetFileParser(Type type) + private IPetroglyphXmlFileParser GetFileParser(Type type, IXmlParserErrorListener? listener) { if (type == typeof(XmlFileContainer)) - return new XmlFileContainerParser(serviceProvider); + return new XmlFileContainerParser(serviceProvider, listener); if (type == typeof(GameConstants)) - return new GameConstantsParser(serviceProvider); + return new GameConstantsParser(serviceProvider, listener); if (type == typeof(GameObject)) - return new GameObjectFileFileParser(serviceProvider); + return new GameObjectFileFileParser(serviceProvider, listener); if (type == typeof(SfxEvent)) - return new SfxEventFileParser(serviceProvider); + return new SfxEventFileParser(serviceProvider, listener); throw new ParserNotFoundException(type); } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IPrimitiveXmlErrorParserProvider.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IPrimitiveXmlErrorParserProvider.cs new file mode 100644 index 0000000..3ed446e --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IPrimitiveXmlErrorParserProvider.cs @@ -0,0 +1,3 @@ +namespace PG.StarWarsGame.Files.XML.ErrorHandling; + +public interface IPrimitiveXmlErrorParserProvider : IXmlParserErrorProvider; \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IPrimitiveXmlParserErrorListener.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IPrimitiveXmlParserErrorListener.cs new file mode 100644 index 0000000..5157546 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IPrimitiveXmlParserErrorListener.cs @@ -0,0 +1,3 @@ +namespace PG.StarWarsGame.Files.XML.ErrorHandling; + +internal interface IPrimitiveXmlParserErrorListener : IXmlParserErrorListener, IPrimitiveXmlErrorParserProvider; \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IXmlParserErrorListener.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IXmlParserErrorListener.cs new file mode 100644 index 0000000..59b395d --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IXmlParserErrorListener.cs @@ -0,0 +1,8 @@ +using PG.StarWarsGame.Files.XML.Parsers; + +namespace PG.StarWarsGame.Files.XML.ErrorHandling; + +public interface IXmlParserErrorListener +{ + public void OnXmlParseError(IPetroglyphXmlParser parser, XmlParseErrorEventArgs error); +} \ 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 new file mode 100644 index 0000000..2470187 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/IXmlParserErrorProvider.cs @@ -0,0 +1,6 @@ +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/PrimitiveXmlParserErrorBroker.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/PrimitiveXmlParserErrorBroker.cs new file mode 100644 index 0000000..c66ba6b --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/PrimitiveXmlParserErrorBroker.cs @@ -0,0 +1,13 @@ +using PG.StarWarsGame.Files.XML.Parsers; + +namespace PG.StarWarsGame.Files.XML.ErrorHandling; + +internal class PrimitiveXmlParserErrorBroker : IPrimitiveXmlParserErrorListener +{ + public event XmlErrorEventHandler? XmlParseError; + + public void OnXmlParseError(IPetroglyphXmlParser parser, XmlParseErrorEventArgs error) + { + XmlParseError?.Invoke(parser, error); + } +} \ 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 new file mode 100644 index 0000000..69962e3 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlErrorEventHandler.cs @@ -0,0 +1,5 @@ +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/XmlParseErrorEventArgs.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlParseErrorEventArgs.cs new file mode 100644 index 0000000..1ea7d70 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlParseErrorEventArgs.cs @@ -0,0 +1,37 @@ +using System; +using System.Xml.Linq; +using AnakinRaW.CommonUtilities; + +namespace PG.StarWarsGame.Files.XML.ErrorHandling; + +public class XmlParseErrorEventArgs : EventArgs +{ + public string File { get; } + + public XElement? Element { get; } + + public XmlParseErrorKind ErrorKind { get; } + + public string Message { get; } + + public XmlParseErrorEventArgs(string file, XElement? element, XmlParseErrorKind errorKind, string message) + { + ThrowHelper.ThrowIfNullOrEmpty(file); + File = file; + Element = element; + ErrorKind = errorKind; + Message = message; + } + + public static XmlParseErrorEventArgs FromMissingFile(string file) + { + ThrowHelper.ThrowIfNullOrEmpty(file); + return new XmlParseErrorEventArgs(file, null, XmlParseErrorKind.MissingFile, $"XML file '{file}' not found."); + } + + public static XmlParseErrorEventArgs FromEmptyRoot(string file, XElement element) + { + ThrowHelper.ThrowIfNullOrEmpty(file); + return new XmlParseErrorEventArgs(file, element, XmlParseErrorKind.EmptyRoot, $"XML file '{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 new file mode 100644 index 0000000..ea329d9 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ErrorHandling/XmlParseErrorKind.cs @@ -0,0 +1,42 @@ +namespace PG.StarWarsGame.Files.XML.ErrorHandling; + +public enum XmlParseErrorKind +{ + /// + /// The error not specified any further. + /// + Unknown = 0, + /// + /// The XML file could not be found. + /// + MissingFile = 1, + /// + /// The root node of an XML file is empty. + /// + EmptyRoot = 2, + /// + /// A tag's value is syntactically correct, but the semantics of value are not valid. For example, + /// when the input is '-1' but an uint type is expected. + /// + InvalidValue = 3, + /// + /// A tag's value is has an invalid syntax. + /// + MalformedValue = 4, + /// + /// The value is too long + /// + TooLongData = 5, + /// + /// The data is missing an XML attribute. Usually this is the Name attribute. + /// + MissingAttribute = 6, + /// + /// The data points to a non-existing reference. + /// + MissingReference = 7, + /// + /// The XML file does not start with the XML header. + /// + DataBeforeHeader = 8, +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/PG.StarWarsGame.Files.XML.csproj.DotSettings b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/PG.StarWarsGame.Files.XML.csproj.DotSettings new file mode 100644 index 0000000..2dedfd6 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/PG.StarWarsGame.Files.XML.csproj.DotSettings @@ -0,0 +1,6 @@ + + False + True + True + True + True \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ParseErrorEventArgs.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ParseErrorEventArgs.cs deleted file mode 100644 index be59de3..0000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/ParseErrorEventArgs.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Xml.Linq; -using AnakinRaW.CommonUtilities; - -namespace PG.StarWarsGame.Files.XML; - -public class ParseErrorEventArgs : EventArgs -{ - public string File { get; } - - public XElement? Element { get; } - - public XmlParseErrorKind ErrorKind { get; } - - public string Message { get; } - - public ParseErrorEventArgs(string file, XElement? element, XmlParseErrorKind errorKind, string message) - { - ThrowHelper.ThrowIfNullOrEmpty(file); - File = file; - Element = element; - ErrorKind = errorKind; - Message = message; - } - - public static ParseErrorEventArgs FromMissingFile(string file) - { - ThrowHelper.ThrowIfNullOrEmpty(file); - return new ParseErrorEventArgs(file, null, XmlParseErrorKind.MissingFile, $"XML file '{file}' not found."); - } - - public static ParseErrorEventArgs FromEmptyRoot(string file, XElement element) - { - ThrowHelper.ThrowIfNullOrEmpty(file); - return new ParseErrorEventArgs(file, element, XmlParseErrorKind.EmptyRoot, $"XML file '{file}' has an empty root node."); - } -} - -public enum XmlParseErrorKind -{ - /// - /// The error not specified any further. - /// - Unknown, - /// - /// The XML file could not be found. - /// - MissingFile, - /// - /// The root node of an XML file is empty. - /// - EmptyRoot, - /// - /// A tag's value is syntactically correct, but the semantics of value are not valid. For example, - /// when the input is '-1' but an uint type is expected. - /// - InvalidValue, - /// - /// A tag's value is has an invalid syntax. - /// - MalformedValue, - /// - /// The value is too long - /// - TooLongData, - /// - /// The data is missing an XML attribute. Usually this is the Name attribute. - /// - MissingAttribute, - /// - /// The data points to a non-existing reference. - /// - MissingReference, -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlParser.cs index dffbd69..220fa05 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/IPetroglyphXmlParser.cs @@ -1,12 +1,9 @@ -using System; -using System.Xml.Linq; +using System.Xml.Linq; namespace PG.StarWarsGame.Files.XML.Parsers; public interface IPetroglyphXmlParser { - event EventHandler ParseError; - object Parse(XElement element); } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlElementParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlElementParser.cs index 013db76..53ce6cb 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlElementParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlElementParser.cs @@ -1,11 +1,12 @@ using System; using System.Linq; using System.Xml.Linq; +using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Files.XML.Parsers; -public abstract class PetroglyphXmlElementParser(IServiceProvider serviceProvider) - : PetroglyphXmlParser(serviceProvider) +public abstract class PetroglyphXmlElementParser(IServiceProvider serviceProvider, IXmlParserErrorListener? listener = null) + : PetroglyphXmlParser(serviceProvider, listener) { protected string GetTagName(XElement element) { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs index 3798aef..f9b64f9 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlFileParser.cs @@ -1,14 +1,23 @@ using System; using System.IO; +using System.IO.Abstractions; +using System.Text; using System.Xml; using System.Xml.Linq; +using AnakinRaW.CommonUtilities.FileSystem; +using Microsoft.Extensions.DependencyInjection; using PG.Commons.Hashing; using PG.Commons.Utilities; +using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Files.XML.Parsers; -public abstract class PetroglyphXmlFileParser(IServiceProvider serviceProvider) : PetroglyphXmlParser(serviceProvider), IPetroglyphXmlFileParser +public abstract class PetroglyphXmlFileParser(IServiceProvider serviceProvider, IXmlParserErrorListener? listener = null) : + PetroglyphXmlParser(serviceProvider, listener), IPetroglyphXmlFileParser { + private readonly IXmlParserErrorListener? _listener = listener; + private readonly IFileSystem _fileSystem = serviceProvider.GetRequiredService(); + protected virtual bool LoadLineInfo => true; public T ParseFile(Stream xmlStream) @@ -28,11 +37,19 @@ public void ParseFile(Stream xmlStream, IValueListDictionary parsedEnt private XElement? GetRootElement(Stream xmlStream) { - var fileName = xmlStream.GetFilePath(); + var 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."); - var xmlReader = XmlReader.Create(xmlStream, new XmlReaderSettings(), fileName); + SkipLeadingWhiteSpace(fileName, xmlStream); + + var xmlReader = XmlReader.Create(xmlStream, new XmlReaderSettings + { + IgnoreWhitespace = true, + IgnoreComments = true, + IgnoreProcessingInstructions = true + }, fileName); var options = LoadOptions.SetBaseUri; if (LoadLineInfo) @@ -42,6 +59,41 @@ public void ParseFile(Stream xmlStream, IValueListDictionary parsedEnt return doc.Root; } + private string GetStrippedFileName(string filePath) + { + if (!_fileSystem.Path.IsPathFullyQualified(filePath)) + return filePath; + + var pathPartIndex = filePath.LastIndexOf("DATA\\XML\\", StringComparison.OrdinalIgnoreCase); + + if (pathPartIndex == -1) + return filePath; + + return filePath.Substring(pathPartIndex); + } + + + private void SkipLeadingWhiteSpace(string fileName, Stream stream) + { + using var r = new StreamReader(stream, Encoding.ASCII, false, 10, true); + var count = 0; + + while (true) + { + var c = (char)r.Read(); + if (!char.IsWhiteSpace(c)) + break; + count++; + } + + if (count != 0) + _listener?.OnXmlParseError(this, new XmlParseErrorEventArgs(fileName, null, + XmlParseErrorKind.DataBeforeHeader, $"XML header is not the first entry of the file '{fileName}'")); + + stream.Position = count; + } + + object? IPetroglyphXmlFileParser.ParseFile(Stream stream) { return ParseFile(stream); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlParser.cs index a3a536c..21b4b43 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlParser.cs @@ -2,13 +2,14 @@ using System; using System.Xml.Linq; using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Files.XML.ErrorHandling; using PG.StarWarsGame.Files.XML.Parsers.Primitives; namespace PG.StarWarsGame.Files.XML.Parsers; public abstract class PetroglyphXmlParser : IPetroglyphXmlParser { - public event EventHandler? ParseError; + private readonly IXmlParserErrorListener? _errorListener; protected IServiceProvider ServiceProvider { get; } @@ -16,18 +17,19 @@ public abstract class PetroglyphXmlParser : IPetroglyphXmlParser protected IPrimitiveParserProvider PrimitiveParserProvider { get; } - protected PetroglyphXmlParser(IServiceProvider serviceProvider) + protected PetroglyphXmlParser(IServiceProvider serviceProvider, IXmlParserErrorListener? errorListener = null) { ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); Logger = serviceProvider.GetService()?.CreateLogger(GetType()); PrimitiveParserProvider = serviceProvider.GetRequiredService(); + _errorListener = errorListener; } public abstract T Parse(XElement element); - protected virtual void OnParseError(ParseErrorEventArgs e) + protected virtual void OnParseError(XmlParseErrorEventArgs e) { - ParseError?.Invoke(this, e); + _errorListener?.OnXmlParseError(this, e); } object IPetroglyphXmlParser.Parse(XElement element) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlPrimitiveElementParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlPrimitiveElementParser.cs index 7a155c5..cd97dd8 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlPrimitiveElementParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/PetroglyphXmlPrimitiveElementParser.cs @@ -1,10 +1,12 @@ using System; +using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Files.XML.Parsers; public abstract class PetroglyphXmlPrimitiveElementParser : PetroglyphXmlParser, IPetroglyphXmlElementParser { - private protected PetroglyphXmlPrimitiveElementParser(IServiceProvider serviceProvider) : base(serviceProvider) + private protected PetroglyphXmlPrimitiveElementParser(IServiceProvider serviceProvider, IPrimitiveXmlParserErrorListener listener) : + base(serviceProvider, listener) { } } \ 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 f5ba6b0..a0418db 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/CommaSeparatedStringKeyValueListParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/CommaSeparatedStringKeyValueListParser.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Xml.Linq; +using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; @@ -10,7 +11,7 @@ namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; // TODO: This class is not yet implemented, compliant to the engine public sealed class CommaSeparatedStringKeyValueListParser : PetroglyphXmlPrimitiveElementParser> { - internal CommaSeparatedStringKeyValueListParser(IServiceProvider serviceProvider) : base(serviceProvider) + internal CommaSeparatedStringKeyValueListParser(IServiceProvider serviceProvider, IPrimitiveXmlParserErrorListener listener) : base(serviceProvider, listener) { } 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 d3d28a0..f0124a8 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlBooleanParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlBooleanParser.cs @@ -1,11 +1,12 @@ using System; using System.Xml.Linq; +using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; public sealed class PetroglyphXmlBooleanParser : PetroglyphXmlPrimitiveElementParser { - internal PetroglyphXmlBooleanParser(IServiceProvider serviceProvider) : base(serviceProvider) + internal PetroglyphXmlBooleanParser(IServiceProvider serviceProvider, IPrimitiveXmlParserErrorListener listener) : base(serviceProvider, listener) { } 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 a6f5d7b..68d8ec7 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlByteParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlByteParser.cs @@ -1,12 +1,13 @@ using System; using System.Xml.Linq; using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; public sealed class PetroglyphXmlByteParser : PetroglyphXmlPrimitiveElementParser { - internal PetroglyphXmlByteParser(IServiceProvider serviceProvider) : base(serviceProvider) + internal PetroglyphXmlByteParser(IServiceProvider serviceProvider, IPrimitiveXmlParserErrorListener listener) : base(serviceProvider, listener) { } @@ -18,14 +19,14 @@ public override byte Parse(XElement element) if (intValue != asByte) { var location = XmlLocationInfo.FromElement(element); - OnParseError(new ParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.InvalidValue, + OnParseError(new XmlParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.InvalidValue, $"Expected a byte value (0 - 255) but got value '{intValue}' at {location}")); } return asByte; } - protected override void OnParseError(ParseErrorEventArgs e) + protected override void OnParseError(XmlParseErrorEventArgs e) { Logger?.LogWarning(e.Message); base.OnParseError(e); 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 8e201d6..6691061 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlFloatParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlFloatParser.cs @@ -2,12 +2,13 @@ using System.Globalization; using System.Xml.Linq; using Microsoft.Extensions.Logging; +using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; public sealed class PetroglyphXmlFloatParser : PetroglyphXmlPrimitiveElementParser { - internal PetroglyphXmlFloatParser(IServiceProvider serviceProvider) : base(serviceProvider) + internal PetroglyphXmlFloatParser(IServiceProvider serviceProvider, IPrimitiveXmlParserErrorListener listener) : base(serviceProvider, listener) { } @@ -17,14 +18,14 @@ public override float Parse(XElement element) if (!double.TryParse(element.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var doubleValue)) { var location = XmlLocationInfo.FromElement(element); - OnParseError(new ParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.MalformedValue, + OnParseError(new XmlParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.MalformedValue, $"Expected double but got value '{element.Value}' at {location}")); return 0.0f; } return (float)doubleValue; } - protected override void OnParseError(ParseErrorEventArgs e) + protected override void OnParseError(XmlParseErrorEventArgs e) { Logger?.LogWarning(e.Message); base.OnParseError(e); 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 383c2ce..02400b8 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlIntegerParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlIntegerParser.cs @@ -1,12 +1,13 @@ using Microsoft.Extensions.Logging; using System; using System.Xml.Linq; +using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; public sealed class PetroglyphXmlIntegerParser : PetroglyphXmlPrimitiveElementParser { - internal PetroglyphXmlIntegerParser(IServiceProvider serviceProvider) : base(serviceProvider) + internal PetroglyphXmlIntegerParser(IServiceProvider serviceProvider, IPrimitiveXmlParserErrorListener listener) : base(serviceProvider, listener) { } @@ -19,7 +20,7 @@ public override int Parse(XElement element) if (!int.TryParse(element.Value, out var i)) { var location = XmlLocationInfo.FromElement(element); - OnParseError(new ParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.MalformedValue, + OnParseError(new XmlParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.MalformedValue, $"Expected integer but got '{element.Value}' at {location}")); return 0; } @@ -27,7 +28,7 @@ public override int Parse(XElement element) return i; } - protected override void OnParseError(ParseErrorEventArgs e) + protected override void OnParseError(XmlParseErrorEventArgs e) { Logger?.LogWarning(e.Message); base.OnParseError(e); 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 4cbdfed..82fbb38 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlLooseStringListParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlLooseStringListParser.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Xml.Linq; +using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; @@ -17,7 +18,7 @@ public sealed class PetroglyphXmlLooseStringListParser : PetroglyphXmlPrimitiveE '\r' ]; - internal PetroglyphXmlLooseStringListParser(IServiceProvider serviceProvider) : base(serviceProvider) + internal PetroglyphXmlLooseStringListParser(IServiceProvider serviceProvider, IPrimitiveXmlParserErrorListener listener) : base(serviceProvider, listener) { } @@ -31,7 +32,7 @@ public override IList Parse(XElement element) if (trimmedValued.Length > 0x2000) { var location = XmlLocationInfo.FromElement(element); - OnParseError(new ParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.TooLongData, + OnParseError(new XmlParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.TooLongData, $"Input value is too long '{trimmedValued.Length}' at {XmlLocationInfo.FromElement(element)}")); return Array.Empty(); @@ -42,7 +43,7 @@ public override IList Parse(XElement element) return entries; } - protected override void OnParseError(ParseErrorEventArgs e) + protected override void OnParseError(XmlParseErrorEventArgs e) { Logger?.LogWarning(e.Message); base.OnParseError(e); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlMax100ByteParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlMax100ByteParser.cs index 61aa4ec..af72d8b 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlMax100ByteParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlMax100ByteParser.cs @@ -1,12 +1,13 @@ using Microsoft.Extensions.Logging; using System; using System.Xml.Linq; +using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; public sealed class PetroglyphXmlMax100ByteParser : PetroglyphXmlPrimitiveElementParser { - internal PetroglyphXmlMax100ByteParser(IServiceProvider serviceProvider) : base(serviceProvider) + internal PetroglyphXmlMax100ByteParser(IServiceProvider serviceProvider, IPrimitiveXmlParserErrorListener listener) : base(serviceProvider, listener) { } @@ -23,7 +24,7 @@ public override byte Parse(XElement element) { var location = XmlLocationInfo.FromElement(element); - OnParseError(new ParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.InvalidValue, + OnParseError(new XmlParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.InvalidValue, $"Expected a byte value (0 - 255) but got value '{intValue}' at {location}")); } @@ -31,14 +32,14 @@ public override byte Parse(XElement element) if (asByte > 100) { var location = XmlLocationInfo.FromElement(element); - OnParseError(new ParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.InvalidValue, + OnParseError(new XmlParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.InvalidValue, $"Expected a byte value (0 - 100) but got value '{asByte}' at {location}")); } return asByte; } - protected override void OnParseError(ParseErrorEventArgs e) + protected override void OnParseError(XmlParseErrorEventArgs e) { Logger?.LogWarning(e.Message); base.OnParseError(e); 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 a4a7e98..bc8ca42 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlStringParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlStringParser.cs @@ -1,11 +1,12 @@ using System; using System.Xml.Linq; +using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; public sealed class PetroglyphXmlStringParser : PetroglyphXmlPrimitiveElementParser { - internal PetroglyphXmlStringParser(IServiceProvider serviceProvider) : base(serviceProvider) + internal PetroglyphXmlStringParser(IServiceProvider serviceProvider, IPrimitiveXmlParserErrorListener listener) : base(serviceProvider, listener) { } 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 689471d..2206f12 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlUnsignedIntegerParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlUnsignedIntegerParser.cs @@ -1,12 +1,13 @@ using Microsoft.Extensions.Logging; using System; using System.Xml.Linq; +using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; public sealed class PetroglyphXmlUnsignedIntegerParser : PetroglyphXmlPrimitiveElementParser { - internal PetroglyphXmlUnsignedIntegerParser(IServiceProvider serviceProvider) : base(serviceProvider) + internal PetroglyphXmlUnsignedIntegerParser(IServiceProvider serviceProvider, IPrimitiveXmlParserErrorListener listener) : base(serviceProvider, listener) { } @@ -19,14 +20,14 @@ public override uint Parse(XElement element) { var location = XmlLocationInfo.FromElement(element); - OnParseError(new ParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.InvalidValue, + OnParseError(new XmlParseErrorEventArgs(location.XmlFile, element, XmlParseErrorKind.InvalidValue, $"Expected unsigned integer but got '{intValue}' at {location}")); } return asUint; } - protected override void OnParseError(ParseErrorEventArgs e) + protected override void OnParseError(XmlParseErrorEventArgs e) { Logger?.LogWarning(e.Message); base.OnParseError(e); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PrimitiveParserProvider.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PrimitiveParserProvider.cs index 57f5f15..747c83a 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PrimitiveParserProvider.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PrimitiveParserProvider.cs @@ -1,26 +1,48 @@ using System; +using System.Threading; +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; internal class PrimitiveParserProvider(IServiceProvider serviceProvider) : IPrimitiveParserProvider { - private readonly Lazy _lazyStringParser = new(() => new PetroglyphXmlStringParser(serviceProvider)); - private readonly Lazy _lazyUintParser = new(() => new PetroglyphXmlUnsignedIntegerParser(serviceProvider)); - private readonly Lazy _lazyLooseStringListParser = new(() => new PetroglyphXmlLooseStringListParser(serviceProvider)); - private readonly Lazy _lazyIntParser = new(() => new PetroglyphXmlIntegerParser(serviceProvider)); - private readonly Lazy _lazyFloatParser = new(() => new PetroglyphXmlFloatParser(serviceProvider)); - private readonly Lazy _lazyByteParser = new(() => new PetroglyphXmlByteParser(serviceProvider)); - private readonly Lazy _lazyMax100ByteParser = new(() => new PetroglyphXmlMax100ByteParser(serviceProvider)); - private readonly Lazy _lazyBoolParser = new(() => new PetroglyphXmlBooleanParser(serviceProvider)); - private readonly Lazy _lazyCommaStringKeyListParser = new(() => new CommaSeparatedStringKeyValueListParser(serviceProvider)); + private readonly IPrimitiveXmlParserErrorListener _primitiveParserErrorListener = serviceProvider.GetRequiredService(); - public PetroglyphXmlStringParser StringParser => _lazyStringParser.Value; - public PetroglyphXmlUnsignedIntegerParser UIntParser => _lazyUintParser.Value; - public PetroglyphXmlLooseStringListParser LooseStringListParser => _lazyLooseStringListParser.Value; - public PetroglyphXmlIntegerParser IntParser => _lazyIntParser.Value; - public PetroglyphXmlFloatParser FloatParser => _lazyFloatParser.Value; - public PetroglyphXmlByteParser ByteParser => _lazyByteParser.Value; - public PetroglyphXmlMax100ByteParser Max100ByteParser => _lazyMax100ByteParser.Value; - public PetroglyphXmlBooleanParser BooleanParser => _lazyBoolParser.Value; - public CommaSeparatedStringKeyValueListParser CommaSeparatedStringKeyValueListParser => _lazyCommaStringKeyListParser.Value; + private PetroglyphXmlStringParser _stringParser = null!; + private PetroglyphXmlUnsignedIntegerParser _uintParser = null!; + private PetroglyphXmlLooseStringListParser _looseStringListParser = null!; + private PetroglyphXmlIntegerParser _intParser = null!; + private PetroglyphXmlFloatParser _floatParser = null!; + private PetroglyphXmlByteParser _byteParser = null!; + private PetroglyphXmlMax100ByteParser _max100ByteParser = null!; + private PetroglyphXmlBooleanParser _booleanParser = null!; + private CommaSeparatedStringKeyValueListParser _commaSeparatedStringKeyValueListParser = null!; + + public PetroglyphXmlStringParser StringParser => + LazyInitializer.EnsureInitialized(ref _stringParser, () => new PetroglyphXmlStringParser(serviceProvider, _primitiveParserErrorListener)); + + public PetroglyphXmlUnsignedIntegerParser UIntParser => + LazyInitializer.EnsureInitialized(ref _uintParser, () => new PetroglyphXmlUnsignedIntegerParser(serviceProvider, _primitiveParserErrorListener)); + + public PetroglyphXmlLooseStringListParser LooseStringListParser => + LazyInitializer.EnsureInitialized(ref _looseStringListParser, () => new PetroglyphXmlLooseStringListParser(serviceProvider, _primitiveParserErrorListener)); + + public PetroglyphXmlIntegerParser IntParser => + LazyInitializer.EnsureInitialized(ref _intParser, () => new PetroglyphXmlIntegerParser(serviceProvider, _primitiveParserErrorListener)); + + public PetroglyphXmlFloatParser FloatParser => + LazyInitializer.EnsureInitialized(ref _floatParser, () => new PetroglyphXmlFloatParser(serviceProvider, _primitiveParserErrorListener)); + + public PetroglyphXmlByteParser ByteParser => + LazyInitializer.EnsureInitialized(ref _byteParser, () => new PetroglyphXmlByteParser(serviceProvider, _primitiveParserErrorListener)); + + public PetroglyphXmlMax100ByteParser Max100ByteParser => + LazyInitializer.EnsureInitialized(ref _max100ByteParser, () => new PetroglyphXmlMax100ByteParser(serviceProvider, _primitiveParserErrorListener)); + + public PetroglyphXmlBooleanParser BooleanParser => + LazyInitializer.EnsureInitialized(ref _booleanParser, () => new PetroglyphXmlBooleanParser(serviceProvider, _primitiveParserErrorListener)); + + public CommaSeparatedStringKeyValueListParser CommaSeparatedStringKeyValueListParser => + LazyInitializer.EnsureInitialized(ref _commaSeparatedStringKeyValueListParser, () => new CommaSeparatedStringKeyValueListParser(serviceProvider, _primitiveParserErrorListener)); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/XmlFileContainerParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/XmlFileContainerParser.cs index 2c8424a..20aeace 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/XmlFileContainerParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/XmlFileContainerParser.cs @@ -2,10 +2,12 @@ using System.Collections.Generic; using System.Xml.Linq; using PG.Commons.Hashing; +using PG.StarWarsGame.Files.XML.ErrorHandling; namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; -public class XmlFileContainerParser(IServiceProvider serviceProvider) : PetroglyphXmlFileParser(serviceProvider) +public class XmlFileContainerParser(IServiceProvider serviceProvider, IXmlParserErrorListener? listener = null) : + PetroglyphXmlFileParser(serviceProvider, listener) { protected override bool LoadLineInfo => false; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XmlLocationInfo.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XmlLocationInfo.cs index 67d7e78..e92cc63 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XmlLocationInfo.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XmlLocationInfo.cs @@ -23,7 +23,7 @@ public static XmlLocationInfo FromElement(XElement element) public override string ToString() { if (string.IsNullOrEmpty(XmlFile)) - return "No File information"; - return Line is null ? XmlFile! : $"{XmlFile} at line: {Line}"; + return "(n/a)"; + return Line is null ? XmlFile : $"{XmlFile} at line: {Line}"; } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XmlServiceContribution.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XmlServiceContribution.cs index 9761d64..6fb18d4 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XmlServiceContribution.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/XmlServiceContribution.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for details. using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Files.XML.ErrorHandling; using PG.StarWarsGame.Files.XML.Parsers.Primitives; namespace PG.StarWarsGame.Files.XML; @@ -10,6 +11,8 @@ public static class XmlServiceContribution { public static void ContributeServices(IServiceCollection serviceCollection) { + serviceCollection.AddSingleton(_ => new PrimitiveXmlParserErrorBroker()); + serviceCollection.AddSingleton(sp => sp.GetRequiredService()); serviceCollection.AddSingleton(sp => new PrimitiveParserProvider(sp)); } } \ No newline at end of file