diff --git a/.github/workflows/dotnet.yml b/.github/workflows/ci.yml similarity index 62% rename from .github/workflows/dotnet.yml rename to .github/workflows/ci.yml index 6781da000..c572ac26c 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/ci.yml @@ -3,16 +3,20 @@ name: CI on: push: branches: [ "master", "dev" ] - paths: [ ".github/workflows/dotnet.yml", "Src/**", "Benchmarks/**", "Generators/**", "Resources/**", "Samples/**", "Tests/**", "Tools/**", "GBX.NET.sln" ] + paths: [ ".github/workflows/ci.yml", "Src/**", "Benchmarks/**", "Generators/**", "Resources/**", "Samples/**", "Tests/**", "Tools/**", "GBX.NET.sln" ] pull_request: branches: [ "master", "dev" ] - paths: [ ".github/workflows/dotnet.yml", "Src/**", "Src/**", "Benchmarks/**", "Generators/**", "Resources/**", "Samples/**", "Tests/**", "Tools/**", "GBX.NET.sln" ] + paths: [ ".github/workflows/ci.yml", "Src/**", "Src/**", "Benchmarks/**", "Generators/**", "Resources/**", "Samples/**", "Tests/**", "Tools/**", "GBX.NET.sln" ] workflow_dispatch: jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v4 @@ -31,10 +35,12 @@ jobs: run: dotnet build -c Release --no-restore /p:ContinuousIntegrationBuild=true - name: Test + if: ${{ (matrix.os == 'ubuntu-latest') || (github.ref == 'refs/heads/master') }} run: dotnet test -c Release --no-build --verbosity normal --no-build --collect:"XPlat Code Coverage" --results-directory ./coverage - name: Code Coverage Report uses: irongut/CodeCoverageSummary@v1.3.0 + if: ${{ (github.ref == 'refs/heads/master') && (matrix.os == 'ubuntu-latest') }} with: filename: coverage/**/coverage.cobertura.xml badge: true diff --git a/.github/workflows/deploy-explorer-pages.yml b/.github/workflows/deploy-explorer-pages.yml index 70c681c4d..a891922fe 100644 --- a/.github/workflows/deploy-explorer-pages.yml +++ b/.github/workflows/deploy-explorer-pages.yml @@ -3,6 +3,7 @@ name: Deploy Explorer to Pages on: push: branches: ["dev"] + paths: [ ".github/workflows/deploy-explorer-pages.yml", "Src/**", "Benchmarks/**", "Generators/**", "Resources/**", "Samples/**", "Tests/**", "Tools/**", "GBX.NET.sln" ] workflow_dispatch: # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cf6f4e271..efb421877 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -92,6 +92,9 @@ jobs: - name: Publish ${{ matrix.lib }} nupkg to github.com run: dotnet nuget push ${{ matrix.lib }}/bin/Release/*.nupkg -k ${{ secrets.GITHUB_TOKEN }} -s https://nuget.pkg.github.com/bigbang1112/index.json --skip-duplicate + + - name: Publish ${{ matrix.lib }} nupkg to nuget.gbx.tools + run: dotnet nuget push ${{ matrix.lib }}/bin/Release/*.nupkg -k ${{ secrets.NUGET_GBXTOOLS_API_KEY }} -s https://nuget.gbx.tools/v3/index.json --skip-duplicate - name: Upload ${{ matrix.lib }} nupkg to this release run: gh release upload ${{ github.ref_name }} ${{ matrix.lib }}/bin/Release/*.nupkg diff --git a/GBX.NET.sln b/GBX.NET.sln index c585e343b..0612dc811 100644 --- a/GBX.NET.sln +++ b/GBX.NET.sln @@ -170,7 +170,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CryCryptSwitch", "Tools\Cry EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GBX.NET.BlockInfo", "Src\GBX.NET.BlockInfo\GBX.NET.BlockInfo.csproj", "{89F6FFE5-E32A-4713-8D63-98D070A17DC1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MuxCryptSwitch", "Tools\MuxCryptSwitch\MuxCryptSwitch.csproj", "{0B5609C6-7B10-4E8D-B41C-35CC1A2532A4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MuxCryptSwitch", "Tools\MuxCryptSwitch\MuxCryptSwitch.csproj", "{0B5609C6-7B10-4E8D-B41C-35CC1A2532A4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GBX.NET.Tool.StandardIO", "Src\GBX.NET.Tool.StandardIO\GBX.NET.Tool.StandardIO.csproj", "{55BD1330-7431-41A8-90EB-DEB20D9EB981}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -406,6 +408,10 @@ Global {0B5609C6-7B10-4E8D-B41C-35CC1A2532A4}.Debug|Any CPU.Build.0 = Debug|Any CPU {0B5609C6-7B10-4E8D-B41C-35CC1A2532A4}.Release|Any CPU.ActiveCfg = Release|Any CPU {0B5609C6-7B10-4E8D-B41C-35CC1A2532A4}.Release|Any CPU.Build.0 = Release|Any CPU + {55BD1330-7431-41A8-90EB-DEB20D9EB981}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {55BD1330-7431-41A8-90EB-DEB20D9EB981}.Debug|Any CPU.Build.0 = Debug|Any CPU + {55BD1330-7431-41A8-90EB-DEB20D9EB981}.Release|Any CPU.ActiveCfg = Release|Any CPU + {55BD1330-7431-41A8-90EB-DEB20D9EB981}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -474,6 +480,7 @@ Global {4327AB87-DF45-4364-9D27-1C72BA0E78FF} = {F3336145-FDA9-4517-AEDC-7F4C4D526ECB} {89F6FFE5-E32A-4713-8D63-98D070A17DC1} = {80DCE6B7-4BD9-415C-B053-92B059D7C938} {0B5609C6-7B10-4E8D-B41C-35CC1A2532A4} = {F3336145-FDA9-4517-AEDC-7F4C4D526ECB} + {55BD1330-7431-41A8-90EB-DEB20D9EB981} = {80DCE6B7-4BD9-415C-B053-92B059D7C938} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8EA2F0DE-BA72-486D-AB3A-9320C0CE5CFD} diff --git a/Src/GBX.NET.Tool.CLI/ComplexConfig.cs b/Src/GBX.NET.Tool.CLI/ComplexConfig.cs new file mode 100644 index 000000000..69178d9cc --- /dev/null +++ b/Src/GBX.NET.Tool.CLI/ComplexConfig.cs @@ -0,0 +1,57 @@ + +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; + +namespace GBX.NET.Tool.CLI; + +internal sealed class ComplexConfig : IComplexConfig +{ + private readonly string configName; + private readonly SettingsManager settings; + + private readonly ConcurrentDictionary cache = new(); + + public ComplexConfig(string configName, SettingsManager settings) + { + this.configName = configName; + this.settings = settings; + } + + [RequiresDynamicCode(DynamicCodeMessages.JsonSerializeMessage)] + [RequiresUnreferencedCode(DynamicCodeMessages.JsonSerializeMessage)] + public T Get(string filePathWithoutExtension, bool cache = false) where T : class + { + if (cache && this.cache.TryGetValue(filePathWithoutExtension, out var value)) + { + return (T)value; + } + + var result = settings.GetFileFromConfig(configName, filePathWithoutExtension); + + if (cache) + { + this.cache.TryAdd(filePathWithoutExtension, result); + } + + return result; + } + + [RequiresDynamicCode(DynamicCodeMessages.JsonSerializeMessage)] + [RequiresUnreferencedCode(DynamicCodeMessages.JsonSerializeMessage)] + public async Task GetAsync(string filePathWithoutExtension, bool cache = false, CancellationToken cancellationToken = default) where T : class + { + if (cache && this.cache.TryGetValue(filePathWithoutExtension, out var value)) + { + return (T)value; + } + + var result = await settings.GetFileFromConfigAsync(configName, filePathWithoutExtension, cancellationToken).ConfigureAwait(false); + + if (cache) + { + this.cache.TryAdd(filePathWithoutExtension, result); + } + + return result; + } +} diff --git a/Src/GBX.NET.Tool.CLI/GBX.NET.Tool.CLI.csproj b/Src/GBX.NET.Tool.CLI/GBX.NET.Tool.CLI.csproj index 50f018c43..c17dd1182 100644 --- a/Src/GBX.NET.Tool.CLI/GBX.NET.Tool.CLI.csproj +++ b/Src/GBX.NET.Tool.CLI/GBX.NET.Tool.CLI.csproj @@ -2,7 +2,7 @@ GBX.NET.Tool.CLI - 0.2.0 + 0.3.0 BigBang1112 CLI implementation for the GBX.NET tool framework using Spectre.Console. Copyright (c) 2024 Petr Pivoňka diff --git a/Src/GBX.NET.Tool.CLI/OutputDistributor.cs b/Src/GBX.NET.Tool.CLI/OutputDistributor.cs index 02ad381b6..24c20dddb 100644 --- a/Src/GBX.NET.Tool.CLI/OutputDistributor.cs +++ b/Src/GBX.NET.Tool.CLI/OutputDistributor.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using System.Diagnostics; namespace GBX.NET.Tool.CLI; @@ -28,20 +29,39 @@ public OutputDistributor(string runningDir, ToolSettings toolSettings, ILogger l } } - public async Task DistributeOutputsAsync(IEnumerable outputs, CancellationToken cancellationToken) + public async Task DistributeOutputsAsync(IEnumerable outputs, bool mutating, CancellationToken cancellationToken) { + var watch = default(Stopwatch); + + if (outputs is not System.Collections.ICollection) + { + watch = Stopwatch.StartNew(); + } + foreach (var output in outputs) { - await DistributeOutputAsync(output, cancellationToken); + if (watch is not null) + { + if (mutating) + { + logger.LogInformation("Mutated in {Milliseconds}ms!", watch.ElapsedMilliseconds); + } + else + { + logger.LogInformation("Produced in {Milliseconds}ms!", watch.ElapsedMilliseconds); + } + } + + await DistributeOutputAsync(output, mutating, cancellationToken); } } - public async ValueTask DistributeOutputAsync(object? output, CancellationToken cancellationToken) + public async ValueTask DistributeOutputAsync(object? output, bool mutating, CancellationToken cancellationToken) { switch (output) { case IEnumerable objList: - await DistributeOutputsAsync(objList, cancellationToken); + await DistributeOutputsAsync(objList, mutating, cancellationToken); break; case null: break; @@ -63,12 +83,14 @@ public async ValueTask DistributeOutputAsync(object? output, CancellationToken c Directory.CreateDirectory(dirPath); } + var watch = Stopwatch.StartNew(); + await using (var fs = new FileStream(finalPath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, useAsync: true)) { gbx.Save(fs); } - logger.LogInformation("Gbx ({FilePath}) saved.", filePath); + logger.LogInformation("Gbx ({FilePath}) saved in {Milliseconds}ms.", filePath, watch.ElapsedMilliseconds); break; default: diff --git a/Src/GBX.NET.Tool.CLI/SettingsManager.cs b/Src/GBX.NET.Tool.CLI/SettingsManager.cs index f8dfdea56..9a87f67e2 100644 --- a/Src/GBX.NET.Tool.CLI/SettingsManager.cs +++ b/Src/GBX.NET.Tool.CLI/SettingsManager.cs @@ -30,7 +30,7 @@ public SettingsManager( this.ymlSerializer = yamlSerializer; } - public async Task GetOrCreateFileAsync( + public async Task GetOrCreateJsonFileAsync( string fileName, JsonTypeInfo typeInfo, bool resetOnException = false, @@ -84,6 +84,66 @@ public async Task GetOrCreateFileAsync( return result; } + public T GetOrCreateYmlFile( + string fileName, + bool resetOnException = false, + ILogger? logger = null) where T : new() + { + T result; + + if (ymlDeserializer is null || ymlSerializer is null) + { + throw new InvalidOperationException("YAML deserializer or serializer is not available."); + } + + var filePath = Path.Combine(runningDir, fileName + ".yml"); + + if (File.Exists(filePath)) + { + logger?.LogDebug("File {FileName} exists. Deserializing...", fileName); + + try + { + using var fs = File.OpenRead(filePath); + using var reader = new StreamReader(fs); + + result = ymlDeserializer.Deserialize(reader); + } + catch (Exception ex) + { + if (!resetOnException) + { + throw; + } + + AnsiConsole.WriteException(ex); + + result = new(); + } + } + else + { + result = new(); + + logger?.LogDebug("File {FileName} does not exist.", fileName); + + var directory = Path.GetDirectoryName(filePath); + + if (!string.IsNullOrWhiteSpace(directory)) + { + Directory.CreateDirectory(directory); + } + } + + logger?.LogDebug("Creating and serializing {FileName}...", fileName); + + using var writer = File.CreateText(filePath); + + ymlSerializer.Serialize(writer, result); + + return result; + } + [RequiresDynamicCode(DynamicCodeMessages.JsonSerializeMessage)] [RequiresUnreferencedCode(DynamicCodeMessages.JsonSerializeMessage)] public async Task PopulateConfigAsync(string configName, Config config, CancellationToken cancellationToken) @@ -126,27 +186,17 @@ public async Task PopulateConfigAsync(string configName, Config config, Cancella if (Attribute.IsDefined(prop, typeof(ExternalFileAttribute))) { - var att = prop.GetCustomAttribute()!; - - var filePathWithoutExtension = Path.Combine(configDir, att.FileName); - - if (File.Exists(filePathWithoutExtension + ".json")) + if (!prop.CanWrite) { - await using var fs = new FileStream(filePathWithoutExtension + ".json", FileMode.Open, FileAccess.Read, FileShare.None, 4096, useAsync: true); - - var value = jsonContext is null - ? await JsonSerializer.DeserializeAsync(fs, prop.PropertyType, jsonOptions, cancellationToken) - : await JsonSerializer.DeserializeAsync(fs, prop.PropertyType, jsonContext, cancellationToken: cancellationToken); - - prop.SetValue(config, value); + continue; } - else if (ymlDeserializer is not null && File.Exists(filePathWithoutExtension + ".yml")) - { - await using var fs = new FileStream(filePathWithoutExtension + ".yml", FileMode.Open, FileAccess.Read, FileShare.None, 4096, useAsync: true); - using var reader = new StreamReader(fs); - var value = ymlDeserializer.Deserialize(reader, prop.PropertyType); + var att = prop.GetCustomAttribute()!; + + var value = await GetFileFromConfigAsync(configName, att.FileNameWithoutExtension, prop.PropertyType, cancellationToken); + if (value is not null) + { prop.SetValue(config, value); } } @@ -171,4 +221,76 @@ public async Task PopulateConfigAsync(string configName, Config config, Cancella ymlSerializer.Serialize(writer, config); } } + + [RequiresDynamicCode(DynamicCodeMessages.JsonSerializeMessage)] + [RequiresUnreferencedCode(DynamicCodeMessages.JsonSerializeMessage)] + public async Task GetFileFromConfigAsync(string configName, string relativeFilePathWithoutExtension, Type type, CancellationToken cancellationToken) + { + var configDir = Path.Combine(runningDir, "Config", configName); + var filePathWithoutExtension = Path.Combine(configDir, relativeFilePathWithoutExtension); + + if (ymlDeserializer is not null && File.Exists(filePathWithoutExtension + ".yml")) + { + await using var fs = new FileStream(filePathWithoutExtension + ".yml", FileMode.Open, FileAccess.Read, FileShare.None, 4096, useAsync: true); + using var reader = new StreamReader(fs); + + return ymlDeserializer.Deserialize(reader, type) ?? throw new InvalidOperationException("Deserialization failed."); + } + + if (File.Exists(filePathWithoutExtension + ".json")) + { + await using var fs = new FileStream(filePathWithoutExtension + ".json", FileMode.Open, FileAccess.Read, FileShare.None, 4096, useAsync: true); + + var obj = jsonContext is null + ? await JsonSerializer.DeserializeAsync(fs, type, jsonOptions, cancellationToken) + : await JsonSerializer.DeserializeAsync(fs, type, jsonContext, cancellationToken: cancellationToken); + + return obj ?? throw new InvalidOperationException("Deserialization failed."); + } + + throw new FileNotFoundException("File not found.", filePathWithoutExtension); + } + + [RequiresDynamicCode(DynamicCodeMessages.JsonSerializeMessage)] + [RequiresUnreferencedCode(DynamicCodeMessages.JsonSerializeMessage)] + public async Task GetFileFromConfigAsync(string configName, string relativeFilePathWithoutExtension, CancellationToken cancellationToken) + { + return (T)await GetFileFromConfigAsync(configName, relativeFilePathWithoutExtension, typeof(T), cancellationToken); + } + + [RequiresDynamicCode(DynamicCodeMessages.JsonSerializeMessage)] + [RequiresUnreferencedCode(DynamicCodeMessages.JsonSerializeMessage)] + public object GetFileFromConfig(string configName, string relativeFilePathWithoutExtension, Type type) + { + var configDir = Path.Combine(runningDir, "Config", configName); + var filePathWithoutExtension = Path.Combine(configDir, relativeFilePathWithoutExtension); + + if (ymlDeserializer is not null && File.Exists(filePathWithoutExtension + ".yml")) + { + using var fs = File.OpenRead(filePathWithoutExtension + ".yml"); + using var reader = new StreamReader(fs); + + return ymlDeserializer.Deserialize(reader, type) ?? throw new InvalidOperationException("Deserialization failed."); + } + + if (File.Exists(filePathWithoutExtension + ".json")) + { + using var fs = File.OpenRead(filePathWithoutExtension + ".json"); + + var obj = jsonContext is null + ? JsonSerializer.Deserialize(fs, type, jsonOptions) + : JsonSerializer.Deserialize(fs, type, jsonContext); + + return obj ?? throw new InvalidOperationException("Deserialization failed."); + } + + throw new FileNotFoundException("File not found.", filePathWithoutExtension); + } + + [RequiresDynamicCode(DynamicCodeMessages.JsonSerializeMessage)] + [RequiresUnreferencedCode(DynamicCodeMessages.JsonSerializeMessage)] + public T GetFileFromConfig(string configName, string relativeFilePathWithoutExtension) + { + return (T)GetFileFromConfig(configName, relativeFilePathWithoutExtension, typeof(T)); + } } diff --git a/Src/GBX.NET.Tool.CLI/ToolConsole.cs b/Src/GBX.NET.Tool.CLI/ToolConsole.cs index b70c0336d..dc952d613 100644 --- a/Src/GBX.NET.Tool.CLI/ToolConsole.cs +++ b/Src/GBX.NET.Tool.CLI/ToolConsole.cs @@ -2,7 +2,9 @@ using GBX.NET.Tool.CLI.Exceptions; using Microsoft.Extensions.Logging; using Spectre.Console; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Reflection; namespace GBX.NET.Tool.CLI; @@ -98,9 +100,11 @@ public static async Task> RunAsync(string[] args, ToolCo private async Task RunAsync(CancellationToken cancellationToken) { // Load console settings from file if exists otherwise create one - var consoleSettings = await settingsManager.GetOrCreateFileAsync("ConsoleSettings", - ToolJsonContext.Default.ConsoleSettings, - cancellationToken: cancellationToken); + var consoleSettings = options.YmlSerializer is null || options.YmlDeserializer is null + ? await settingsManager.GetOrCreateJsonFileAsync("ConsoleSettings", + ToolJsonContext.Default.ConsoleSettings, + cancellationToken: cancellationToken) + : settingsManager.GetOrCreateYmlFile("ConsoleSettings"); var toolSettings = argsResolver.Resolve(consoleSettings); @@ -169,9 +173,14 @@ private async Task RunAsync(CancellationToken cancellationToken) throw new ConsoleProblemException("No files were passed to the tool.\nPlease drag and drop files onto the executable, or include the input paths as the command line arguments.\nFile paths, directory paths, or URLs are supported in any order."); } + var configName = string.IsNullOrWhiteSpace(toolSettings.ConsoleSettings.ConfigName) ? "Default" + : toolSettings.ConsoleSettings.ConfigName; + + var complexConfig = new ComplexConfig(configName, settingsManager); + // If the tool has setup, apply tool things below to setup - var toolInstanceMaker = new ToolInstanceMaker(toolFunctionality, toolSettings, logger); + var toolInstanceMaker = new ToolInstanceMaker(toolFunctionality, toolSettings, complexConfig, logger); var outputDistributor = new OutputDistributor(runningDir, toolSettings, logger); AnsiConsole.WriteLine(); @@ -189,9 +198,6 @@ private async Task RunAsync(CancellationToken cancellationToken) if (toolInstance is IConfigurable configurable) { - var configName = string.IsNullOrWhiteSpace(toolSettings.ConsoleSettings.ConfigName) ? "Default" - : toolSettings.ConsoleSettings.ConfigName; - logger.LogInformation("Populating tool config (name: {ConfigName}, type: {ConfigType})...", configName, configurable.Config.GetType()); await settingsManager.PopulateConfigAsync(configName, configurable.Config, cancellationToken); @@ -204,18 +210,33 @@ private async Task RunAsync(CancellationToken cancellationToken) logger.LogInformation("Producing..."); var produceMethod = toolFunctionality.ProduceMethods[0]; - var result = produceMethod.Invoke(toolInstance, null); - if (result is IEnumerable) + try { - logger.LogInformation("Producing for each distributed output..."); + var watch = Stopwatch.StartNew(); + + var result = produceMethod.Invoke(toolInstance, null); + + if (result is IEnumerable and not System.Collections.ICollection) + { + logger.LogInformation("Producing for each distributed output..."); + } + else + { + logger.LogInformation("Produced in {Milliseconds}ms! Distributing output...", watch.ElapsedMilliseconds); + } + + await outputDistributor.DistributeOutputAsync(result, mutating: false, cancellationToken); } - else + catch (TargetInvocationException ex) { - logger.LogInformation("Produced! Distributing output..."); - } + logger.LogError(ex.InnerException, "Error while producing."); - await outputDistributor.DistributeOutputAsync(result, cancellationToken); + if (ex.InnerException is not null) + { + AnsiConsole.WriteException(ex.InnerException); + } + } } else if (toolFunctionality.ProduceMethods.Length > 1) { @@ -255,7 +276,7 @@ private async Task RunAsync(CancellationToken cancellationToken) } } - await outputDistributor.DistributeOutputAsync(result, cancellationToken); + await outputDistributor.DistributeOutputAsync(result, mutating: false, cancellationToken); } } @@ -264,18 +285,33 @@ private async Task RunAsync(CancellationToken cancellationToken) logger.LogInformation("Mutating..."); var mutateMethod = toolFunctionality.MutateMethods[0]; - var result = mutateMethod.Invoke(toolInstance, null); - if (result is IEnumerable) + try { - logger.LogInformation("Mutationing while distributing output..."); + var watch = Stopwatch.StartNew(); + + var result = mutateMethod.Invoke(toolInstance, null); + + if (result is IEnumerable) + { + logger.LogInformation("Mutating while distributing output..."); + } + else + { + logger.LogInformation("Mutated in {Milliseconds}ms! Distributing output...", watch.ElapsedMilliseconds); + } + + await outputDistributor.DistributeOutputAsync(result, mutating: true, cancellationToken); } - else + catch (TargetInvocationException ex) { - logger.LogInformation("Mutated! Distributing output..."); - } + logger.LogError(ex.InnerException, "Error while mutating."); - await outputDistributor.DistributeOutputAsync(result, cancellationToken); + if (ex.InnerException is not null) + { + AnsiConsole.WriteException(ex.InnerException); + } + } } else if (toolFunctionality.MutateMethods.Length > 1) { @@ -295,7 +331,7 @@ private async Task RunAsync(CancellationToken cancellationToken) logger.LogInformation("Mutated! Distributing output..."); } - await outputDistributor.DistributeOutputAsync(result, cancellationToken); + await outputDistributor.DistributeOutputAsync(result, mutating: true, cancellationToken); } } diff --git a/Src/GBX.NET.Tool.CLI/ToolInstanceMaker.cs b/Src/GBX.NET.Tool.CLI/ToolInstanceMaker.cs index 5ae1c24c7..55687d92c 100644 --- a/Src/GBX.NET.Tool.CLI/ToolInstanceMaker.cs +++ b/Src/GBX.NET.Tool.CLI/ToolInstanceMaker.cs @@ -11,16 +11,18 @@ internal sealed class ToolInstanceMaker where T : ITool { private readonly ToolFunctionality toolFunctionality; private readonly ToolSettings toolSettings; + private readonly ComplexConfig complexConfig; private readonly ILogger logger; private readonly Dictionary resolvedInputs = []; private readonly HashSet usedObjects = []; private readonly List unprocessedObjects = []; - public ToolInstanceMaker(ToolFunctionality toolFunctionality, ToolSettings toolSettings, ILogger logger) + public ToolInstanceMaker(ToolFunctionality toolFunctionality, ToolSettings toolSettings, ComplexConfig complexConfig, ILogger logger) { this.toolFunctionality = toolFunctionality; this.toolSettings = toolSettings; + this.complexConfig = complexConfig; this.logger = logger; } @@ -112,6 +114,12 @@ public async IAsyncEnumerable MakeToolInstancesAsync([EnumeratorCancellation] continue; } + if (type == typeof(IComplexConfig)) + { + paramsForCtor[index++] = complexConfig; + continue; + } + if (!type.IsGenericType) { continue; diff --git a/Src/GBX.NET.Tool.StandardIO/Class1.cs b/Src/GBX.NET.Tool.StandardIO/Class1.cs new file mode 100644 index 000000000..1baff861e --- /dev/null +++ b/Src/GBX.NET.Tool.StandardIO/Class1.cs @@ -0,0 +1,6 @@ +namespace GBX.NET.Tool.StandardIO; + +public class Class1 +{ + +} diff --git a/Src/GBX.NET.Tool.StandardIO/GBX.NET.Tool.StandardIO.csproj b/Src/GBX.NET.Tool.StandardIO/GBX.NET.Tool.StandardIO.csproj new file mode 100644 index 000000000..fa71b7ae6 --- /dev/null +++ b/Src/GBX.NET.Tool.StandardIO/GBX.NET.Tool.StandardIO.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/Src/GBX.NET.Tool/ExternalFileAttribute.cs b/Src/GBX.NET.Tool/ExternalFileAttribute.cs index 7d41d7fb9..eaa78321a 100644 --- a/Src/GBX.NET.Tool/ExternalFileAttribute.cs +++ b/Src/GBX.NET.Tool/ExternalFileAttribute.cs @@ -5,7 +5,7 @@ /// /// The name of the file to be created and read from. [AttributeUsage(AttributeTargets.Property)] -public class ExternalFileAttribute(string fileName) : Attribute +public class ExternalFileAttribute(string fileNameWithoutExtension) : Attribute { - public string FileName { get; } = fileName; + public string FileNameWithoutExtension { get; } = fileNameWithoutExtension; } diff --git a/Src/GBX.NET.Tool/GBX.NET.Tool.csproj b/Src/GBX.NET.Tool/GBX.NET.Tool.csproj index 5e214b20a..f9da5ac51 100644 --- a/Src/GBX.NET.Tool/GBX.NET.Tool.csproj +++ b/Src/GBX.NET.Tool/GBX.NET.Tool.csproj @@ -2,7 +2,7 @@ GBX.NET.Tool - 0.1.0 + 0.2.0 BigBang1112 Base library for creating rich tools for different environments with GBX.NET. Copyright (c) 2024 Petr Pivoňka diff --git a/Src/GBX.NET.Tool/IComplexConfig.cs b/Src/GBX.NET.Tool/IComplexConfig.cs new file mode 100644 index 000000000..d577618f3 --- /dev/null +++ b/Src/GBX.NET.Tool/IComplexConfig.cs @@ -0,0 +1,14 @@ +using System.Diagnostics.CodeAnalysis; + +namespace GBX.NET.Tool; + +public interface IComplexConfig +{ + [RequiresDynamicCode("")] + [RequiresUnreferencedCode("")] + T Get(string filePathWithoutExtension, bool cache = false) where T : class; + + [RequiresDynamicCode("")] + [RequiresUnreferencedCode("")] + Task GetAsync(string filePathWithoutExtension, bool cache = false, CancellationToken cancellationToken = default) where T : class; +} diff --git a/Src/GBX.NET/Engines/Game/CGameCtnBlockInfo.chunkl b/Src/GBX.NET/Engines/Game/CGameCtnBlockInfo.chunkl index 73a800ab6..2ff0a10dc 100644 --- a/Src/GBX.NET/Engines/Game/CGameCtnBlockInfo.chunkl +++ b/Src/GBX.NET/Engines/Game/CGameCtnBlockInfo.chunkl @@ -14,8 +14,8 @@ CGameCtnBlockInfo 0x0304E000 CSceneMobil // VariantBaseAir 0x00C [TM10, TMSX, TMF] - iso4 // Sound1Loc/SpawnLocAir? - iso4 // Sound2Loc/SpawnLocGround? + iso4? SpawnLocGround + iso4? SpawnLocAir 0x00D [TMSX, TMF] bool // something with replacing? diff --git a/Src/GBX.NET/Engines/Game/CGameCtnChallenge.cs b/Src/GBX.NET/Engines/Game/CGameCtnChallenge.cs index 0f4a051ab..49edebd4e 100644 --- a/Src/GBX.NET/Engines/Game/CGameCtnChallenge.cs +++ b/Src/GBX.NET/Engines/Game/CGameCtnChallenge.cs @@ -18,6 +18,8 @@ public partial class CGameCtnChallenge : private TimeInt32? goldTime; // Only used if ChallengeParameters is null private TimeInt32? authorTime; // Only used if ChallengeParameters is null private int authorScore; // Only used if ChallengeParameters is null + private string? mapType; // Only used if ChallengeParameters is null + private string? mapStyle; // Only used if ChallengeParameters is null private Ident mapInfo = Ident.Empty; [AppliedWithChunk] @@ -148,6 +150,42 @@ public int AuthorScore } } + /// + /// Map type, the expected mode. If is available, it uses the value from there instead. + /// + [AppliedWithChunk(sinceVersion: 3)] + public string? MapType + { + get => ChallengeParameters is null ? mapType : ChallengeParameters.MapType; + set + { + if (ChallengeParameters is not null) + { + ChallengeParameters.MapType = value; + } + + mapType = value; + } + } + + /// + /// Map style. If is available, it uses the value from there instead. + /// + [AppliedWithChunk(sinceVersion: 3)] + public string? MapStyle + { + get => ChallengeParameters is null ? mapStyle : ChallengeParameters.MapStyle; + set + { + if (ChallengeParameters is not null) + { + ChallengeParameters.MapStyle = value; + } + + mapStyle = value; + } + } + /// /// The map's UID. /// @@ -921,7 +959,12 @@ private void WriteLightMapCacheSmall(CGameCtnChallenge n, GbxWriter w) w.Write(n.LightmapFrames?.Length ?? 0); } - foreach (var frame in n.LightmapFrames ?? []) + if (n.LightmapFrames is null || n.LightmapFrames.Length == 0) + { + return; + } + + foreach (var frame in n.LightmapFrames) { w.WriteWritable(frame, version: lightmapVersion); } @@ -1177,6 +1220,11 @@ public override void Read(CGameCtnChallenge n, GbxReader r) if (Version >= 5) { + if (Version >= 9) + { + throw new ChunkVersionNotSupportedException(Version); + } + var blockIndexes = r.ReadArray(); // block indexes, -1 means itemIndexes will have the value instead var usedBlocks = new CGameCtnBlock?[blockIndexes.Length]; @@ -1222,6 +1270,11 @@ public override void Read(CGameCtnChallenge n, GbxReader r) } } + if (Version >= 8) + { + return; + } + // always the same count as anchoredObjects var snappedIndexes = r.ReadArray(); // "snapped onto block/item" indexes @@ -1250,11 +1303,6 @@ public override void Read(CGameCtnChallenge n, GbxReader r) n.anchoredObjects[i].SnappedOnGroup = snapItemGroups?[snappedIndex] ?? 0; } - - if (Version >= 8) - { - throw new ChunkVersionNotSupportedException(Version); - } } } @@ -1400,6 +1448,11 @@ public override void Write(CGameCtnChallenge n, GbxWriter w) itemW.WriteArray(Enumerable.Repeat(-1, usedBlockIndexList.Count).ToArray()); } + if (Version >= 8) + { + return; + } + itemW.WriteArray(snappedOnIndices.ToArray()); } @@ -1684,16 +1737,16 @@ public partial class Chunk0304305B : IVersionable { public int Version { get; set; } - public bool U01; - public bool U02; + public bool U13; + public bool U14; public override void ReadWrite(CGameCtnChallenge n, GbxReaderWriter rw) { rw.VersionInt32(this); n.HasLightmaps = rw.Boolean(n.HasLightmaps); - rw.Boolean(ref U01); - rw.Boolean(ref U02); + rw.Boolean(ref U13); + rw.Boolean(ref U14); if (!n.HasLightmaps) { diff --git a/Src/GBX.NET/Engines/Game/CGameGhost.cs b/Src/GBX.NET/Engines/Game/CGameGhost.cs index bc834d767..6e0e31203 100644 --- a/Src/GBX.NET/Engines/Game/CGameGhost.cs +++ b/Src/GBX.NET/Engines/Game/CGameGhost.cs @@ -34,11 +34,12 @@ public override void Read(CGameGhost n, GbxReader r) n.RawData = r.ReadData(); n.sampleData = new Data(n.RawData) { - Offsets = r.ReadArray(), - IsFixedTimeStep = r.ReadBoolean(), - SamplePeriod = r.ReadTimeInt32(), - Version = r.ReadInt32() + Offsets = r.ReadArray() }; + Times = r.ReadArray(); + n.sampleData.IsFixedTimeStep = r.ReadBoolean(); + n.sampleData.SamplePeriod = r.ReadTimeInt32(); + n.sampleData.Version = r.ReadInt32(); } public override void Write(CGameGhost n, GbxWriter w) diff --git a/Src/GBX.NET/Engines/Plug/CPlugCrystal.chunkl b/Src/GBX.NET/Engines/Plug/CPlugCrystal.chunkl index 4d18b0abc..9989a055b 100644 --- a/Src/GBX.NET/Engines/Plug/CPlugCrystal.chunkl +++ b/Src/GBX.NET/Engines/Plug/CPlugCrystal.chunkl @@ -46,6 +46,7 @@ enum ELayerType Cubes // Voxels Trigger // TriggerShape SpawnPosition // RespawnPos + Light = 18 enum EAxis X @@ -179,6 +180,12 @@ archive SpawnPositionLayer (inherits: ModifierLayer, contextual) if SpawnPositionVersion >= 1 float RollAngle +archive LightLayer (inherits: ModifierLayer, contextual) + base + int LightVersion + CPlugLightUserModel[] Lights + LightPos[] LightPositions + archive VoxelSpace throw @@ -205,4 +212,8 @@ archive Part int U03 string Name int U04 - int[] U05 \ No newline at end of file + int[] U05 + +archive LightPos + int + iso4 \ No newline at end of file diff --git a/Src/GBX.NET/Engines/Plug/CPlugCrystal.cs b/Src/GBX.NET/Engines/Plug/CPlugCrystal.cs index 02845dce5..a694013d3 100644 --- a/Src/GBX.NET/Engines/Plug/CPlugCrystal.cs +++ b/Src/GBX.NET/Engines/Plug/CPlugCrystal.cs @@ -66,7 +66,8 @@ public override void Read(CPlugCrystal n, GbxReader r) for (var i = 0; i < layerCount; i++) { - Layer layer = (ELayerType)r.ReadInt32() switch + var layerType = (ELayerType)r.ReadInt32(); + Layer layer = layerType switch { ELayerType.Geometry => new GeometryLayer(), ELayerType.SubdivideSmooth => new SubdivideSmoothLayer(), @@ -84,7 +85,8 @@ public override void Read(CPlugCrystal n, GbxReader r) ELayerType.Cubes => new CubesLayer(), ELayerType.Trigger => new TriggerLayer(), ELayerType.SpawnPosition => new SpawnPositionLayer(), - _ => throw new NotSupportedException() + ELayerType.Light => new LightLayer(), + _ => throw new NotSupportedException($"Layer type {layerType} is not supported") }; layer.Read(r, n); @@ -119,6 +121,7 @@ public override void Write(CPlugCrystal n, GbxWriter w) CubesLayer => (int)ELayerType.Cubes, TriggerLayer => (int)ELayerType.Trigger, SpawnPositionLayer => (int)ELayerType.SpawnPosition, + LightLayer => (int)ELayerType.Light, _ => throw new NotSupportedException() }); @@ -199,6 +202,12 @@ public partial class TriggerLayer; [ArchiveGenerationOptions(StructureKind = StructureKind.SeparateReadAndWrite)] public partial class SpawnPositionLayer; + [ArchiveGenerationOptions(StructureKind = StructureKind.SeparateReadAndWrite)] + public partial class LightLayer; + + [ArchiveGenerationOptions(StructureKind = StructureKind.SeparateReadAndWrite)] + public partial class LightPos; + [ArchiveGenerationOptions(StructureKind = StructureKind.SeparateReadAndWrite)] public partial class VoxelSpace; @@ -358,8 +367,8 @@ public void Read(GbxReader r, CPlugCrystal n, int v = 0) if (Version >= 25) { - materialIndex = Version >= 33 - ? r.ReadOptimizedInt(n.Materials.Count) // normal int when count = 0? + materialIndex = Version >= 33 && n.Materials.Count > 0 + ? r.ReadOptimizedInt(n.Materials.Count) // normal int when count = 0? yes : r.ReadInt32(); } @@ -578,7 +587,7 @@ public void Write(GbxWriter w, CPlugCrystal n, int v = 0) { var materialIndex = face.Material is null ? -1 : n.Materials.IndexOf(face.Material); - if (Version >= 33) + if (Version >= 33 && n.Materials.Count > 0) { // this can write 255 in case of -1, which is not correct? w.WriteOptimizedInt(materialIndex, n.Materials.Count); diff --git a/Src/GBX.NET/Engines/Plug/CPlugMaterialUserInst.chunkl b/Src/GBX.NET/Engines/Plug/CPlugMaterialUserInst.chunkl index 30a3ebaff..b9b705798 100644 --- a/Src/GBX.NET/Engines/Plug/CPlugMaterialUserInst.chunkl +++ b/Src/GBX.NET/Engines/Plug/CPlugMaterialUserInst.chunkl @@ -7,9 +7,9 @@ CPlugMaterialUserInst 0x090FD000 id MaterialName id Model string BaseTexture - byte SurfacePhysicId + byte SurfacePhysicId v10+ - byte SurfaceGameplayId + byte SurfaceGameplayId v1+ if IsUsingGameMaterial string Link @@ -57,6 +57,29 @@ enum ETexAddress Clamp Border +enum GameplayId + None + Turbo + Turbo2 + TurboRoulette + FreeWheeling + NoGrip + NoSteering + ForceAcceleration + Reset + SlowMotion + Bumper + Bumper2 + Fragile + NoBrakes + Cruise + ReactorBoost_Oriented + ReactorBoost2_Oriented + VehicleTransform_Reset + VehicleTransform_CarSnow + VehicleTransform_CarRally + VehicleTransform_CarDesert + archive Cst id id diff --git a/Src/GBX.NET/Engines/Plug/CPlugVisualQuads.chunkl b/Src/GBX.NET/Engines/Plug/CPlugVisualQuads.chunkl new file mode 100644 index 000000000..6bc1a8c5c --- /dev/null +++ b/Src/GBX.NET/Engines/Plug/CPlugVisualQuads.chunkl @@ -0,0 +1,2 @@ +CPlugVisualQuads 0x09027000 +- inherits: CPlugVisual3D \ No newline at end of file diff --git a/Src/GBX.NET/Extensions/Exporters/ObjExporter.cs b/Src/GBX.NET/Extensions/Exporters/ObjExporter.cs index c826e7017..f588c9d91 100644 --- a/Src/GBX.NET/Extensions/Exporters/ObjExporter.cs +++ b/Src/GBX.NET/Extensions/Exporters/ObjExporter.cs @@ -278,11 +278,6 @@ public static void Export(CPlugSolid solid, TextWriter objWriter, TextWriter mtl continue; } - if (visual.TexCoords.Length == 0) - { - continue; - } - var materialName = GbxPath.GetFileNameWithoutExtension(t.ShaderFile.FilePath); objWriter.WriteLine("g {0}", materialName); @@ -292,7 +287,10 @@ public static void Export(CPlugSolid solid, TextWriter objWriter, TextWriter mtl foreach (var index in visual.IndexBuffer.Indices) { - objWriter.Write('f'); + if (triangleCounter % 3 == 0) + { + objWriter.Write('f'); + } var v = visual.Vertices[index]; var locatedPos = new Vec3( @@ -300,9 +298,10 @@ public static void Export(CPlugSolid solid, TextWriter objWriter, TextWriter mtl v.Position.X * loc.YZ + v.Position.Y * loc.YY + v.Position.Z * loc.YZ + loc.TY, v.Position.X * loc.ZX + v.Position.Y * loc.ZY + v.Position.Z * loc.ZZ + loc.TZ ); - var uvIndex = uvs[visual.TexCoords[0].TexCoords[index].UV]; - var faceIndex = $" {positionsDict[locatedPos] + 1}/{uvIndex + 1}"; + var faceIndex = visual.TexCoords.Length > 0 + ? $" {positionsDict[locatedPos] + 1}/{uvs[visual.TexCoords[0].TexCoords[index].UV] + 1}" + : $" {positionsDict[locatedPos] + 1}"; objWriter.Write(faceIndex); @@ -443,7 +442,10 @@ public static void Export(CPlugSolid2Model solid, TextWriter objWriter, TextWrit foreach (var index in visual.IndexBuffer.Indices) { - objWriter.Write('f'); + if (triangleCounter % 3 == 0) + { + objWriter.Write('f'); + } var v = visual.VertexStreams.FirstOrDefault()?.Positions?[index] ?? visual.Vertices[index].Position; diff --git a/Src/GBX.NET/GBX.NET.csproj b/Src/GBX.NET/GBX.NET.csproj index c08d16eac..824d810b7 100644 --- a/Src/GBX.NET/GBX.NET.csproj +++ b/Src/GBX.NET/GBX.NET.csproj @@ -2,7 +2,7 @@ GBX.NET - 2.0.7 + 2.0.8 BigBang1112 General purpose library for Gbx files - data from Nadeo games like Trackmania or Shootmania. It supports high performance serialization and deserialization of 200+ Gbx classes. Copyright (c) 2024 Petr Pivoňka diff --git a/Src/GBX.NET/README.md b/Src/GBX.NET/README.md index 80135d596..44c6c7941 100644 --- a/Src/GBX.NET/README.md +++ b/Src/GBX.NET/README.md @@ -186,7 +186,7 @@ Some of the common types to start with (a lot more are supported): Many *essential* Gbx files from many games are supported: -- **Trackmania (2020)**, April 2024 update +- **Trackmania (2020)**, July 2024 update - **ManiaPlanet 4**(.1), TM2/SM - **Trackmania Turbo** - ManiaPlanet 3, TM2/SM diff --git a/Src/GBX.NET/Serialization/GbxReader.cs b/Src/GBX.NET/Serialization/GbxReader.cs index c56dbc047..269cc56a3 100644 --- a/Src/GBX.NET/Serialization/GbxReader.cs +++ b/Src/GBX.NET/Serialization/GbxReader.cs @@ -1276,8 +1276,8 @@ public void ReadMarker(string value) public int ReadOptimizedInt(int determineFrom) => (uint)determineFrom switch { - >= ushort.MaxValue => ReadInt32(), - >= byte.MaxValue => ReadUInt16(), + > ushort.MaxValue => ReadInt32(), + > byte.MaxValue => ReadUInt16(), _ => ReadByte() }; diff --git a/Src/GBX.NET/Serialization/GbxWriter.cs b/Src/GBX.NET/Serialization/GbxWriter.cs index 2b846b3f1..b1a13a5c6 100644 --- a/Src/GBX.NET/Serialization/GbxWriter.cs +++ b/Src/GBX.NET/Serialization/GbxWriter.cs @@ -874,10 +874,10 @@ public void WriteOptimizedInt(int value, int determineFrom) { switch ((uint)determineFrom) { - case >= ushort.MaxValue: + case > ushort.MaxValue: Write(value); break; - case >= byte.MaxValue: + case > byte.MaxValue: Write((ushort)value); break; default: diff --git a/Tools/BulkParseTest/Properties/launchSettings.json b/Tools/BulkParseTest/Properties/launchSettings.json new file mode 100644 index 000000000..ccc8fa1ac --- /dev/null +++ b/Tools/BulkParseTest/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "BulkParseTest": { + "commandName": "Project", + "commandLineArgs": "\"E:\\TMUF_CORRECT\"" + } + } +} \ No newline at end of file