From 24f0620784929a13a6484532877861f823476f05 Mon Sep 17 00:00:00 2001 From: Wannes Gennar Date: Tue, 12 Nov 2024 05:32:39 +0100 Subject: [PATCH] Maintenance (#114) Changes: - Refactor source generator for incremental processing. - Add multi-reward support to assignments ( fixes #109 ). - Add header validation for X-Super-Client and X-Super-Contact in RateLimitMiddleware ( fixes #94 ). General: - Update package dependencies. - Update helldivers-2/json. - Format code with dotnet format. - Remove unused package reference. --- docs/containers.md | 21 +++ .../Configuration/ApiConfiguration.cs | 5 + src/Helldivers-2-API/Helldivers-2-API.csproj | 10 +- .../Middlewares/RateLimitMiddleware.cs | 36 +++++ .../appsettings.Development.json | 1 + .../Helldivers-2-Core.csproj | 2 +- .../Mapping/V1/AssignmentMapper.cs | 1 + .../ArrowHead/Assignments/Setting.cs | 4 +- src/Helldivers-2-Models/V1/Assignment.cs | 2 + src/Helldivers-2-Models/json | 2 +- .../Contracts/IJsonParser.cs | 18 +++ .../Helldivers-2-SourceGen.csproj | 7 +- .../Parsers/BaseJsonParser.cs | 50 +++++++ .../Parsers/BiomesParser.cs | 22 +++ .../Parsers/EnvironmentalsParser.cs | 23 +++ .../Parsers/FactionsParser.cs | 22 +++ .../Parsers/PlanetsParser.cs | 40 ++++++ .../StaticJsonSourceGenerator.cs | 136 +++++++++--------- .../Helldivers-2-Sync.csproj | 8 +- 19 files changed, 324 insertions(+), 86 deletions(-) create mode 100644 src/Helldivers-2-SourceGen/Contracts/IJsonParser.cs create mode 100644 src/Helldivers-2-SourceGen/Parsers/BaseJsonParser.cs create mode 100644 src/Helldivers-2-SourceGen/Parsers/BiomesParser.cs create mode 100644 src/Helldivers-2-SourceGen/Parsers/EnvironmentalsParser.cs create mode 100644 src/Helldivers-2-SourceGen/Parsers/FactionsParser.cs create mode 100644 src/Helldivers-2-SourceGen/Parsers/PlanetsParser.cs diff --git a/docs/containers.md b/docs/containers.md index 303c966..b15a862 100644 --- a/docs/containers.md +++ b/docs/containers.md @@ -129,3 +129,24 @@ make to the application. Alternatively, if you use the hosted versions you can request an API key that allows for higher rate limits by sponsoring this project! (if you self-host you can generate your own keys too!). + +# Disabling the client validation +If you run the container and navigate to it in your browser +you might come across the following message: +```json +{"message":"The X-Super-Client and X-Super-Contact headers are required"} +``` + +This is because by default the API will validate all requests send `X-Super-Client` and `X-Super-Contact` +headers along. You can disable this behaviour by setting the following flag: +```json +{ + "Helldivers": { + "API": { + "ValidateClients": false + } + } +} +``` +Or through environment flags with `-e Helldivers__API__ValidateClients` + diff --git a/src/Helldivers-2-API/Configuration/ApiConfiguration.cs b/src/Helldivers-2-API/Configuration/ApiConfiguration.cs index 1dadb48..69a338f 100644 --- a/src/Helldivers-2-API/Configuration/ApiConfiguration.cs +++ b/src/Helldivers-2-API/Configuration/ApiConfiguration.cs @@ -20,6 +20,11 @@ public sealed class ApiConfiguration /// public string Blacklist { get; set; } = string.Empty; + /// + /// Whether X-Super-Client and X-Super-Contact headers are validated. + /// + public bool ValidateClients { get; set; } = true; + /// /// Contains the for the API. /// diff --git a/src/Helldivers-2-API/Helldivers-2-API.csproj b/src/Helldivers-2-API/Helldivers-2-API.csproj index 98c8a3c..8316a3a 100644 --- a/src/Helldivers-2-API/Helldivers-2-API.csproj +++ b/src/Helldivers-2-API/Helldivers-2-API.csproj @@ -23,8 +23,8 @@ - - + + @@ -36,12 +36,12 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/Helldivers-2-API/Middlewares/RateLimitMiddleware.cs b/src/Helldivers-2-API/Middlewares/RateLimitMiddleware.cs index 6c2db5c..158570c 100644 --- a/src/Helldivers-2-API/Middlewares/RateLimitMiddleware.cs +++ b/src/Helldivers-2-API/Middlewares/RateLimitMiddleware.cs @@ -2,8 +2,11 @@ using Helldivers.API.Extensions; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; using System.Net; using System.Security.Claims; +using System.Text; +using System.Text.Json; using System.Threading.RateLimiting; namespace Helldivers.API.Middlewares; @@ -26,6 +29,12 @@ IMemoryCache cache /// public async Task InvokeAsync(HttpContext context, RequestDelegate next) { + if (IsValidRequest(context) is false) + { + await RejectRequest(context); + return; + } + var limiter = GetRateLimiter(context); using var lease = await limiter.AcquireAsync(permitCount: 1, context.RequestAborted); if (limiter.GetStatistics() is { } statistics) @@ -45,6 +54,18 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next) await next(context); } + /// + /// Checks if the request is "valid" (contains the correct X-Super-* headers). + /// + private bool IsValidRequest(HttpContext context) + { + if (options.Value.ValidateClients is false) + return true; + + return context.Request.Headers.ContainsKey(Constants.CLIENT_HEADER_NAME) + && context.Request.Headers.ContainsKey(Constants.CONTACT_HEADER_NAME); + } + private RateLimiter GetRateLimiter(HttpContext http) { if (http.User.Identity?.IsAuthenticated ?? false) @@ -86,4 +107,19 @@ private RateLimiter GetRateLimiterForUser(ClaimsPrincipal user) }); }) ?? throw new InvalidOperationException($"Creating rate limiter failed for {name}"); } + + private async Task RejectRequest(HttpContext context) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + context.Response.Headers.WWWAuthenticate = "X-Super-Client"; + context.Response.ContentType = "application/json"; + + var writer = new Utf8JsonWriter(context.Response.Body); + writer.WriteStartObject(); + writer.WritePropertyName("message"); + writer.WriteStringValue("The X-Super-Client and X-Super-Contact headers are required"); + writer.WriteEndObject(); + + await writer.FlushAsync(context.RequestAborted); + } } diff --git a/src/Helldivers-2-API/appsettings.Development.json b/src/Helldivers-2-API/appsettings.Development.json index 3bc86a6..ebc03f0 100644 --- a/src/Helldivers-2-API/appsettings.Development.json +++ b/src/Helldivers-2-API/appsettings.Development.json @@ -10,6 +10,7 @@ }, "Helldivers": { "API": { + "ValidateClients": false, "Authentication": { "SigningKey": "I4eGmsXbDXfxlRo5N+w0ToRGN8aWSIaYWbZ2zMFqqnI=" } diff --git a/src/Helldivers-2-Core/Helldivers-2-Core.csproj b/src/Helldivers-2-Core/Helldivers-2-Core.csproj index 35ca7b9..50b582d 100644 --- a/src/Helldivers-2-Core/Helldivers-2-Core.csproj +++ b/src/Helldivers-2-Core/Helldivers-2-Core.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/Helldivers-2-Core/Mapping/V1/AssignmentMapper.cs b/src/Helldivers-2-Core/Mapping/V1/AssignmentMapper.cs index 3eaeeda..e17e042 100644 --- a/src/Helldivers-2-Core/Mapping/V1/AssignmentMapper.cs +++ b/src/Helldivers-2-Core/Mapping/V1/AssignmentMapper.cs @@ -54,6 +54,7 @@ private Assignment MapToV1(Dictionary trans Description: LocalizedMessage.FromStrings(descriptions), Tasks: invariant.Setting.Tasks.Select(MapToV1).ToList(), Reward: MapToV1(invariant.Setting.Reward), + Rewards: invariant.Setting.Rewards.Select(MapToV1).ToList(), Expiration: expiration ); } diff --git a/src/Helldivers-2-Models/ArrowHead/Assignments/Setting.cs b/src/Helldivers-2-Models/ArrowHead/Assignments/Setting.cs index 882bd76..56f1b38 100644 --- a/src/Helldivers-2-Models/ArrowHead/Assignments/Setting.cs +++ b/src/Helldivers-2-Models/ArrowHead/Assignments/Setting.cs @@ -8,7 +8,8 @@ /// The briefing (description) of this assignment. /// A description of what is expected of Helldivers to complete the assignment. /// A list of s describing the assignment requirements. -/// Contains information on the rewards players willr eceive upon completion. +/// Contains information on the reward players will receive upon completion. +/// Contains information on the rewards players will receive upon completion. /// Flags, suspected to be a binary OR'd value, purpose unknown. public sealed record Setting( int Type, @@ -17,5 +18,6 @@ public sealed record Setting( string TaskDescription, List Tasks, Reward Reward, + List Rewards, int Flags ); diff --git a/src/Helldivers-2-Models/V1/Assignment.cs b/src/Helldivers-2-Models/V1/Assignment.cs index 88d9bd7..f60a7c1 100644 --- a/src/Helldivers-2-Models/V1/Assignment.cs +++ b/src/Helldivers-2-Models/V1/Assignment.cs @@ -15,6 +15,7 @@ namespace Helldivers.Models.V1; /// A very short summary of the description. /// A list of tasks that need to be completed for this assignment. /// The reward for completing the assignment. +/// A list of rewards for completing the assignment. /// The date when the assignment will expire. public sealed record Assignment( long Id, @@ -24,5 +25,6 @@ public sealed record Assignment( LocalizedMessage Description, List Tasks, Reward Reward, + List Rewards, DateTime Expiration ); diff --git a/src/Helldivers-2-Models/json b/src/Helldivers-2-Models/json index 2522dfc..6885068 160000 --- a/src/Helldivers-2-Models/json +++ b/src/Helldivers-2-Models/json @@ -1 +1 @@ -Subproject commit 2522dfcd6ce55ac2c670520affa869a16a46e78c +Subproject commit 688506848075317ea0037a508321b40f96b054d8 diff --git a/src/Helldivers-2-SourceGen/Contracts/IJsonParser.cs b/src/Helldivers-2-SourceGen/Contracts/IJsonParser.cs new file mode 100644 index 0000000..6286548 --- /dev/null +++ b/src/Helldivers-2-SourceGen/Contracts/IJsonParser.cs @@ -0,0 +1,18 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace Helldivers.SourceGen.Contracts; + +/// +/// Interface for parsing JSON strings and generating corresponding C# source code. +/// +public interface IJsonParser +{ + /// + /// Parses the given source text and generates corresponding C# source code. + /// + /// The source file to be parsed. + /// A for cancelling the parse process. + /// A object representing the generated C# source code. + SourceText Parse(AdditionalText file, CancellationToken cancellationToken = default); +} diff --git a/src/Helldivers-2-SourceGen/Helldivers-2-SourceGen.csproj b/src/Helldivers-2-SourceGen/Helldivers-2-SourceGen.csproj index 121b4f2..43ddd4a 100644 --- a/src/Helldivers-2-SourceGen/Helldivers-2-SourceGen.csproj +++ b/src/Helldivers-2-SourceGen/Helldivers-2-SourceGen.csproj @@ -14,16 +14,15 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + - + diff --git a/src/Helldivers-2-SourceGen/Parsers/BaseJsonParser.cs b/src/Helldivers-2-SourceGen/Parsers/BaseJsonParser.cs new file mode 100644 index 0000000..e1e422e --- /dev/null +++ b/src/Helldivers-2-SourceGen/Parsers/BaseJsonParser.cs @@ -0,0 +1,50 @@ +using Helldivers.SourceGen.Contracts; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using System.Text; + +namespace Helldivers.SourceGen.Parsers; + +/// +/// An abstract base class for parsing JSON data and generating corresponding C# source code. +/// +public abstract class BaseJsonParser : IJsonParser +{ + private const string TEMPLATE = @"// +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +using global::System.Collections.Generic; +using global::Helldivers.Models.Domain.Localization; + +namespace Helldivers.Models; + +public static partial class Static +{{ + /// Public list of {0} entries from {1} + public static {2} {0} = {3}; +}}"; + + /// + public SourceText Parse(AdditionalText file, CancellationToken cancellationToken = default) + { + var filename = Path.GetFileName(file.Path); + var json = file.GetText(cancellationToken)?.ToString(); + + var name = Path.GetFileNameWithoutExtension(file.Path); + name = $"{char.ToUpper(name[0])}{name.Substring(1)}"; + + if (string.IsNullOrWhiteSpace(json) is false) + { + var (type, csharp) = Parse(json!); + + var output = string.Format(TEMPLATE, name, filename, type, csharp); + return SourceText.From(output, Encoding.UTF8); + } + + return SourceText.From("// Could not read JSON file"); + } + + /// + /// Convert the JSON string into C# code that can be injected. + /// + protected abstract (string Type, string Source) Parse(string json); +} diff --git a/src/Helldivers-2-SourceGen/Parsers/BiomesParser.cs b/src/Helldivers-2-SourceGen/Parsers/BiomesParser.cs new file mode 100644 index 0000000..3a1df0a --- /dev/null +++ b/src/Helldivers-2-SourceGen/Parsers/BiomesParser.cs @@ -0,0 +1,22 @@ +using System.Text; +using System.Text.Json; + +namespace Helldivers.SourceGen.Parsers; + +/// +/// BiomesParser is responsible for parsing a JSON string representation of biomes. +/// +public class BiomesParser : BaseJsonParser +{ + /// + protected override (string Type, string Source) Parse(string json) + { + var builder = new StringBuilder("new Dictionary()\n\t{\n"); + var entries = JsonSerializer.Deserialize>>(json)!; + foreach (var pair in entries) + builder.AppendLine($@"{'\t'}{'\t'}{{ ""{pair.Key}"", new Helldivers.Models.V1.Planets.Biome(""{pair.Value["name"]}"", ""{pair.Value["description"]}"") }},"); + + builder.Append("\t}"); + return ("IReadOnlyDictionary", builder.ToString()); + } +} diff --git a/src/Helldivers-2-SourceGen/Parsers/EnvironmentalsParser.cs b/src/Helldivers-2-SourceGen/Parsers/EnvironmentalsParser.cs new file mode 100644 index 0000000..b00298c --- /dev/null +++ b/src/Helldivers-2-SourceGen/Parsers/EnvironmentalsParser.cs @@ -0,0 +1,23 @@ +using System.Text; +using System.Text.Json; + +namespace Helldivers.SourceGen.Parsers; + +/// +/// The EnvironmentalsParser class is responsible for parsing JSON strings +/// representing environmental hazards and converting them into C# source code. +/// +public class EnvironmentalsParser : BaseJsonParser +{ + /// + protected override (string Type, string Source) Parse(string json) + { + var builder = new StringBuilder("new Dictionary()\n\t{\n"); + var entries = JsonSerializer.Deserialize>>(json)!; + foreach (var pair in entries) + builder.AppendLine($@"{'\t'}{'\t'}{{ ""{pair.Key}"", new Helldivers.Models.V1.Planets.Hazard(""{pair.Value["name"]}"", ""{pair.Value["description"]}"") }},"); + + builder.Append("\t}"); + return ("IReadOnlyDictionary", builder.ToString()); + } +} diff --git a/src/Helldivers-2-SourceGen/Parsers/FactionsParser.cs b/src/Helldivers-2-SourceGen/Parsers/FactionsParser.cs new file mode 100644 index 0000000..9b4cc0a --- /dev/null +++ b/src/Helldivers-2-SourceGen/Parsers/FactionsParser.cs @@ -0,0 +1,22 @@ +using System.Text; +using System.Text.Json; + +namespace Helldivers.SourceGen.Parsers; + +/// +/// Parses JSON data of factions and generates the corresponding C# source code representation. +/// +public class FactionsParser : BaseJsonParser +{ + /// + protected override (string Type, string Source) Parse(string json) + { + var builder = new StringBuilder("new Dictionary()\n\t{\n"); + var entries = JsonSerializer.Deserialize>(json)!; + foreach (var pair in entries) + builder.AppendLine($@"{'\t'}{'\t'}{{ {pair.Key}, ""{pair.Value}"" }},"); + + builder.Append("\t}"); + return ("IReadOnlyDictionary", builder.ToString()); + } +} diff --git a/src/Helldivers-2-SourceGen/Parsers/PlanetsParser.cs b/src/Helldivers-2-SourceGen/Parsers/PlanetsParser.cs new file mode 100644 index 0000000..c3248e3 --- /dev/null +++ b/src/Helldivers-2-SourceGen/Parsers/PlanetsParser.cs @@ -0,0 +1,40 @@ +using System.Text; +using System.Text.Json; + +namespace Helldivers.SourceGen.Parsers; + +/// +/// Handles parsing the planets.json and generating the resulting planet data. +/// +public class PlanetsParser : BaseJsonParser +{ + /// + protected override (string Type, string Source) Parse(string json) + { + var builder = new StringBuilder("new Dictionary Environmentals)>()\n\t{\n"); + var document = JsonDocument.Parse(json); + foreach (var property in document.RootElement.EnumerateObject()) + { + var index = property.Name; + var name = property.Value.GetProperty("name").GetString(); + var names = property + .Value + .GetProperty("names") + .EnumerateObject() + .ToDictionary(prop => prop.Name, prop => prop.Value.GetString()!); + var sector = property.Value.GetProperty("sector").GetString(); + var biome = property.Value.GetProperty("biome").GetString(); + var environmentals = property + .Value + .GetProperty("environmentals") + .EnumerateArray() + .Select(prop => $@"""{prop.GetString()!}""") + .ToList(); + + builder.AppendLine($@"{'\t'}{'\t'}{{ {index}, (LocalizedMessage.FromStrings([{string.Join(", ", names.Select(pair => $@"new KeyValuePair(""{pair.Key}"", ""{pair.Value}"")"))}]), ""{sector}"", ""{biome}"", [{string.Join(", ", environmentals)}]) }},"); + } + + builder.Append("\t}"); + return ("IReadOnlyDictionary Environmentals)>", builder.ToString()); + } +} diff --git a/src/Helldivers-2-SourceGen/StaticJsonSourceGenerator.cs b/src/Helldivers-2-SourceGen/StaticJsonSourceGenerator.cs index feca41f..da5d73d 100644 --- a/src/Helldivers-2-SourceGen/StaticJsonSourceGenerator.cs +++ b/src/Helldivers-2-SourceGen/StaticJsonSourceGenerator.cs @@ -1,6 +1,6 @@ +using Helldivers.SourceGen.Contracts; +using Helldivers.SourceGen.Parsers; using Microsoft.CodeAnalysis; -using System.Text; -using System.Text.Json; namespace Helldivers.SourceGen; @@ -9,14 +9,74 @@ namespace Helldivers.SourceGen; /// When using a simple text file as a baseline, we can create a non-incremental source generator. /// [Generator] -public class StaticJsonSourceGenerator : ISourceGenerator +public class StaticJsonSourceGenerator : IIncrementalGenerator { + private static readonly IJsonParser PlanetParser = new PlanetsParser(); + private static readonly IJsonParser BiomesParser = new BiomesParser(); + private static readonly IJsonParser EnvironmentalsParser = new EnvironmentalsParser(); + private static readonly IJsonParser FactionsParser = new FactionsParser(); + /// - public void Initialize(GeneratorInitializationContext context) + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var generated = context + .AdditionalTextsProvider + .Where(static file => file.Path.EndsWith(".json")) + .Select(static (file, cancellationToken) => + { + var parser = GetParserForFile(file); + + var name = Path.GetFileNameWithoutExtension(file.Path); + + return (parser.Parse(file, cancellationToken), name); + }); + + context.RegisterSourceOutput(generated, static (context, pair) => + { + var (source, name) = pair; + + try + { + context.AddSource(name, source); + } + catch (Exception exception) + { + context.AddSource($"{name}.g.cs", $"// An exception was thrown processing {name}.json\n{exception.ToString()}"); + context.ReportDiagnostic( + Diagnostic.Create( + new DiagnosticDescriptor( + id: "HDJSON", // Unique ID for your error + title: "JSON source generator failed", // Title of the error + messageFormat: $"An error occured generating C# code from JSON files: {exception}", // Message format + category: "HD2", // Category of the error + DiagnosticSeverity.Error, // Severity of the error + isEnabledByDefault: true // Whether the error is enabled by default + ), + Location.None // No specific location provided for simplicity + ) + ); + } + }); + } + + private static IJsonParser GetParserForFile(AdditionalText file) { - // No initialization required for this generator. + var name = Path.GetFileNameWithoutExtension(file.Path); + name = $"{char.ToUpper(name[0])}{name.Substring(1)}"; + + return name.ToLowerInvariant() switch + { + "planets" => PlanetParser, + "biomes" => BiomesParser, + "environmentals" => EnvironmentalsParser, + "factions" => FactionsParser, + _ => throw new Exception($"Generator does not know how to parse {name}") + }; } + + +#if false /// public void Execute(GeneratorExecutionContext context) { @@ -76,69 +136,5 @@ public static partial class Static } } } - - /// - /// Parses a JSON file that's an object where keys are numerical and values are names (strings). - /// - private (string Type, string Source) ParseFactionsDictionary(string json) - { - var builder = new StringBuilder("new Dictionary()\n\t{\n"); - var entries = JsonSerializer.Deserialize>(json)!; - foreach (var pair in entries) - builder.AppendLine($@"{'\t'}{'\t'}{{ {pair.Key}, ""{pair.Value}"" }},"); - - builder.Append("\t}"); - return ("IReadOnlyDictionary", builder.ToString()); - } - - private (string Type, string Source) ParsePlanetsDictionary(string json) - { - var builder = new StringBuilder("new Dictionary Environmentals)>()\n\t{\n"); - var document = JsonDocument.Parse(json); - foreach (var property in document.RootElement.EnumerateObject()) - { - var index = property.Name; - var name = property.Value.GetProperty("name").GetString(); - var names = property - .Value - .GetProperty("names") - .EnumerateObject() - .ToDictionary(prop => prop.Name, prop => prop.Value.GetString()!); - var sector = property.Value.GetProperty("sector").GetString(); - var biome = property.Value.GetProperty("biome").GetString(); - var environmentals = property - .Value - .GetProperty("environmentals") - .EnumerateArray() - .Select(prop => $@"""{prop.GetString()!}""") - .ToList(); - - builder.AppendLine($@"{'\t'}{'\t'}{{ {index}, (LocalizedMessage.FromStrings([{string.Join(", ", names.Select(pair => $@"new KeyValuePair(""{pair.Key}"", ""{pair.Value}"")"))}]), ""{sector}"", ""{biome}"", [{string.Join(", ", environmentals)}]) }},"); - } - - builder.Append("\t}"); - return ("IReadOnlyDictionary Environmentals)>", builder.ToString()); - } - - private (string Type, string Source) ParseBiomesDictionary(string json) - { - var builder = new StringBuilder("new Dictionary()\n\t{\n"); - var entries = JsonSerializer.Deserialize>>(json)!; - foreach (var pair in entries) - builder.AppendLine($@"{'\t'}{'\t'}{{ ""{pair.Key}"", new Helldivers.Models.V1.Planets.Biome(""{pair.Value["name"]}"", ""{pair.Value["description"]}"") }},"); - - builder.Append("\t}"); - return ("IReadOnlyDictionary", builder.ToString()); - } - - private (string Type, string Source) ParseEnvironmentalsDictionary(string json) - { - var builder = new StringBuilder("new Dictionary()\n\t{\n"); - var entries = JsonSerializer.Deserialize>>(json)!; - foreach (var pair in entries) - builder.AppendLine($@"{'\t'}{'\t'}{{ ""{pair.Key}"", new Helldivers.Models.V1.Planets.Hazard(""{pair.Value["name"]}"", ""{pair.Value["description"]}"") }},"); - - builder.Append("\t}"); - return ("IReadOnlyDictionary", builder.ToString()); - } +#endif } diff --git a/src/Helldivers-2-Sync/Helldivers-2-Sync.csproj b/src/Helldivers-2-Sync/Helldivers-2-Sync.csproj index 67c2d7c..2016b56 100644 --- a/src/Helldivers-2-Sync/Helldivers-2-Sync.csproj +++ b/src/Helldivers-2-Sync/Helldivers-2-Sync.csproj @@ -6,10 +6,10 @@ - - - - + + + +