From 003874e2258bd85b301a4a584ddb5497f4ad3f4a Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Mon, 3 Jun 2024 21:53:57 +0200 Subject: [PATCH] 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()