├── docs ├── .nojekyll ├── _navbar.md ├── openapi │ ├── redoc.html │ └── swagger-ui.html ├── _coverpage.md └── index.html ├── toc.yml ├── .github ├── FUNDING.yml ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── bug_report.yml └── workflows │ ├── fly.yml │ ├── dotnet.yml │ ├── pages.yml │ ├── sync.yml │ └── codeql.yml ├── .gitmodules ├── src ├── Helldivers-2-Models │ ├── ArrowHead │ │ ├── WarId.cs │ │ ├── Info │ │ │ ├── PlanetCoordinates.cs │ │ │ ├── HomeWorld.cs │ │ │ ├── PlanetRegion.cs │ │ │ └── PlanetInfo.cs │ │ ├── Status │ │ │ ├── JointOperation.cs │ │ │ ├── PlanetAttack.cs │ │ │ ├── SpaceStation.cs │ │ │ ├── Campaign.cs │ │ │ ├── PlanetStatus.cs │ │ │ ├── PlanetRegionStatus.cs │ │ │ └── PlanetEvent.cs │ │ ├── Assignments │ │ │ ├── Reward.cs │ │ │ ├── Task.cs │ │ │ └── Setting.cs │ │ ├── NewsFeedItem.cs │ │ ├── SpaceStations │ │ │ ├── Cost.cs │ │ │ └── TacticalAction.cs │ │ ├── Assignment.cs │ │ ├── WarSummary.cs │ │ ├── SpaceStation.cs │ │ ├── WarInfo.cs │ │ ├── WarStatus.cs │ │ └── Summary │ │ │ ├── GalaxyStats.cs │ │ │ └── PlanetStats.cs │ ├── Steam │ │ ├── SteamNewsFeed.cs │ │ ├── SteamAppNews.cs │ │ └── SteamNewsFeedItem.cs │ ├── V1 │ │ ├── Planets │ │ │ ├── Position.cs │ │ │ ├── Biome.cs │ │ │ ├── RegionSize.cs │ │ │ ├── Hazard.cs │ │ │ ├── Event.cs │ │ │ └── Region.cs │ │ ├── Assignments │ │ │ ├── Reward.cs │ │ │ └── Task.cs │ │ ├── Dispatch.cs │ │ ├── Campaign.cs │ │ ├── SteamNews.cs │ │ ├── War.cs │ │ ├── Assignment.cs │ │ ├── Statistics.cs │ │ └── Planet.cs │ ├── V2 │ │ ├── SpaceStations │ │ │ ├── TacticalAction.cs │ │ │ └── Cost.cs │ │ ├── Dispatch.cs │ │ └── SpaceStation.cs │ ├── SteamSerializerContext.cs │ ├── V2SerializerContext.cs │ ├── ArrowHeadSerializerContext.cs │ ├── Helldivers-2-Models.csproj │ ├── V1SerializerContext.cs │ └── Domain │ │ └── Localization │ │ ├── LocalizedMessageConverter.cs │ │ └── LocalizedMessage.cs ├── Helldivers-2-Core │ ├── Storage │ │ ├── V1 │ │ │ ├── WarStore.cs │ │ │ ├── PlanetStore.cs │ │ │ ├── CampaignStore.cs │ │ │ ├── DispatchStore.cs │ │ │ └── AssignmentStore.cs │ │ ├── V2 │ │ │ ├── DispatchStore.cs │ │ │ └── SpaceStationStore.cs │ │ ├── Steam │ │ │ └── SteamNewsStore.cs │ │ ├── StoreBase.cs │ │ └── ArrowHead │ │ │ └── ArrowHeadStore.cs │ ├── Helldivers-2-Core.csproj │ ├── Facades │ │ ├── SteamFacade.cs │ │ ├── V2Facade.cs │ │ └── V1Facade.cs │ ├── Mapping │ │ ├── Steam │ │ │ └── SteamNewsMapper.cs │ │ ├── V1 │ │ │ ├── WarMapper.cs │ │ │ ├── CampaignMapper.cs │ │ │ ├── DispatchMapper.cs │ │ │ ├── StatisticsMapper.cs │ │ │ └── AssignmentMapper.cs │ │ ├── V2 │ │ │ ├── DispatchMapper.cs │ │ │ └── SpaceStationMapper.cs │ │ └── MappingContext.cs │ ├── Contracts │ │ ├── IStore.cs │ │ └── Collections │ │ │ └── IStore.cs │ ├── Localization │ │ └── CultureDictionary.cs │ ├── StorageFacade.cs │ └── Extensions │ │ └── ServiceCollectionExtensions.cs ├── Helldivers-2-SourceGen │ ├── Properties │ │ └── launchSettings.json │ ├── Contracts │ │ └── IJsonParser.cs │ ├── Parsers │ │ ├── FactionsParser.cs │ │ ├── BiomesParser.cs │ │ ├── EnvironmentalsParser.cs │ │ ├── PlanetRegionsParser.cs │ │ ├── PlanetsParser.cs │ │ └── BaseJsonParser.cs │ ├── Readme.md │ ├── Helldivers-2-SourceGen.csproj │ └── StaticJsonSourceGenerator.cs ├── Helldivers-2-API │ ├── Properties │ │ └── launchSettings.json │ ├── Constants.cs │ ├── Controllers │ │ ├── V1 │ │ │ ├── WarController.cs │ │ │ ├── CampaignsController.cs │ │ │ ├── DispatchController.cs │ │ │ ├── AssignmentsController.cs │ │ │ ├── SteamController.cs │ │ │ └── PlanetController.cs │ │ ├── V2 │ │ │ ├── DispatchController.cs │ │ │ └── SpaceStationController.cs │ │ ├── DevelopmentController.cs │ │ └── ArrowHeadController.cs │ ├── appsettings.Development.json │ ├── Middlewares │ │ ├── RedirectFlyDomainMiddleware.cs │ │ ├── BlacklistMiddleware.cs │ │ ├── EtagMiddleware.cs │ │ └── RateLimitMiddleware.cs │ ├── Configuration │ │ ├── AuthenticationConfiguration.cs │ │ └── ApiConfiguration.cs │ ├── appsettings.json │ ├── Extensions │ │ └── ClaimsPrincipalExtensions.cs │ ├── OpenApi │ │ ├── TypeMappers │ │ │ ├── EnumStringTypeMapper.cs │ │ │ └── LocalizedMessageTypeMapper.cs │ │ ├── DocumentProcessors │ │ │ ├── HelldiversDocumentProcessor.cs │ │ │ └── ArrowHeadDocumentProcessor.cs │ │ └── OperationProcessors │ │ │ └── SuperHeadersProcessor.cs │ ├── Dockerfile │ ├── Helldivers-2-API.csproj │ └── Metrics │ │ └── ClientMetric.cs ├── Helldivers-2-CI │ ├── Helldivers-2-CI.csproj │ └── Program.cs └── Helldivers-2-Sync │ ├── Helldivers-2-Sync.csproj │ ├── Services │ └── SteamApiService.cs │ ├── Extensions │ └── ServiceCollectionExtensions.cs │ ├── Configuration │ └── HelldiversSyncConfiguration.cs │ └── Hosted │ └── SteamSyncService.cs ├── index.md ├── Directory.Build.props ├── .dockerignore ├── SECURITY.md ├── LICENSE ├── fly.toml ├── .gitattributes └── README.md /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /toc.yml: -------------------------------------------------------------------------------- 1 | - name: Docs 2 | href: docs/ 3 | - name: OpenAPI 4 | href: docs/openapi/swagger-ui.html 5 | - name: API 6 | href: api/ 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: dealloc 4 | patreon: Helldivers2API 5 | buy_me_a_coffee: dealloc 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/Helldivers-2-Models/json"] 2 | path = src/Helldivers-2-Models/json 3 | url = https://github.com/helldivers-2/json.git 4 | -------------------------------------------------------------------------------- /docs/_navbar.md: -------------------------------------------------------------------------------- 1 | * [Self host](containers.md) 2 | * OpenAPI 3 | * [Redoc](openapi/redoc.html ':ignore') 4 | * [Swagger UI](openapi/swagger-ui.html ':ignore') 5 | -------------------------------------------------------------------------------- /docs/openapi/redoc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Defines owners of specific parts of the codebase. 2 | 3 | # Global owner of all code 4 | * @dealloc 5 | 6 | # All documentation should be approved by following owners 7 | *.md @chatterchats 8 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHead/WarId.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.ArrowHead; 2 | 3 | /// 4 | /// Represents the ID returned from the WarID endpoint. 5 | /// 6 | public sealed record WarId(int Id); 7 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Storage/V1/WarStore.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Contracts; 2 | using Helldivers.Models.V1; 3 | 4 | namespace Helldivers.Core.Storage.V1; 5 | 6 | /// 7 | public sealed class WarStore : StoreBase 8 | { 9 | // 10 | } 11 | -------------------------------------------------------------------------------- /index.md: -------------------------------------------------------------------------------- 1 | --- 2 | _layout: landing 3 | --- 4 | 5 | # This is the **HOMEPAGE**. 6 | 7 | Refer to [Markdown](http://daringfireball.net/projects/markdown/) for how to write markdown files. 8 | 9 | ## Quick Start Notes: 10 | 11 | 1. Add images to the *images* folder if the file is referencing an image. 12 | -------------------------------------------------------------------------------- /src/Helldivers-2-SourceGen/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "DebugRoslynSourceGenerator": { 5 | "commandName": "DebugRoslynComponent", 6 | "targetProject": "../Helldivers_2_SourceGen.Sample/Helldivers_2_SourceGen.Sample.csproj" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /src/Helldivers-2-Models/Steam/SteamNewsFeed.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.Steam; 2 | 3 | /// 4 | /// Represents the response of the Steam news feed API call. 5 | /// 6 | /// Contains the object. 7 | public sealed record SteamNewsFeed( 8 | SteamAppNews AppNews 9 | ); 10 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/V1/Planets/Position.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.V1; 2 | 3 | /// 4 | /// Represents a position on the galactic war map. 5 | /// 6 | /// The X coordinate. 7 | /// The Y coordinate. 8 | public record struct Position( 9 | double X, 10 | double Y 11 | ); 12 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHead/Info/PlanetCoordinates.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.ArrowHead.Info; 2 | 3 | /// 4 | /// Represents a set of coordinates returned by ArrowHead's API. 5 | /// 6 | /// The X coordinate 7 | /// The Y coordinate 8 | public sealed record PlanetCoordinates(double X, double Y); 9 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Storage/V1/PlanetStore.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Contracts.Collections; 2 | using Helldivers.Models.V1; 3 | 4 | namespace Helldivers.Core.Storage.V1; 5 | 6 | /// 7 | public class PlanetStore : StoreBase 8 | { 9 | /// 10 | protected override bool GetAsyncPredicate(Planet planet, int index) => planet.Index == index; 11 | } 12 | -------------------------------------------------------------------------------- /docs/_coverpage.md: -------------------------------------------------------------------------------- 1 | ![logo](https://avatars.githubusercontent.com/u/164774207?s=200&v=4) 2 | 3 | # Helldivers 2API 4 | 5 | > A community driven API wrapper for Helldivers 2 6 | 7 | - Provides fast & cached access to the official Helldivers 2 API 8 | - Convenience abstractions for modern API design 9 | - You can self host! 10 | 11 | [GitHub](https://github.com/helldivers-2/api) 12 | [Get Started](README.md) -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHead/Status/JointOperation.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.ArrowHead.Status; 2 | 3 | /// 4 | /// Represents a joint operation. 5 | /// 6 | /// 7 | /// 8 | /// 9 | public sealed record JointOperation( 10 | int Id, 11 | int PlanetIndex, 12 | int HqNodeIndex 13 | ); 14 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | enable 4 | enable 5 | net10.0 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/Steam/SteamAppNews.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.Steam; 2 | 3 | /// 4 | /// Represents the response of the Steam news feed API call. 5 | /// 6 | /// The appid of Helldivers 2 on Steam. 7 | /// A list of newsfeed items. 8 | public sealed record SteamAppNews( 9 | long AppId, 10 | List NewsItems 11 | ); 12 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Storage/V1/CampaignStore.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Contracts.Collections; 2 | using Helldivers.Models.V1; 3 | 4 | namespace Helldivers.Core.Storage.V1; 5 | 6 | /// 7 | public sealed class CampaignStore : StoreBase 8 | { 9 | /// 10 | protected override bool GetAsyncPredicate(Campaign campaign, int key) 11 | => campaign.Id == key; 12 | } 13 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Storage/V1/DispatchStore.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Contracts.Collections; 2 | using Helldivers.Models.V1; 3 | 4 | namespace Helldivers.Core.Storage.V1; 5 | 6 | /// 7 | public sealed class DispatchStore : StoreBase 8 | { 9 | /// 10 | protected override bool GetAsyncPredicate(Dispatch dispatch, int key) 11 | => dispatch.Id == key; 12 | } 13 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Storage/V2/DispatchStore.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Contracts.Collections; 2 | using Helldivers.Models.V2; 3 | 4 | namespace Helldivers.Core.Storage.V2; 5 | 6 | /// 7 | public sealed class DispatchStore : StoreBase 8 | { 9 | /// 10 | protected override bool GetAsyncPredicate(Dispatch dispatch, int key) 11 | => dispatch.Id == key; 12 | } 13 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.dockerignore 2 | **/.env 3 | **/.git 4 | **/.gitignore 5 | **/.project 6 | **/.settings 7 | **/.toolstarget 8 | **/.vs 9 | **/.vscode 10 | **/.idea 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md 26 | tests/ 27 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Storage/V2/SpaceStationStore.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Contracts.Collections; 2 | using Helldivers.Models.V2; 3 | 4 | namespace Helldivers.Core.Storage.V2; 5 | 6 | /// 7 | public sealed class SpaceStationStore : StoreBase 8 | { 9 | /// 10 | protected override bool GetAsyncPredicate(SpaceStation station, long index) => station.Id32 == index; 11 | } 12 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Storage/V1/AssignmentStore.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Contracts.Collections; 2 | using Helldivers.Models.V1; 3 | 4 | namespace Helldivers.Core.Storage.V1; 5 | 6 | /// 7 | public sealed class AssignmentStore : StoreBase 8 | { 9 | /// 10 | protected override bool GetAsyncPredicate(Assignment assignment, long key) 11 | => assignment.Id == key; 12 | } 13 | -------------------------------------------------------------------------------- /docs/openapi/swagger-ui.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 12 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHead/Info/HomeWorld.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.ArrowHead.Info; 2 | 3 | /// 4 | /// Represents information about the homeworld(s) of a given race. 5 | /// 6 | /// The identifier of the race (faction) this describes the homeworld of. 7 | /// A list of identifiers. 8 | public sealed record HomeWorld(int Race, List PlanetIndices); 9 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "http": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "launchUrl": "", 9 | "applicationUrl": "http://localhost:5000", 10 | "environmentVariables": { 11 | "ASPNETCORE_ENVIRONMENT": "Development" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHead/Status/PlanetAttack.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models.ArrowHead.Info; 2 | 3 | namespace Helldivers.Models.ArrowHead.Status; 4 | 5 | /// 6 | /// Represents an attack on a . 7 | /// 8 | /// Where the attack originates from. 9 | /// The planet under attack. 10 | public sealed record PlanetAttack( 11 | int Source, 12 | int Target 13 | ); 14 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/V1/Assignments/Reward.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.V1.Assignments; 2 | 3 | /// 4 | /// The reward for completing an . 5 | /// 6 | /// The type of reward (medals, super credits, ...). 7 | /// The amount of that will be awarded. 8 | public sealed record Reward( 9 | int Type, // TODO: map to enum 10 | ulong Amount 11 | ); 12 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Storage/Steam/SteamNewsStore.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Contracts.Collections; 2 | using Helldivers.Models.V1; 3 | 4 | namespace Helldivers.Core.Storage.Steam; 5 | 6 | /// 7 | public class SteamNewsStore : StoreBase 8 | { 9 | /// 10 | protected override bool GetAsyncPredicate(SteamNews item, string key) 11 | => string.Equals(item.Id, key, StringComparison.InvariantCultureIgnoreCase); 12 | } 13 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/V2/SpaceStations/TacticalAction.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.V2.SpaceStations; 2 | 3 | /// 4 | /// Represents a tactical action that the Space Station can take. 5 | /// 6 | public sealed record TacticalAction( 7 | long Id32, 8 | long MediaId32, 9 | string Name, 10 | string Description, 11 | string StrategicDescription, 12 | int Status, 13 | DateTime StatusExpire, 14 | List Costs, 15 | List EffectIds 16 | ); 17 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Helldivers-2-Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | Helldivers.Core 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Discord Chat 4 | url: https://discord.gg/E8UUfWYmf9 5 | about: Ask questions and discuss with other developers in real time. 6 | - name: Questions & Discussions 7 | url: https://github.com/helldivers-2/api/discussions 8 | about: Use GitHub discussions for message-board style questions and discussions. 9 | - name: Buy me a coffee 10 | url: https://www.buymeacoffee.com/dealloc 11 | about: If you want to support the development, check out buymeacoffee 12 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/SteamSerializerContext.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models.Steam; 2 | using Helldivers.Models.V1; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Helldivers.Models; 6 | 7 | /// 8 | /// Source generated for Steam models. 9 | /// 10 | [JsonSerializable(typeof(SteamNewsFeed))] 11 | [JsonSerializable(typeof(List))] 12 | [JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)] 13 | public sealed partial class SteamSerializerContext : JsonSerializerContext 14 | { 15 | // 16 | } 17 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.API; 2 | 3 | /// 4 | /// Contains constant strings used througout the API. 5 | /// 6 | public static class Constants 7 | { 8 | /// 9 | /// The name of the header that identifies the client to the API. 10 | /// 11 | public static string CLIENT_HEADER_NAME = "X-Super-Client"; 12 | 13 | /// 14 | /// The name of the header with developer contact information. 15 | /// 16 | public static string CONTACT_HEADER_NAME = "X-Super-Contact"; 17 | } 18 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/V1/Assignments/Task.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.V1.Assignments; 2 | 3 | /// 4 | /// Represents a task in an that needs to be completed 5 | /// to finish the assignment. 6 | /// 7 | /// The type of task this represents. 8 | /// A list of numbers, purpose unknown. 9 | /// A list of numbers, purpose unknown 10 | public sealed record Task( 11 | int Type, 12 | List Values, 13 | List ValueTypes 14 | ); 15 | -------------------------------------------------------------------------------- /src/Helldivers-2-CI/Helldivers-2-CI.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net10.0 6 | Helldivers_2_CI 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/V1/Planets/Biome.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.V1.Planets; 2 | 3 | /// 4 | /// Represents information about a biome of a planet. 5 | /// 6 | /// The name of this biome. 7 | /// A human-readable description of the biome. 8 | public sealed record Biome( 9 | string Name, 10 | string Description 11 | ) 12 | { 13 | /// 14 | /// Used when the biome could not be determined. 15 | /// 16 | public static readonly Biome Unknown = new("UNKNOWN BIOME", string.Empty); 17 | }; 18 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHead/Assignments/Reward.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.ArrowHead.Assignments; 2 | 3 | /// 4 | /// Represents the reward of an . 5 | /// 6 | /// The type of reward, currently only one value is known: 1 which represents Medals 7 | /// Internal identifier of this . 8 | /// The amount of the players will receive upon completion. 9 | public sealed record Reward( 10 | int Type, 11 | ulong Id32, 12 | ulong Amount 13 | ); 14 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHead/Assignments/Task.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.ArrowHead.Assignments; 2 | 3 | /// 4 | /// Represents a task in an . 5 | /// It's exact values are not known, therefore little of it's purpose is clear. 6 | /// 7 | /// A numerical value, purpose unknown. 8 | /// A list of numerical values, purpose unknown. 9 | /// A list of numerical values, purpose unknown. 10 | public sealed record Task( 11 | int Type, 12 | List Values, 13 | List ValueTypes 14 | ); 15 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHead/NewsFeedItem.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.ArrowHead; 2 | 3 | /// 4 | /// Represents an item in the newsfeed of Super Earth. 5 | /// 6 | /// The identifier of this newsfeed item. 7 | /// A unix timestamp (in seconds) when this item was published. 8 | /// A numerical type, purpose unknown. 9 | /// The message containing a human readable text. 10 | public sealed record NewsFeedItem( 11 | int Id, 12 | long Published, 13 | int Type, 14 | string Message 15 | ); 16 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting security issues 2 | We take security very seriously and appreciate your efforts in responsibly disclosing your findings. 3 | We will make every effort to acknowledge your contributions to the safety of the API. 4 | 5 | To report a security issue related to either ArrowHead's API or the unofficial community wrapper, please use 6 | Github Security Advisory ["Report a Vulnerability"](https://github.com/helldivers-2/api/security/advisories/new) tab. 7 | 8 | This will notify the relevant maintainers and action will be taken as soon as possible. 9 | We will also try to acknowledge receiving your report as soon as possible. 10 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/V1/Planets/RegionSize.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.V1.Planets; 2 | 3 | /// 4 | /// Indicates what size a has. 5 | /// 6 | public enum RegionSize 7 | { 8 | /// 9 | /// The region is a settlement. 10 | /// 11 | Settlement = 0, 12 | /// 13 | /// The region is a town. 14 | /// 15 | Town = 1, 16 | /// 17 | /// The region is a city. 18 | /// 19 | City = 2, 20 | /// 21 | /// The region is a megacity 22 | /// 23 | MegaCity = 3, 24 | } 25 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/V2SerializerContext.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models.V2; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Helldivers.Models; 5 | 6 | /// 7 | /// Source generated for V2's models. 8 | /// 9 | [JsonSerializable(typeof(Dispatch))] 10 | [JsonSerializable(typeof(List))] 11 | [JsonSerializable(typeof(SpaceStation))] 12 | [JsonSerializable(typeof(List))] 13 | [JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true, UseStringEnumConverter = true)] 14 | public sealed partial class V2SerializerContext : JsonSerializerContext 15 | { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/V1/Planets/Hazard.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.V1.Planets; 2 | 3 | /// 4 | /// Describes an environmental hazard that can be present on a . 5 | /// 6 | /// The name of this environmental hazard. 7 | /// The description of the environmental hazard. 8 | public sealed record Hazard( 9 | string Name, 10 | string Description 11 | ) 12 | { 13 | /// 14 | /// Used when the hazard could not be determined. 15 | /// 16 | public static readonly Hazard Unknown = new("-UNKNOWN HAZARD-", string.Empty); 17 | }; 18 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/V1/Dispatch.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models.Domain.Localization; 2 | 3 | namespace Helldivers.Models.V1; 4 | 5 | /// 6 | /// A message from high command to the players, usually updates on the status of the war effort. 7 | /// 8 | /// The unique identifier of this dispatch. 9 | /// When the dispatch was published. 10 | /// The type of dispatch, purpose unknown. 11 | /// The message this dispatch represents. 12 | public sealed record Dispatch( 13 | int Id, 14 | DateTime Published, 15 | int Type, 16 | LocalizedMessage Message 17 | ); 18 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/V2/Dispatch.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models.Domain.Localization; 2 | 3 | namespace Helldivers.Models.V2; 4 | 5 | /// 6 | /// A message from high command to the players, usually updates on the status of the war effort. 7 | /// 8 | /// The unique identifier of this dispatch. 9 | /// When the dispatch was published. 10 | /// The type of dispatch, purpose unknown. 11 | /// The message this dispatch represents. 12 | public sealed record Dispatch( 13 | int Id, 14 | DateTime Published, 15 | int Type, 16 | LocalizedMessage Message 17 | ); 18 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/Controllers/V1/WarController.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Contracts; 2 | using Helldivers.Models.V1; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace Helldivers.API.Controllers.V1; 6 | 7 | /// 8 | /// Contains API endpoints for . 9 | /// 10 | public static class WarController 11 | { 12 | /// 13 | /// Gets the current state. 14 | /// 15 | [ProducesResponseType(StatusCodes.Status200OK)] 16 | public static async Task Show(HttpContext context, IStore store) 17 | { 18 | var war = await store.Get(context.RequestAborted); 19 | 20 | return Results.Ok(war); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/V2/SpaceStations/Cost.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.V2.SpaceStations; 2 | 3 | /// 4 | /// Represents the "Cost" of a tactical action 5 | /// 6 | /// 7 | /// 8 | /// 9 | /// 10 | /// 11 | /// 12 | /// 13 | public sealed record Cost( 14 | string Id, 15 | long ItemMixId, 16 | long TargetValue, 17 | double CurrentValue, 18 | double DeltaPerSecond, 19 | long MaxDonationAmmount, 20 | long MaxDonationPeriodSeconds 21 | ); 22 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHead/SpaceStations/Cost.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.ArrowHead.SpaceStations; 2 | 3 | /// 4 | /// Represents the "Cost" of a tactical action 5 | /// 6 | /// 7 | /// 8 | /// 9 | /// 10 | /// 11 | /// 12 | /// 13 | public sealed record Cost( 14 | string Id, 15 | long ItemMixId, 16 | long TargetValue, 17 | double CurrentValue, 18 | double DeltaPerSecond, 19 | long MaxDonationAmmount, 20 | long MaxDonationPeriodSeconds 21 | ); 22 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHead/Info/PlanetRegion.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.ArrowHead.Info; 2 | 3 | /// 4 | /// A region of a planet, containing information about its health and size. 5 | /// 6 | /// The index of the region's planet 7 | /// The index of the region 8 | /// The ID that identifies the region in the JSON files. 9 | /// The maximum health of this region. 10 | /// The size of the region. 11 | public sealed record PlanetRegion( 12 | int PlanetIndex, 13 | int RegionIndex, 14 | ulong SettingsHash, 15 | ulong MaxHealth, 16 | int RegionSize 17 | ); 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest new features for the API 4 | title: '[FEATURE]: ' 5 | labels: ['feature request'] 6 | --- 7 | 8 | ### What problem does it solve 9 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 10 | 11 | ### What is the solution 12 | A clear and concise description of what you want to happen. 13 | 14 | ### Existing alternatives 15 | A clear and concise description of any alternative solutions or features you've considered. 16 | 17 | ### Added value 18 | A clear description of the value this feature would add to the community and applications. 19 | 20 | ### Additional notes 21 | Add any other context or screenshots about the feature request here. 22 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHead/Assignment.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models.ArrowHead.Assignments; 2 | 3 | namespace Helldivers.Models.ArrowHead; 4 | 5 | /// 6 | /// Represents an assignment given from Super Earth to the Helldivers. 7 | /// 8 | /// Internal identifier of this assignment. 9 | /// A list of numbers, how they represent progress is unknown. 10 | /// The amount of seconds until this assignment expires. 11 | /// Contains detailed information on this assignment like briefing, rewards, ... 12 | public sealed record Assignment( 13 | long Id32, 14 | List Progress, 15 | long ExpiresIn, 16 | Setting Setting 17 | ); 18 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHead/Status/SpaceStation.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.ArrowHead.Status; 2 | 3 | /// 4 | /// Represents one of the space stations as passed from the ArrowHead API. 5 | /// 6 | /// The unique identifier of the station. 7 | /// The id of the planet it's currently orbiting 8 | /// When the election for the next planet will end (in seconds relative to game start). 9 | /// A set of flags, purpose currently unknown. 10 | public sealed record SpaceStation( 11 | long Id32, 12 | int PlanetIndex, 13 | // TODO PlanetActiveEffects 14 | ulong CurrentElectionEndWarTime, 15 | int Flags 16 | ); 17 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHead/WarSummary.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models.ArrowHead.Summary; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Helldivers.Models.ArrowHead; 5 | 6 | /// 7 | /// Gets general statistics about the galaxy and specific planets. 8 | /// 9 | public sealed record WarSummary 10 | { 11 | /// 12 | /// Contains galaxy wide statistics aggregated from all planets. 13 | /// 14 | [JsonPropertyName("galaxy_stats")] 15 | public GalaxyStats GalaxyStats { get; set; } = null!; 16 | 17 | /// 18 | /// Contains statistics for specific planets. 19 | /// 20 | [JsonPropertyName("planets_stats")] 21 | public List PlanetsStats { get; set; } = null!; 22 | } 23 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Facades/SteamFacade.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Contracts.Collections; 2 | using Helldivers.Core.Mapping.Steam; 3 | using Helldivers.Core.Mapping.V1; 4 | using Helldivers.Models.Steam; 5 | using Helldivers.Models.V1; 6 | 7 | namespace Helldivers.Core.Facades; 8 | 9 | /// 10 | /// Handles dispatching incoming data to all stores for Steam. 11 | /// 12 | public sealed class SteamFacade(IStore store, SteamNewsMapper mapper) 13 | { 14 | /// 15 | public async ValueTask UpdateStores(SteamNewsFeed feed) 16 | { 17 | if (feed is { AppNews.NewsItems: var items }) 18 | { 19 | await store.SetStore(items.Select(mapper.MapToDomain).ToList()); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/V1/Campaign.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.V1; 2 | 3 | /// 4 | /// Represents an ongoing campaign on a planet. 5 | /// 6 | /// The unique identifier of this . 7 | /// The planet on which this campaign is being fought. 8 | /// The type of campaign, this should be mapped onto an enum. 9 | /// Indicates how many campaigns have already been fought on this . 10 | /// The faction that is currently fighting this campaign. 11 | public record Campaign( 12 | int Id, 13 | Planet Planet, 14 | int Type, // TODO: map to enum 15 | ulong Count, 16 | string Faction 17 | ); 18 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/Steam/SteamNewsFeedItem.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.Steam; 2 | 3 | /// 4 | /// Represents a single item in the Steam news feed response. 5 | /// 6 | /// A unique identifier assigned by Steam. 7 | /// The title of the news article. 8 | /// The URL to the full article on Steam 9 | /// The name of the author that released the news item. 10 | /// The contents of the news item. 11 | /// When the news item was published. 12 | public sealed record SteamNewsFeedItem( 13 | string Gid, 14 | string Title, 15 | string Url, 16 | string Author, 17 | string Contents, 18 | long Date 19 | ); 20 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Helldivers": "Trace", 6 | "Microsoft.AspNetCore": "Warning", 7 | "System.Net.Http.HttpClient.ApiService": "Warning", 8 | "Microsoft.AspNetCore.Authentication": "Warning" 9 | } 10 | }, 11 | "Helldivers": { 12 | "API": { 13 | "RateLimit": 500, 14 | "RateLimitWindow": 10, 15 | "ValidateClients": false, 16 | "Authentication": { 17 | "SigningKey": "I4eGmsXbDXfxlRo5N+w0ToRGN8aWSIaYWbZ2zMFqqnI=" 18 | } 19 | }, 20 | "Synchronization": { 21 | "IntervalSeconds": 300, 22 | "DefaultLanguage": "en-US", 23 | "Languages": [ 24 | "en-US", 25 | "de-DE" 26 | ] 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHead/Status/Campaign.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.ArrowHead.Status; 2 | 3 | /// 4 | /// Contains information of ongoing campaigns. 5 | /// 6 | /// The identifier of this campaign. 7 | /// The of the planet this campaign refers to. 8 | /// A numerical type, indicates the type of campaign (see helldivers-2/json). 9 | /// A numerical count, the amount of campaigns the planet has seen. 10 | /// A numerical race, the race of the planet this campaign refers to. 11 | public sealed record Campaign( 12 | int Id, 13 | int PlanetIndex, 14 | int Type, 15 | ulong Count, 16 | int Race 17 | ); 18 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHeadSerializerContext.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models.ArrowHead; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Helldivers.Models; 5 | 6 | /// 7 | /// Source generated for ArrowHead models. 8 | /// 9 | [JsonSerializable(typeof(WarId))] 10 | [JsonSerializable(typeof(WarInfo))] 11 | [JsonSerializable(typeof(WarStatus))] 12 | [JsonSerializable(typeof(NewsFeedItem))] 13 | [JsonSerializable(typeof(WarSummary))] 14 | [JsonSerializable(typeof(List))] 15 | [JsonSerializable(typeof(List))] 16 | [JsonSerializable(typeof(List))] 17 | [JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)] 18 | public sealed partial class ArrowHeadSerializerContext : JsonSerializerContext 19 | { 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Mapping/Steam/SteamNewsMapper.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models.Steam; 2 | using Helldivers.Models.V1; 3 | 4 | namespace Helldivers.Core.Mapping.Steam; 5 | 6 | /// 7 | /// Maps from Steam's models. 8 | /// 9 | public sealed class SteamNewsMapper 10 | { 11 | /// 12 | /// Maps information from Steam's API into a V1 . 13 | /// 14 | public SteamNews MapToDomain(SteamNewsFeedItem item) 15 | { 16 | return new SteamNews( 17 | Id: item.Gid, 18 | Title: item.Title, 19 | Url: item.Url, 20 | Author: item.Author, 21 | Content: item.Contents, 22 | PublishedAt: DateTime.UnixEpoch.AddSeconds(item.Date) 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/Middlewares/RedirectFlyDomainMiddleware.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.API.Middlewares; 2 | 3 | /// 4 | /// Automatically generates a redirect if a user still uses the deprecated `fly.io` URL. 5 | /// 6 | public class RedirectFlyDomainMiddleware : IMiddleware 7 | { 8 | private const string RedirectDomain = "https://api.helldivers2.dev"; 9 | 10 | /// 11 | public async Task InvokeAsync(HttpContext context, RequestDelegate next) 12 | { 13 | if (context.Request.Host.Host.Equals("helldivers-2-dotnet.fly.dev", StringComparison.InvariantCultureIgnoreCase)) 14 | { 15 | var url = $"{RedirectDomain}{context.Request.Path}"; 16 | 17 | context.Response.Redirect(url, permanent: true); 18 | } 19 | 20 | await next(context); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/V1/SteamNews.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.V1; 2 | 3 | /// 4 | /// Represents a new article from Steam's news feed. 5 | /// 6 | /// The identifier assigned by Steam to this news item. 7 | /// The title of the Steam news item. 8 | /// The URL to Steam where this news item was posted. 9 | /// The author who posted this message on Steam. 10 | /// The message posted by Steam, currently in Steam's weird markdown format. 11 | /// When this message was posted. 12 | public sealed record SteamNews( 13 | string Id, 14 | string Title, 15 | string Url, 16 | string Author, 17 | string Content, 18 | DateTime PublishedAt 19 | ); 20 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/V2/SpaceStation.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models.V1; 2 | using Helldivers.Models.V2.SpaceStations; 3 | 4 | namespace Helldivers.Models.V2; 5 | 6 | /// 7 | /// Represents a Super Earth Democracy Space Station. 8 | /// 9 | /// The unique identifier of the station. 10 | /// The planet it's currently orbiting. 11 | /// When the election for the next planet will end. 12 | /// A set of flags, purpose currently unknown. 13 | /// A list of tactical actions the space station supports. 14 | public sealed record SpaceStation( 15 | long Id32, 16 | Planet Planet, 17 | DateTime ElectionEnd, 18 | int Flags, 19 | List TacticalActions 20 | ); 21 | -------------------------------------------------------------------------------- /src/Helldivers-2-SourceGen/Contracts/IJsonParser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.Text; 3 | 4 | namespace Helldivers.SourceGen.Contracts; 5 | 6 | /// 7 | /// Interface for parsing JSON strings and generating corresponding C# source code. 8 | /// 9 | public interface IJsonParser 10 | { 11 | /// 12 | /// Parses the given source text and generates corresponding C# source code. 13 | /// 14 | /// The source file to be parsed. 15 | /// A for cancelling the parse process. 16 | /// A object representing the generated C# source code. 17 | SourceText Parse(AdditionalText file, CancellationToken cancellationToken = default); 18 | } 19 | -------------------------------------------------------------------------------- /src/Helldivers-2-Sync/Helldivers-2-Sync.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | Helldivers.Sync 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHead/Status/PlanetStatus.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models.ArrowHead.Info; 2 | 3 | namespace Helldivers.Models.ArrowHead.Status; 4 | 5 | /// 6 | /// Represents the 'current' status of a planet in the galactic war. 7 | /// 8 | /// The identifier of the this status refers to. 9 | /// The faction currently owning the planet. 10 | /// The current health / liberation of a planet. 11 | /// If left alone, how much the health of the planet would regenerate. 12 | /// The amount of helldivers currently active on this planet. 13 | public sealed record PlanetStatus( 14 | int Index, 15 | int Owner, 16 | long Health, 17 | double RegenPerSecond, 18 | ulong Players 19 | ); 20 | -------------------------------------------------------------------------------- /src/Helldivers-2-SourceGen/Parsers/FactionsParser.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.Json; 3 | 4 | namespace Helldivers.SourceGen.Parsers; 5 | 6 | /// 7 | /// Parses JSON data of factions and generates the corresponding C# source code representation. 8 | /// 9 | public class FactionsParser : BaseJsonParser 10 | { 11 | /// 12 | protected override (string Type, string Source) Parse(string json) 13 | { 14 | var builder = new StringBuilder("new Dictionary()\n\t{\n"); 15 | var entries = JsonSerializer.Deserialize>(json)!; 16 | foreach (var pair in entries) 17 | builder.AppendLine($@"{'\t'}{'\t'}{{ {pair.Key}, ""{pair.Value}"" }},"); 18 | 19 | builder.Append("\t}"); 20 | return ("IReadOnlyDictionary", builder.ToString()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Contracts/IStore.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace Helldivers.Core.Contracts; 4 | 5 | /// 6 | /// Provides access to the current version of . 7 | /// 8 | /// 9 | /// If multiple variants per exist, the underlying 10 | /// implementation is responsible for selecting the correct variant 11 | /// based on . 12 | /// 13 | public interface IStore where T : class 14 | { 15 | /// 16 | /// Updates the state of the store with the given . 17 | /// 18 | ValueTask SetStore(T value); 19 | 20 | /// 21 | /// Gets the current snapshot of . 22 | /// 23 | Task Get(CancellationToken cancellationToken = default); 24 | } 25 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/Middlewares/BlacklistMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.API.Configuration; 2 | using Helldivers.API.Metrics; 3 | using Microsoft.Extensions.Options; 4 | 5 | namespace Helldivers.API.Middlewares; 6 | 7 | /// 8 | /// Handles closing connections from blacklisted clients that violate ToS. 9 | /// 10 | public sealed class BlacklistMiddleware(IOptions options) : IMiddleware 11 | { 12 | /// 13 | public async Task InvokeAsync(HttpContext context, RequestDelegate next) 14 | { 15 | var client = ClientMetric.GetClientName(context); 16 | if (options.Value.Blacklist.Contains(client, StringComparison.InvariantCultureIgnoreCase)) 17 | { 18 | // don't send response, only wastes more bytes. 19 | context.Abort(); 20 | return; 21 | } 22 | 23 | await next(context); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/Helldivers-2-Models.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | Helldivers.Models 6 | 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Helldivers 2 API 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Helldivers-2-Sync/Services/SteamApiService.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models; 2 | using Helldivers.Models.Steam; 3 | using System.Text.Json; 4 | 5 | namespace Helldivers.Sync.Services; 6 | 7 | /// 8 | /// Handles communication with the Steam API for fetching information. 9 | /// 10 | public sealed class SteamApiService(HttpClient http) 11 | { 12 | /// 13 | /// Fetches the latest from Steam's news feed for Helldivers 2. 14 | /// 15 | public async Task GetLatest(int count = 20) 16 | { 17 | var url = $"https://api.steampowered.com/ISteamNews/GetNewsForApp/v2/?appid=553850&count={count}&feeds=steam_community_announcements"; 18 | await using var response = await http.GetStreamAsync(url); 19 | 20 | var feed = await JsonSerializer.DeserializeAsync( 21 | response, 22 | SteamSerializerContext.Default.SteamNewsFeed 23 | ); 24 | 25 | return feed!; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/Configuration/AuthenticationConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.API.Configuration; 2 | 3 | /// 4 | /// Contains configuration for the authentication functionality of the API. 5 | /// 6 | public sealed class AuthenticationConfiguration 7 | { 8 | /// 9 | /// Whether the API authentication is enabled or disabled. 10 | /// 11 | public bool Enabled { get; set; } = true; 12 | 13 | /// 14 | /// A list of valid issuers of authentication tokens. 15 | /// 16 | public List ValidIssuers { get; set; } = []; 17 | 18 | /// 19 | /// A list of valid audiences for said authentication tokens. 20 | /// 21 | public List ValidAudiences { get; set; } = []; 22 | 23 | /// 24 | /// A string containing a base64 encoded secret used for signing and verifying authentication tokens. 25 | /// 26 | public string SigningKey { get; set; } = null!; 27 | } 28 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHead/SpaceStations/TacticalAction.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.ArrowHead.SpaceStations; 2 | 3 | /// 4 | /// Represents information of a space station from the 'SpaceStation' endpoint returned by ArrowHead's API. 5 | /// 6 | /// 7 | /// 8 | /// 9 | /// 10 | /// 11 | /// 12 | /// 13 | /// 14 | /// 15 | /// 16 | public sealed record TacticalAction( 17 | long Id32, 18 | long MediaId32, 19 | string Name, 20 | string Description, 21 | string StrategicDescription, 22 | int Status, 23 | int StatusExpireAtWarTimeSeconds, 24 | List Cost, 25 | List EffectIds, 26 | List ActiveEffectIds 27 | ); 28 | -------------------------------------------------------------------------------- /src/Helldivers-2-SourceGen/Parsers/BiomesParser.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.Json; 3 | 4 | namespace Helldivers.SourceGen.Parsers; 5 | 6 | /// 7 | /// BiomesParser is responsible for parsing a JSON string representation of biomes. 8 | /// 9 | public class BiomesParser : BaseJsonParser 10 | { 11 | /// 12 | protected override (string Type, string Source) Parse(string json) 13 | { 14 | var builder = new StringBuilder("new Dictionary()\n\t{\n"); 15 | var entries = JsonSerializer.Deserialize>>(json)!; 16 | foreach (var pair in entries) 17 | builder.AppendLine($@"{'\t'}{'\t'}{{ ""{pair.Key}"", new Helldivers.Models.V1.Planets.Biome(""{pair.Value["name"]}"", ""{pair.Value["description"]}"") }},"); 18 | 19 | builder.Append("\t}"); 20 | return ("IReadOnlyDictionary", builder.ToString()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Helldivers": "Information", 6 | "System.Net.Http.HttpClient": "Warning", 7 | "Microsoft.AspNetCore": "Warning" 8 | } 9 | }, 10 | "AllowedHosts": "*", 11 | "Helldivers": { 12 | "API": { 13 | "RateLimit": 5, 14 | "RateLimitWindow": 10, 15 | "Blacklist": "", 16 | "Authentication": { 17 | "Enabled": true, 18 | "ValidIssuers": ["dealloc"], 19 | "ValidAudiences": [ 20 | "https://api.helldivers2.dev" 21 | ] 22 | } 23 | }, 24 | "Synchronization": { 25 | "IntervalSeconds": 20, 26 | "DefaultLanguage": "en-US", 27 | "SpaceStations": [ 28 | 749875195 29 | ], 30 | "Languages": [ 31 | "en-US", 32 | "de-DE", 33 | "es-ES", 34 | "ru-RU", 35 | "fr-FR", 36 | "it-IT", 37 | "pl-PL", 38 | "zh-Hans", 39 | "zh-Hant" 40 | ] 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/V1/War.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.V1; 2 | 3 | /// 4 | /// Global information of the ongoing war. 5 | /// 6 | /// When this war was started. 7 | /// When this war will end (or has ended). 8 | /// The time the snapshot of the war was taken, also doubles as the timestamp of which all other data dates from. 9 | /// The minimum game client version required to play in this war. 10 | /// A list of factions currently involved in the war. 11 | /// A fraction used to calculate the impact of a mission on the war effort. 12 | /// The statistics available for the galaxy wide war effort. 13 | public record War( 14 | DateTime Started, 15 | DateTime Ended, 16 | DateTime Now, 17 | string ClientVersion, 18 | List Factions, 19 | double ImpactMultiplier, 20 | Statistics Statistics 21 | ); 22 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/Extensions/ClaimsPrincipalExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | 3 | namespace Helldivers.API.Extensions; 4 | 5 | /// 6 | /// Contains extension methods for working with s. 7 | /// 8 | public static class ClaimsPrincipalExtensions 9 | { 10 | /// 11 | /// Attempts to retrieve the integer value of a with type . 12 | /// 13 | /// Thrown if the claim could not be found, or is not a valid integer. 14 | public static int GetIntClaim(this ClaimsPrincipal user, string type) 15 | { 16 | var claim = user.Claims.FirstOrDefault(c => 17 | string.Equals(c.Type, type, StringComparison.InvariantCultureIgnoreCase)); 18 | 19 | if (claim is { Value: var str } && int.TryParse(str, out var result)) 20 | return result; 21 | 22 | throw new InvalidOperationException($"Cannot fetch {type} or it is not a valid number"); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Mapping/V1/WarMapper.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models; 2 | using Helldivers.Models.V1; 3 | 4 | namespace Helldivers.Core.Mapping.V1; 5 | 6 | /// 7 | /// Handles mapping for . 8 | /// 9 | public sealed class WarMapper(StatisticsMapper statisticsMapper) 10 | { 11 | /// 12 | /// Handles mapping to V1. 13 | /// 14 | public War MapToV1(MappingContext context, List planets) 15 | { 16 | return new War( 17 | Started: DateTime.UnixEpoch.AddSeconds(context.WarInfo.StartDate), 18 | Ended: DateTime.UnixEpoch.AddSeconds(context.WarInfo.EndDate), 19 | Now: DateTime.UnixEpoch.AddSeconds(context.InvariantWarStatus.Time), 20 | ClientVersion: context.WarInfo.MinimumClientVersion, 21 | Factions: Static.Factions.Values.ToList(), 22 | ImpactMultiplier: context.InvariantWarStatus.ImpactMultiplier, 23 | Statistics: statisticsMapper.MapToV1(context.WarSummary.GalaxyStats, planets) 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/V1SerializerContext.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models.V1; 2 | using Helldivers.Models.V1.Planets; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Helldivers.Models; 6 | 7 | /// 8 | /// Source generated for V1's models. 9 | /// 10 | [JsonSerializable(typeof(Assignment))] 11 | [JsonSerializable(typeof(List))] 12 | [JsonSerializable(typeof(Campaign))] 13 | [JsonSerializable(typeof(List))] 14 | [JsonSerializable(typeof(Dispatch))] 15 | [JsonSerializable(typeof(List))] 16 | [JsonSerializable(typeof(Planet))] 17 | [JsonSerializable(typeof(List))] 18 | [JsonSerializable(typeof(Statistics))] 19 | [JsonSerializable(typeof(SteamNews))] 20 | [JsonSerializable(typeof(List))] 21 | [JsonSerializable(typeof(War))] 22 | [JsonSerializable(typeof(Region))] 23 | [JsonSerializable(typeof(List))] 24 | [JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true, UseStringEnumConverter = true)] 25 | public sealed partial class V1SerializerContext : JsonSerializerContext 26 | { 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/Helldivers-2-SourceGen/Parsers/EnvironmentalsParser.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.Json; 3 | 4 | namespace Helldivers.SourceGen.Parsers; 5 | 6 | /// 7 | /// The EnvironmentalsParser class is responsible for parsing JSON strings 8 | /// representing environmental hazards and converting them into C# source code. 9 | /// 10 | public class EnvironmentalsParser : BaseJsonParser 11 | { 12 | /// 13 | protected override (string Type, string Source) Parse(string json) 14 | { 15 | var builder = new StringBuilder("new Dictionary()\n\t{\n"); 16 | var entries = JsonSerializer.Deserialize>>(json)!; 17 | foreach (var pair in entries) 18 | builder.AppendLine($@"{'\t'}{'\t'}{{ ""{pair.Key}"", new Helldivers.Models.V1.Planets.Hazard(""{pair.Value["name"]}"", ""{pair.Value["description"]}"") }},"); 19 | 20 | builder.Append("\t}"); 21 | return ("IReadOnlyDictionary", builder.ToString()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Helldivers 2 Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/Configuration/ApiConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.API.Configuration; 2 | 3 | /// 4 | /// Contains configuration of the API. 5 | /// 6 | public sealed class ApiConfiguration 7 | { 8 | /// 9 | /// The amount of requests that can be made within the time limit. 10 | /// 11 | public int RateLimit { get; set; } 12 | 13 | /// 14 | /// The time before the rate limit resets (in seconds). 15 | /// 16 | public int RateLimitWindow { get; set; } 17 | 18 | /// 19 | /// A comma separated list of clients that are (temporarily) blacklisted from making requests. 20 | /// 21 | public string Blacklist { get; set; } = string.Empty; 22 | 23 | /// 24 | /// Whether X-Super-Client and X-Super-Contact headers are validated. 25 | /// 26 | public bool ValidateClients { get; set; } = true; 27 | 28 | /// 29 | /// Contains the for the API. 30 | /// 31 | public AuthenticationConfiguration Authentication { get; set; } = null!; 32 | } 33 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/V1/Planets/Event.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.V1.Planets; 2 | 3 | /// 4 | /// An ongoing event on a . 5 | /// 6 | /// The unique identifier of this event. 7 | /// The type of event. 8 | /// The faction that initiated the event. 9 | /// The health of the at the time of snapshot. 10 | /// The maximum health of the at the time of snapshot. 11 | /// When the event started. 12 | /// When the event will end. 13 | /// The identifier of the linked to this event. 14 | /// A list of joint operation identifier linked to this event. 15 | public sealed record Event( 16 | int Id, 17 | int EventType, 18 | string Faction, 19 | long Health, 20 | long MaxHealth, 21 | DateTime StartTime, 22 | DateTime EndTime, 23 | int CampaignId, 24 | List JointOperationIds 25 | ); 26 | -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml app configuration file generated for helldivers-2 on 2024-02-28T00:35:23+01:00 2 | # 3 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 4 | # 5 | 6 | app = 'helldivers-2-dotnet' 7 | primary_region = 'ams' 8 | kill_signal = 'SIGTERM' 9 | 10 | [build] 11 | dockerfile = 'src/Helldivers-2-API/Dockerfile' 12 | ignorefile = '.dockerignore' 13 | 14 | [env] 15 | 16 | [metrics] 17 | port = 8080 18 | path = "/metrics" 19 | 20 | [http_service] 21 | internal_port = 8080 22 | force_https = true 23 | auto_stop_machines = true 24 | auto_start_machines = true 25 | min_machines_running = 0 26 | max_machines_running = 1 27 | processes = ['app'] 28 | 29 | [http_service.concurrency] 30 | type = 'connections' 31 | hard_limit = 1000 32 | soft_limit = 1000 33 | 34 | [[http_service.checks]] 35 | grace_period = "10s" 36 | interval = "60s" 37 | method = "GET" 38 | timeout = "10s" 39 | path = "/raw/api/WarSeason/current/WarID" 40 | [http_service.checks.headers] 41 | X-Super-Client = "Fly Health Check" 42 | X-Super-Contact = "fly.io" 43 | 44 | 45 | 46 | [[vm]] 47 | memory = "256mb" 48 | size = 'shared-cpu-1x' 49 | -------------------------------------------------------------------------------- /src/Helldivers-2-Sync/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Sync.Hosted; 2 | using Helldivers.Sync.Services; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace Helldivers.Sync.Extensions; 6 | 7 | /// 8 | /// Contains extension methods for . 9 | /// 10 | public static class ServiceCollectionExtensions 11 | { 12 | /// 13 | /// Adds all services related to the Helldivers API to the service container. 14 | /// 15 | public static IServiceCollection AddHelldiversSync(this IServiceCollection services) 16 | { 17 | services.AddSingleton(); 18 | services.AddSingleton(); 19 | services.AddHostedService(provider => provider.GetRequiredService()); 20 | services.AddHostedService(provider => provider.GetRequiredService()); 21 | 22 | services.AddHttpClient(); 23 | services.AddHttpClient(http => 24 | { 25 | http.BaseAddress = new Uri("https://api.live.prod.thehelldiversgame.com"); 26 | }); 27 | 28 | return services; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHead/SpaceStation.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models.ArrowHead.SpaceStations; 2 | 3 | namespace Helldivers.Models.ArrowHead; 4 | 5 | /// 6 | /// Represents an assignment given from Super Earth to the Helldivers. 7 | /// 8 | /// The unique identifier of the station. 9 | /// The id of the planet it's currently orbiting 10 | /// The id of the previous planet election. 11 | /// The id of the current planet election. 12 | /// The id of the next planet election. 13 | /// When the election for the next planet will end (in seconds relative to game start). 14 | /// A set of flags, purpose currently unknown. 15 | /// The list of actions the space station crew can perform. 16 | public sealed record SpaceStation( 17 | long Id32, 18 | int PlanetIndex, 19 | string LastElectionId, 20 | string CurrentElectionId, 21 | string NextElectionId, 22 | ulong CurrentElectionEndWarTime, 23 | int Flags, 24 | List TacticalActions 25 | ); 26 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHead/WarInfo.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models.ArrowHead.Info; 2 | 3 | namespace Helldivers.Models.ArrowHead; 4 | 5 | /// 6 | /// Represents mostly static information of the current galactic war. 7 | /// 8 | /// The identifier of the war season this represents. 9 | /// A unix timestamp (in seconds) when this season started. 10 | /// A unix timestamp (in seconds) when this season will end. 11 | /// A version string indicating the minimum game client version the API supports. 12 | /// A list of planets involved in this season's war. 13 | /// A list of homeworlds for the races (factions) involved in this war. 14 | /// The regions that can be found on this planet. 15 | public sealed record WarInfo( 16 | int WarId, 17 | long StartDate, 18 | long EndDate, 19 | string MinimumClientVersion, 20 | List PlanetInfos, 21 | List HomeWorlds, 22 | // TODO: capitalInfo's 23 | // TODO planetPermanentEffects 24 | List PlanetRegions 25 | ); 26 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHead/Assignments/Setting.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.ArrowHead.Assignments; 2 | 3 | /// 4 | /// Contains the details of an like reward and requirements. 5 | /// 6 | /// The type of assignment, values unknown at the moment. 7 | /// The title of this assignment. 8 | /// The briefing (description) of this assignment. 9 | /// A description of what is expected of Helldivers to complete the assignment. 10 | /// A list of s describing the assignment requirements. 11 | /// Contains information on the reward players will receive upon completion. 12 | /// Contains information on the rewards players will receive upon completion. 13 | /// Flags, suspected to be a binary OR'd value, purpose unknown. 14 | public sealed record Setting( 15 | int Type, 16 | string OverrideTitle, 17 | string OverrideBrief, 18 | string TaskDescription, 19 | List Tasks, 20 | Reward? Reward, 21 | List Rewards, 22 | int Flags 23 | ); 24 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHead/Status/PlanetRegionStatus.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models.ArrowHead.Info; 2 | 3 | namespace Helldivers.Models.ArrowHead.Status; 4 | 5 | /// 6 | /// Represents the 'current' status of a planet's region in the galactic war. 7 | /// 8 | /// The identifier of the this region is on. 9 | /// The identifier of the this data refers to. 10 | /// The current faction that controls the region. 11 | /// The current health / liberation of the region. 12 | /// If left alone, how much the health of the region would regenerate. 13 | /// Unknown purpose. 14 | /// Whether this region is currently available to play on(?). 15 | /// The amount of helldivers currently active on this planet. 16 | public sealed record PlanetRegionStatus( 17 | int PlanetIndex, 18 | int RegionIndex, 19 | int Owner, 20 | long Health, 21 | double RegerPerSecond, 22 | double AvailabilityFactor, 23 | bool IsAvailable, 24 | long Players 25 | ); 26 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/OpenApi/TypeMappers/EnumStringTypeMapper.cs: -------------------------------------------------------------------------------- 1 | #if DEBUG 2 | using NJsonSchema; 3 | using NJsonSchema.Generation.TypeMappers; 4 | using System.Collections.ObjectModel; 5 | 6 | namespace Helldivers.API.OpenApi.TypeMappers; 7 | 8 | /// 9 | /// Forces enums to be generated as an enumeration of strings rather than integers. 10 | /// 11 | /// The enum type to convert. 12 | public sealed class EnumStringTypeMapper : ITypeMapper where T : struct, Enum 13 | { 14 | /// 15 | public Type MappedType => typeof(T); 16 | 17 | /// 18 | public bool UseReference => false; 19 | 20 | /// 21 | public void GenerateSchema(JsonSchema schema, TypeMapperContext context) 22 | { 23 | schema.Type = JsonObjectType.String; 24 | schema.Format = null; 25 | 26 | var names = Enum.GetNames(); 27 | 28 | schema.Enumeration.Clear(); 29 | schema.EnumerationNames.Clear(); 30 | 31 | foreach (var name in names) 32 | schema.Enumeration.Add(name); 33 | 34 | schema.EnumerationNames = new Collection(names.ToList()); 35 | schema.ExtensionData ??= new Dictionary()!; 36 | } 37 | } 38 | #endif 39 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHead/Status/PlanetEvent.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models.ArrowHead.Info; 2 | 3 | namespace Helldivers.Models.ArrowHead.Status; 4 | 5 | /// 6 | /// An ongoing event on a planet. 7 | /// 8 | /// The unique identifier of this event. 9 | /// The index of the planet. 10 | /// A numerical identifier that indicates what type of event this is. 11 | /// The identifier of the faction that owns the planet currently. 12 | /// The current health of the event. 13 | /// The current maximum health of the event. 14 | /// When this event started. 15 | /// When the event will end. 16 | /// The unique identifier of a related campaign. 17 | /// A list of identifiers of related joint operations. 18 | public sealed record PlanetEvent( 19 | int Id, 20 | int PlanetIndex, 21 | int EventType, 22 | int Race, 23 | long Health, 24 | long MaxHealth, 25 | long StartTime, 26 | long ExpireTime, 27 | int CampaignId, 28 | List JointOperationIds 29 | ); 30 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHead/Info/PlanetInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.ArrowHead.Info; 2 | 3 | /// 4 | /// Represents information of a planet from the 'WarInfo' endpoint returned by ArrowHead's API. 5 | /// 6 | /// The numerical identifier for this planet, used as reference by other properties throughout the API (like ). 7 | /// Purpose unknown at this time. 8 | /// A set of X/Y coordinates specifying the position of this planet on the galaxy map. 9 | /// A list of links to other planets (supply lines). 10 | /// The identifier of the sector this planet is located in. 11 | /// The 'health' of this planet, indicates how much liberation it needs to switch sides. 12 | /// Whether this planet is currently considered active in the galactic war. 13 | /// The identifier of the faction that initially owned this planet. 14 | public sealed record PlanetInfo( 15 | int Index, 16 | long SettingsHash, 17 | PlanetCoordinates Position, 18 | List Waypoints, 19 | int Sector, 20 | long MaxHealth, 21 | bool Disabled, 22 | int InitialOwner 23 | ); 24 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/Controllers/V1/CampaignsController.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Contracts.Collections; 2 | using Helldivers.Models.V1; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace Helldivers.API.Controllers.V1; 6 | 7 | /// 8 | /// Contains API endpoints for . 9 | /// 10 | public static class CampaignsController 11 | { 12 | /// 13 | /// Fetches a list of all available information available. 14 | /// 15 | [ProducesResponseType>(StatusCodes.Status200OK)] 16 | public static async Task Index(HttpContext context, IStore store) 17 | { 18 | var campaigns = await store.AllAsync(context.RequestAborted); 19 | 20 | return Results.Ok(campaigns); 21 | } 22 | 23 | 24 | /// 25 | /// Fetches a specific identified by . 26 | /// 27 | [ProducesResponseType(StatusCodes.Status200OK)] 28 | public static async Task Show(HttpContext context, IStore store, [FromRoute] int index) 29 | { 30 | var campaign = await store.GetAsync(index, context.RequestAborted); 31 | if (campaign is null) 32 | return Results.NotFound(); 33 | 34 | return Results.Ok(campaign); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/Controllers/V1/DispatchController.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Contracts.Collections; 2 | using Helldivers.Models.V1; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace Helldivers.API.Controllers.V1; 6 | 7 | /// 8 | /// Contains API endpoints for . 9 | /// 10 | public static class DispatchController 11 | { 12 | /// 13 | /// Fetches a list of all available information available. 14 | /// 15 | [ProducesResponseType>(StatusCodes.Status200OK)] 16 | public static async Task Index(HttpContext context, IStore store) 17 | { 18 | var dispatches = await store.AllAsync(context.RequestAborted); 19 | 20 | return Results.Ok(dispatches); 21 | } 22 | 23 | 24 | /// 25 | /// Fetches a specific identified by . 26 | /// 27 | [ProducesResponseType(StatusCodes.Status200OK)] 28 | public static async Task Show(HttpContext context, IStore store, [FromRoute] int index) 29 | { 30 | var dispatch = await store.GetAsync(index, context.RequestAborted); 31 | if (dispatch is null) 32 | return Results.NotFound(); 33 | 34 | return Results.Ok(dispatch); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/Controllers/V2/DispatchController.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Contracts.Collections; 2 | using Helldivers.Models.V2; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace Helldivers.API.Controllers.V2; 6 | 7 | /// 8 | /// Contains API endpoints for . 9 | /// 10 | public static class DispatchController 11 | { 12 | /// 13 | /// Fetches a list of all available information available. 14 | /// 15 | [ProducesResponseType>(StatusCodes.Status200OK)] 16 | public static async Task Index(HttpContext context, IStore store) 17 | { 18 | var dispatches = await store.AllAsync(context.RequestAborted); 19 | 20 | return Results.Ok(dispatches); 21 | } 22 | 23 | 24 | /// 25 | /// Fetches a specific identified by . 26 | /// 27 | [ProducesResponseType(StatusCodes.Status200OK)] 28 | public static async Task Show(HttpContext context, IStore store, [FromRoute] int index) 29 | { 30 | var dispatch = await store.GetAsync(index, context.RequestAborted); 31 | if (dispatch is null) 32 | return Results.NotFound(); 33 | 34 | return Results.Ok(dispatch); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Helldivers-2-SourceGen/Parsers/PlanetRegionsParser.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.Json; 3 | 4 | namespace Helldivers.SourceGen.Parsers; 5 | 6 | 7 | /// 8 | /// Handles parsing the planetRegion.json and generating the resulting region data. 9 | /// 10 | public class PlanetRegionsParser : BaseJsonParser 11 | { 12 | /// 13 | protected override (string Type, string Source) Parse(string json) 14 | { 15 | var builder = new StringBuilder("new Dictionary()\n\t{\n"); 16 | var document = JsonDocument.Parse(json); 17 | foreach (var property in document.RootElement.EnumerateObject()) 18 | { 19 | var index = property.Name; 20 | var name = property.Value.GetProperty("name").GetString(); 21 | string? description = property.Value.GetProperty("description").GetString(); 22 | if (string.IsNullOrWhiteSpace(description)) 23 | description = "null"; 24 | else 25 | description = $@"""{description}"""; 26 | 27 | builder.AppendLine($@"{'\t'}{'\t'}{{ {index}, (""{name}"", {description}) }},"); 28 | } 29 | 30 | builder.Append("\t}"); 31 | return ("IReadOnlyDictionary", builder.ToString()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Contracts/Collections/IStore.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace Helldivers.Core.Contracts.Collections; 4 | 5 | /// 6 | /// Provides access to the current version of . 7 | /// 8 | /// 9 | /// If multiple variants per exist, the underlying 10 | /// implementation is responsible for selecting the correct variant 11 | /// based on . 12 | /// 13 | public interface IStore where T : class 14 | { 15 | /// 16 | /// Updates the state of the store with the given . 17 | /// 18 | ValueTask SetStore(List values); 19 | 20 | /// 21 | /// Fetches all instances available. 22 | /// 23 | Task> AllAsync(CancellationToken cancellationToken = default); 24 | 25 | /// 26 | /// Attempts to fetch a single by . 27 | /// 28 | /// 29 | /// An instance of if found, or null if no instance associated with exists. 30 | /// 31 | Task GetAsync(TKey key, CancellationToken cancellationToken = default); 32 | } 33 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/Controllers/V1/AssignmentsController.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Contracts.Collections; 2 | using Helldivers.Models.V1; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace Helldivers.API.Controllers.V1; 6 | 7 | /// 8 | /// Contains API endpoints for . 9 | /// 10 | public static class AssignmentsController 11 | { 12 | /// 13 | /// Fetches a list of all available information available. 14 | /// 15 | [ProducesResponseType>(StatusCodes.Status200OK)] 16 | public static async Task Index(HttpContext context, IStore store) 17 | { 18 | var assignments = await store.AllAsync(context.RequestAborted); 19 | 20 | return Results.Ok(assignments); 21 | } 22 | 23 | 24 | /// 25 | /// Fetches a specific identified by . 26 | /// 27 | [ProducesResponseType(StatusCodes.Status200OK)] 28 | public static async Task Show(HttpContext context, IStore store, [FromRoute] long index) 29 | { 30 | var assignment = await store.GetAsync(index, context.RequestAborted); 31 | if (assignment is null) 32 | return Results.NotFound(); 33 | 34 | return Results.Ok(assignment); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/Controllers/V2/SpaceStationController.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Contracts.Collections; 2 | using Helldivers.Models.V2; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace Helldivers.API.Controllers.V2; 6 | 7 | /// 8 | /// Contains API endpoints for . 9 | /// 10 | public static class SpaceStationController 11 | { 12 | /// 13 | /// Fetches a list of all available information available. 14 | /// 15 | [ProducesResponseType>(StatusCodes.Status200OK)] 16 | public static async Task Index(HttpContext context, IStore store) 17 | { 18 | // TODO: check implementation. 19 | var stations = await store.AllAsync(context.RequestAborted); 20 | 21 | return Results.Ok(stations); 22 | } 23 | 24 | /// 25 | /// Fetches a specific identified by . 26 | /// 27 | [ProducesResponseType(StatusCodes.Status200OK)] 28 | public static async Task Show(HttpContext context, IStore store, [FromRoute] long index) 29 | { 30 | var station = await store.GetAsync(index, context.RequestAborted); 31 | 32 | if (station is null) 33 | return Results.NotFound(); 34 | 35 | return Results.Ok(station); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/fly.yml: -------------------------------------------------------------------------------- 1 | name: Fly Deploy 2 | on: 3 | release: 4 | types: [ published ] 5 | workflow_dispatch: 6 | inputs: 7 | version: 8 | description: 'Version to use (e.g. v1.2.3)' 9 | required: true 10 | jobs: 11 | deploy: 12 | environment: Fly 13 | name: Deploy app 14 | runs-on: ubuntu-latest 15 | concurrency: deploy-group # optional: ensure only one action runs at a time 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - uses: superfly/flyctl-actions/setup-flyctl@master 20 | 21 | - name: Initial static JSON schema submodule 22 | run: git submodule update --init ./src/Helldivers-2-Models/json 23 | 24 | - name: Set Version 25 | id: set_version 26 | run: | 27 | if [[ -n "${{ github.event.inputs.version }}" ]]; then 28 | VERSION="${{ github.event.inputs.version }}" 29 | else 30 | VERSION="${GITHUB_REF##*/}" 31 | fi 32 | 33 | VERSION_WITHOUT_V="${VERSION#v}" 34 | 35 | echo "VERSION=$VERSION" 36 | echo "VERSION_WITHOUT_V=$VERSION_WITHOUT_V" 37 | echo "version=$VERSION" >> $GITHUB_OUTPUT 38 | echo "version-without-v=$VERSION_WITHOUT_V" >> $GITHUB_OUTPUT 39 | 40 | - run: flyctl deploy --remote-only --build-arg VERSION=${{ steps.set_version.outputs.version-without-v }} 41 | env: 42 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 43 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Mapping/V1/CampaignMapper.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models; 2 | using Helldivers.Models.V1; 3 | 4 | namespace Helldivers.Core.Mapping.V1; 5 | 6 | /// 7 | /// Handles mapping for . 8 | /// 9 | public class CampaignMapper 10 | { 11 | /// 12 | /// Maps all in the current context. 13 | /// 14 | public IEnumerable MapToV1(MappingContext context, List planets) 15 | { 16 | foreach (var campaign in context.InvariantWarStatus.Campaigns) 17 | { 18 | var result = MapToV1(campaign, planets); 19 | 20 | if (result is not null) 21 | yield return result; 22 | } 23 | } 24 | 25 | /// 26 | /// Maps ArrowHead's onto V1's. 27 | /// 28 | private Campaign? MapToV1(Models.ArrowHead.Status.Campaign campaign, List planets) 29 | { 30 | var planet = planets.FirstOrDefault(p => p.Index == campaign.PlanetIndex); 31 | Static.Factions.TryGetValue(campaign.Race, out var currentOwner); 32 | 33 | if (planet is null) 34 | return null; 35 | 36 | return new Campaign( 37 | Id: campaign.Id, 38 | Planet: planet, 39 | Type: campaign.Type, 40 | Count: campaign.Count, 41 | Faction: currentOwner ?? string.Empty 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/Middlewares/EtagMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Sync.Hosted; 2 | using System.Globalization; 3 | 4 | namespace Helldivers.API.Middlewares; 5 | 6 | /// 7 | /// Automatically appends the `Etag` header to responses. 8 | /// 9 | public sealed class EtagMiddleware(ArrowHeadSyncService arrowHead) : IMiddleware 10 | { 11 | /// 12 | public async Task InvokeAsync(HttpContext context, RequestDelegate next) 13 | { 14 | if (arrowHead.LastUpdated is { } lastUpdated) 15 | { 16 | var key = $"{lastUpdated.Subtract(DateTime.UnixEpoch).TotalMilliseconds:0}"; 17 | context.Response.Headers.ETag = key; 18 | 19 | var isModifiedSince = context.Request.Headers.IfModifiedSince.Any(value => 20 | { 21 | if (DateTime.TryParseExact(value, "R", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, 22 | out var date)) 23 | { 24 | if (date >= lastUpdated) 25 | { 26 | return false; 27 | } 28 | } 29 | 30 | return true; 31 | }) || context.Request.Headers.IfModifiedSince.Count == 0; 32 | 33 | if (isModifiedSince is false) 34 | { 35 | context.Response.StatusCode = StatusCodes.Status304NotModified; 36 | return; 37 | } 38 | } 39 | 40 | await next(context); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/V1/Assignment.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models.Domain.Localization; 2 | using Helldivers.Models.V1.Assignments; 3 | using Task = Helldivers.Models.V1.Assignments.Task; 4 | 5 | namespace Helldivers.Models.V1; 6 | 7 | /// 8 | /// Represents an assignment given by Super Earth to the community. 9 | /// This is also known as 'Major Order's in the game. 10 | /// 11 | /// The unique identifier of this assignment. 12 | /// A list of numbers, how they represent progress is unknown. 13 | /// The title of the assignment. 14 | /// A long form description of the assignment, usually contains context. 15 | /// A very short summary of the description. 16 | /// A list of tasks that need to be completed for this assignment. 17 | /// The reward for completing the assignment. 18 | /// A list of rewards for completing the assignment. 19 | /// The date when the assignment will expire. 20 | /// Flags regarding the assignment. 21 | public sealed record Assignment( 22 | long Id, 23 | List Progress, 24 | LocalizedMessage Title, 25 | LocalizedMessage Briefing, 26 | LocalizedMessage Description, 27 | List Tasks, 28 | Reward? Reward, 29 | List Rewards, 30 | DateTime Expiration, 31 | int Flags 32 | ); 33 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Localization/CultureDictionary.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace Helldivers.Core.Localization; 4 | 5 | /// 6 | /// Specialized version of that intelligently maps as 7 | /// keys (eg, if "en-UK" does not exist but "en-US" it'll match). 8 | /// 9 | public class CultureDictionary 10 | { 11 | private readonly Dictionary _items; 12 | 13 | /// Creates a new instance of , 14 | public CultureDictionary(IEnumerable> items) 15 | { 16 | _items = items 17 | .Select(pair => new KeyValuePair(new CultureInfo(pair.Key), pair.Value)) 18 | .SelectMany(pair => new List>([ 19 | new KeyValuePair(pair.Key, pair.Value), 20 | new KeyValuePair(pair.Key.Parent, pair.Value) 21 | ])) 22 | .DistinctBy(pair => pair.Key) 23 | .ToDictionary(pair => pair.Key, pair => pair.Value); 24 | } 25 | 26 | /// 27 | /// Attempts to resolve a by it's closest common value or parent. 28 | /// 29 | public T? Get(CultureInfo? cultureInfo = default) 30 | { 31 | cultureInfo ??= CultureInfo.CurrentCulture; 32 | 33 | return _items.GetValueOrDefault(cultureInfo) 34 | ?? _items.GetValueOrDefault(cultureInfo.Parent); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/OpenApi/DocumentProcessors/HelldiversDocumentProcessor.cs: -------------------------------------------------------------------------------- 1 | #if DEBUG 2 | using NSwag; 3 | using NSwag.Generation.Processors; 4 | using NSwag.Generation.Processors.Contexts; 5 | 6 | namespace Helldivers.API.OpenApi.DocumentProcessors; 7 | 8 | /// 9 | /// Handles processing of the OpenAPI specification of the Production Helldivers API. 10 | /// 11 | public class HelldiversDocumentProcessor : IDocumentProcessor 12 | { 13 | private const string HelldiversFlyServer = "https://api.helldivers2.dev/"; 14 | 15 | /// 16 | public void Process(DocumentProcessorContext context) 17 | { 18 | // First we make a copy of the Paths, since C# doesn't like it when collections are modified during iteration. 19 | var paths = context.Document.Paths.ToList(); 20 | var server = new OpenApiServer 21 | { 22 | Description = "The dotnet helldivers server", 23 | Url = HelldiversFlyServer 24 | }; 25 | 26 | foreach (var (_, item) in paths) 27 | { 28 | item.Servers.Clear(); 29 | item.Servers.Add(server); 30 | } 31 | 32 | foreach (var (_, schema) in context.Document.Components.Schemas) 33 | { 34 | foreach (var (key, property) in schema.Properties) 35 | { 36 | foreach (var oneOf in property.OneOf) 37 | { 38 | property.AnyOf.Add(oneOf); 39 | } 40 | 41 | property.OneOf.Clear(); 42 | } 43 | } 44 | } 45 | } 46 | #endif 47 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/V1/Planets/Region.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.V1.Planets; 2 | 3 | /// 4 | /// A region on a planet. 5 | /// 6 | /// The Name and Description fields may be empty when the underlying data store doesn't contain information on them. 7 | /// This is typically when ArrowHead adds new regions that aren't updated in the data store (helldivers-2/json) yet. 8 | /// 9 | /// Note that some properties may be unavailable when the region is inactive. 10 | /// 11 | /// The identifier of this region. 12 | /// The underlying hash identifier of ArrowHead. 13 | /// The name of the region. 14 | /// A long-form description of the region. 15 | /// The current health of the region. 16 | /// The maximum health of this region. 17 | /// The size of this region. 18 | /// The amount of health this region generates when left alone. 19 | /// Unknown purpose. 20 | /// Whether the region is currently playable(?). 21 | /// The amount of helldivers currently active in this region. 22 | public record struct Region( 23 | int Id, 24 | ulong Hash, 25 | string? Name, 26 | string? Description, 27 | long? Health, 28 | ulong MaxHealth, 29 | RegionSize Size, 30 | double? RegenPerSecond, 31 | double? AvailabilityFactor, 32 | bool IsAvailable, 33 | long Players 34 | ); 35 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHead/WarStatus.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models.ArrowHead.Status; 2 | 3 | namespace Helldivers.Models.ArrowHead; 4 | 5 | /// 6 | /// Represents a snapshot of the current status of the galactic war. 7 | /// 8 | /// The war season this snapshot refers to. 9 | /// The time this snapshot was taken. 10 | /// This is the factor by which influence at the end of a mission is multiplied to calculate the impact on liberation 11 | /// Internal identifier, purpose unknown. 12 | /// A list of statuses for planets. 13 | /// A list of attacks currently ongoing. 14 | /// A list of ongoing campaigns in the galactic war. 15 | /// A list of s. 16 | /// A list of ongoing s. 17 | /// The regions that have a status. 18 | public sealed record WarStatus( 19 | int WarId, 20 | long Time, 21 | double ImpactMultiplier, 22 | long StoryBeatId32, 23 | List PlanetStatus, 24 | List PlanetAttacks, 25 | List Campaigns, 26 | // TODO CommunityTargets 27 | List JointOperations, 28 | List PlanetEvents, 29 | // TODO PlanetActiveEffects 30 | // TODO activeElectionPolicyEffects 31 | // TODO globalEvents 32 | // TODO superEarthWarResults 33 | List PlanetRegions 34 | ); 35 | -------------------------------------------------------------------------------- /src/Helldivers-2-Sync/Configuration/HelldiversSyncConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Sync.Configuration; 2 | 3 | /// 4 | /// Represents configuration for the synchronization system of the Helldivers 2 API. 5 | /// 6 | public sealed class HelldiversSyncConfiguration 7 | { 8 | /// 9 | /// The interval (in seconds) at which the sync services will fetch information from the API. 10 | /// 11 | public int IntervalSeconds { get; set; } = 10; 12 | 13 | /// 14 | /// The default language which the API will use. 15 | /// 16 | public string DefaultLanguage { get; set; } = "en-US"; 17 | 18 | /// 19 | /// The list of space stations to fetch information for. 20 | /// 21 | public List SpaceStations { get; set; } = new(0); 22 | 23 | /// 24 | /// A list of language codes for which translations will be provided. 25 | /// 26 | public List Languages { get; set; } = new(0); 27 | 28 | /// 29 | /// Flag to indicate if the application should only run the sync once. 30 | /// This is used in CI testing to validate sync works. 31 | /// 32 | public bool RunOnce { get; set; } = false; 33 | 34 | /// 35 | /// Get the maximum number of entries returned by ArrowHead from the newsfeed API. 36 | /// 37 | public uint NewsFeedMaxEntries { get; set; } = 1024; 38 | 39 | /// 40 | /// Get all news feed entries that were published after this timestamp 41 | /// 42 | public uint NewsFeedFromTimestamp { get; set; } = 1000; 43 | } 44 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/Controllers/V1/SteamController.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Contracts.Collections; 2 | using Helldivers.Models.V1; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace Helldivers.API.Controllers.V1; 6 | 7 | /// 8 | /// Contains API endpoints for . 9 | /// 10 | public static class SteamController 11 | { 12 | /// 13 | /// Fetches the Steam newsfeed for Helldivers 2. 14 | /// 15 | /// You can definitely get this yourself, however it's included for your convenience! 16 | [ProducesResponseType>(StatusCodes.Status200OK)] 17 | public static async Task Index(HttpContext context, IStore store) 18 | { 19 | var news = await store.AllAsync(context.RequestAborted); 20 | 21 | return Results.Ok(news); 22 | } 23 | 24 | /// 25 | /// Fetches a specific newsfeed item from the Helldivers 2 Steam newsfeed. 26 | /// 27 | /// You can definitely get this yourself, however it's included for your convenience! 28 | /// Returned when the key is not recognized. 29 | [ProducesResponseType(StatusCodes.Status404NotFound)] 30 | [ProducesResponseType>(StatusCodes.Status200OK)] 31 | public static async Task Show(HttpContext context, IStore store, [FromRoute] string gid) 32 | { 33 | var news = await store.GetAsync(gid, context.RequestAborted); 34 | 35 | if (news is null) 36 | return Results.NotFound(); 37 | 38 | return Results.Ok(news); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/OpenApi/OperationProcessors/SuperHeadersProcessor.cs: -------------------------------------------------------------------------------- 1 | #if DEBUG 2 | using NSwag; 3 | using NSwag.Generation.Processors; 4 | using NSwag.Generation.Processors.Contexts; 5 | 6 | namespace Helldivers.API.OpenApi.OperationProcessors; 7 | 8 | /// 9 | /// The is used to add headers that are required for the API to identify 10 | /// the client and to provide developer contact information. 11 | /// 12 | public class SuperHeadersProcessor : IOperationProcessor 13 | { 14 | /// 15 | public bool Process(OperationProcessorContext context) 16 | { 17 | context.OperationDescription.Operation.Parameters.Add( 18 | new OpenApiParameter 19 | { 20 | Name = Constants.CLIENT_HEADER_NAME, 21 | Kind = OpenApiParameterKind.Header, 22 | Type = NJsonSchema.JsonObjectType.String, 23 | IsRequired = true, 24 | Description = "The name of the header that identifies the client to the API.", 25 | Default = string.Empty 26 | } 27 | ); 28 | 29 | context.OperationDescription.Operation.Parameters.Add( 30 | new OpenApiParameter 31 | { 32 | Name = Constants.CONTACT_HEADER_NAME, 33 | Kind = OpenApiParameterKind.Header, 34 | Type = NJsonSchema.JsonObjectType.String, 35 | IsRequired = true, 36 | Description = "The name of the header with developer contact information.", 37 | Default = string.Empty 38 | } 39 | ); 40 | 41 | return true; 42 | } 43 | } 44 | #endif 45 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Facades/V2Facade.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Contracts.Collections; 2 | using Helldivers.Core.Mapping; 3 | using Helldivers.Core.Mapping.V2; 4 | using Helldivers.Models.V1; 5 | using Helldivers.Models.V2; 6 | using Dispatch = Helldivers.Models.V2.Dispatch; 7 | 8 | namespace Helldivers.Core.Facades; 9 | 10 | /// 11 | /// Handles dispatching incoming data to all stores for V2. 12 | /// 13 | public sealed class V2Facade( 14 | IStore dispatchStore, 15 | DispatchMapper dispatchMapper, 16 | IStore planetStore, 17 | IStore spaceStationStore, 18 | SpaceStationMapper spaceStationMapper 19 | ) 20 | { 21 | /// 22 | public async ValueTask UpdateStores(MappingContext context) 23 | { 24 | await UpdateDispatchStore(context); 25 | 26 | // Some mappers need access to the list of planets, so we fetch it from the freshly-mapped store. 27 | var planets = await planetStore.AllAsync(); 28 | await UpdateSpaceStationStore(context, planets); 29 | } 30 | 31 | private async ValueTask UpdateDispatchStore(MappingContext context) 32 | { 33 | var dispatches = dispatchMapper 34 | .MapToV2(context) 35 | .OrderByDescending(dispatch => dispatch.Id) 36 | .ToList(); 37 | 38 | await dispatchStore.SetStore(dispatches); 39 | } 40 | 41 | private async ValueTask UpdateSpaceStationStore(MappingContext context, List planets) 42 | { 43 | var spaceStations = spaceStationMapper 44 | .MapToV2(context, planets) 45 | .ToList(); 46 | 47 | await spaceStationStore.SetStore(spaceStations); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/OpenApi/TypeMappers/LocalizedMessageTypeMapper.cs: -------------------------------------------------------------------------------- 1 | #if DEBUG 2 | using Helldivers.Models.Domain.Localization; 3 | using NJsonSchema; 4 | using NJsonSchema.Generation.TypeMappers; 5 | 6 | namespace Helldivers.API.OpenApi.TypeMappers; 7 | 8 | /// 9 | /// Maps the LocalizedMessage type to a JSON schema describing what values it serializes to. 10 | /// 11 | /// Contains all languages that are supported (and thus should be OpenAPI documented). 12 | public sealed class LocalizedMessageTypeMapper(IReadOnlyList languages) : ITypeMapper 13 | { 14 | /// 15 | public Type MappedType => typeof(LocalizedMessage); 16 | 17 | /// 18 | public bool UseReference => false; 19 | 20 | /// 21 | /// Gets the generated schema for the LocalizedMessage type. 22 | /// 23 | public void GenerateSchema(JsonSchema schema, TypeMapperContext context) 24 | { 25 | var dictionarySchema = new JsonSchema 26 | { 27 | Title = nameof(LocalizedMessage), 28 | Type = JsonObjectType.Object, 29 | Description = "When passing in ivl-IV as Accept-Language, all available languages are returned" 30 | }; 31 | 32 | // NJsonSchema's Properties is read-only, so we can't use the object assignment syntax. 33 | foreach (string language in languages) 34 | { 35 | dictionarySchema.Properties.Add(language, new JsonSchemaProperty 36 | { 37 | Type = JsonObjectType.String, 38 | Description = $"The message in {language}" 39 | }); 40 | } 41 | 42 | schema.OneOf.Add(JsonSchema.FromType(typeof(string))); 43 | schema.OneOf.Add(dictionarySchema); 44 | } 45 | } 46 | #endif 47 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHead/Summary/GalaxyStats.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.ArrowHead.Summary; 2 | 3 | /// 4 | /// Represents galaxy wide statistics. 5 | /// 6 | /// The amount of missions won. 7 | /// The amount of missions lost. 8 | /// The total amount of time spent planetside (in seconds). 9 | /// The total amount of bugs killed since start of the season. 10 | /// The total amount of automatons killed since start of the season. 11 | /// The total amount of Illuminate killed since start of the season. 12 | /// The total amount of bullets fired 13 | /// The total amount of bullets hit 14 | /// The total amount of time played (including off-planet) in seconds. 15 | /// The amount of casualties on the side of humanity. 16 | /// The amount of revives(?). 17 | /// The amount of friendly fire casualties. 18 | /// A percentage indicating how many started missions end in success. 19 | /// A percentage indicating average accuracy of Helldivers. 20 | public sealed record GalaxyStats( 21 | ulong MissionsWon, 22 | ulong MissionsLost, 23 | ulong MissionTime, 24 | ulong BugKills, 25 | ulong AutomatonKills, 26 | ulong IlluminateKills, 27 | ulong BulletsFired, 28 | ulong BulletsHit, 29 | ulong TimePlayed, 30 | ulong Deaths, 31 | ulong Revives, 32 | ulong Friendlies, 33 | ulong MissionSuccessRate, 34 | ulong Accurracy 35 | ); 36 | -------------------------------------------------------------------------------- /src/Helldivers-2-SourceGen/Parsers/PlanetsParser.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.Json; 3 | 4 | namespace Helldivers.SourceGen.Parsers; 5 | 6 | /// 7 | /// Handles parsing the planets.json and generating the resulting planet data. 8 | /// 9 | public class PlanetsParser : BaseJsonParser 10 | { 11 | /// 12 | protected override (string Type, string Source) Parse(string json) 13 | { 14 | var builder = new StringBuilder("new Dictionary()\n\t{\n"); 15 | var document = JsonDocument.Parse(json); 16 | foreach (var property in document.RootElement.EnumerateObject()) 17 | { 18 | var index = property.Name; 19 | var names = property 20 | .Value 21 | .GetProperty("names") 22 | .EnumerateObject() 23 | .ToDictionary(prop => prop.Name, prop => prop.Value.GetString()!); 24 | var sector = property.Value.GetProperty("sector").GetString(); 25 | var biome = property.Value.GetProperty("biome").GetString(); 26 | var environmentals = property 27 | .Value 28 | .GetProperty("environmentals") 29 | .EnumerateArray() 30 | .Select(prop => $@"""{prop.GetString()!}""") 31 | .ToList(); 32 | 33 | builder.AppendLine($@"{'\t'}{'\t'}{{ {index}, (LocalizedMessage.FromStrings([{string.Join(", ", names.Select(pair => $@"[""{pair.Key}"", ""{pair.Value}""]"))}]), ""{sector}"", ""{biome}"", [{string.Join(", ", environmentals)}]) }},"); 34 | } 35 | 36 | builder.Append("\t}"); 37 | return ("IReadOnlyDictionary", builder.ToString()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/OpenApi/DocumentProcessors/ArrowHeadDocumentProcessor.cs: -------------------------------------------------------------------------------- 1 | #if DEBUG 2 | using NSwag; 3 | using NSwag.Generation.Processors; 4 | using NSwag.Generation.Processors.Contexts; 5 | 6 | namespace Helldivers.API.OpenApi.DocumentProcessors; 7 | 8 | /// 9 | /// Handles processing of the OpenAPI specification of the ArrowHead studio API. 10 | /// Specifically, it removes all community API specific aspects (like the '/raw' prefix). 11 | /// 12 | public class ArrowHeadDocumentProcessor : IDocumentProcessor 13 | { 14 | // "/raw".length 15 | private const int PrefixLength = 4; 16 | 17 | private const string ArrowHeadServer = "https://api.live.prod.thehelldiversgame.com/"; 18 | 19 | /// 20 | public void Process(DocumentProcessorContext context) 21 | { 22 | // First we make a copy of the Paths, since C# doesn't like it when collections are modified during iteration. 23 | var paths = context.Document.Paths.ToList(); 24 | var server = new OpenApiServer 25 | { 26 | Description = "The official ArrowHead server", 27 | Url = ArrowHeadServer 28 | }; 29 | 30 | foreach (var (url, item) in paths) 31 | { 32 | // If it's a '/raw' prefixed URL, we strip the /raw prefix. 33 | if (string.IsNullOrWhiteSpace(url) is false && url.StartsWith("/raw")) 34 | { 35 | item.Servers.Clear(); 36 | item.Servers.Add(server); 37 | 38 | context.Document.Paths[url[PrefixLength..]] = item; 39 | context.Document.Paths.Remove(url); 40 | } 41 | } 42 | 43 | foreach (var operation in context.Document.Operations) 44 | { 45 | operation.Operation.OperationId = operation.Operation.OperationId.Replace("Raw", string.Empty); 46 | } 47 | } 48 | } 49 | #endif 50 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/Controllers/V1/PlanetController.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Contracts.Collections; 2 | using Helldivers.Models.V1; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace Helldivers.API.Controllers.V1; 6 | 7 | /// 8 | /// Contains API endpoints for . 9 | /// 10 | public static class PlanetController 11 | { 12 | /// 13 | /// Fetches a list of all available information available. 14 | /// 15 | [ProducesResponseType>(StatusCodes.Status200OK)] 16 | public static async Task Index(HttpContext context, IStore store) 17 | { 18 | var planets = await store.AllAsync(context.RequestAborted); 19 | 20 | return Results.Ok(planets); 21 | } 22 | 23 | 24 | /// 25 | /// Fetches a specific identified by . 26 | /// 27 | [ProducesResponseType(StatusCodes.Status200OK)] 28 | public static async Task Show(HttpContext context, IStore store, [FromRoute] int index) 29 | { 30 | var planet = await store.GetAsync(index, context.RequestAborted); 31 | if (planet is null) 32 | return Results.NotFound(); 33 | 34 | return Results.Ok(planet); 35 | } 36 | 37 | /// 38 | /// Fetches all planets with an active . 39 | /// 40 | [ProducesResponseType>(StatusCodes.Status200OK)] 41 | public static async Task WithEvents(HttpContext context, IStore store) 42 | { 43 | var planets = await store.AllAsync(context.RequestAborted); 44 | var withEvents = planets 45 | .Where(planet => planet.Event is not null) 46 | .ToList(); 47 | 48 | return Results.Ok(withEvents); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine-extra AS base 2 | USER $APP_UID 3 | WORKDIR /app 4 | 5 | FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine-aot AS build 6 | RUN apk add --no-cache build-base clang lld llvm zlib-dev libstdc++ 7 | ARG BUILD_CONFIGURATION=Release 8 | ARG BUILD_RUNTIME=linux-musl-x64 9 | ARG OPENAPI=false 10 | ARG VERSION=0.0.0 11 | 12 | WORKDIR / 13 | COPY ./docs/openapi /docs/openapi 14 | 15 | WORKDIR /src 16 | 17 | COPY ["src/Helldivers-2-API/Helldivers-2-API.csproj", "Helldivers-2-API/"] 18 | COPY ["src/Helldivers-2-Models/Helldivers-2-Models.csproj", "Helldivers-2-Models/"] 19 | COPY ["src/Helldivers-2-Sync/Helldivers-2-Sync.csproj", "Helldivers-2-Sync/"] 20 | COPY ["src/Helldivers-2-Core/Helldivers-2-Core.csproj", "Helldivers-2-Core/"] 21 | COPY ["src/Helldivers-2-SourceGen/Helldivers-2-SourceGen.csproj", "Helldivers-2-SourceGen/"] 22 | COPY ["Directory.Build.props", "."] 23 | 24 | RUN dotnet restore -r $BUILD_RUNTIME "Helldivers-2-API/Helldivers-2-API.csproj" 25 | COPY ./src . 26 | WORKDIR "/src/Helldivers-2-API" 27 | RUN if [[ "${OPENAPI}" = 'true' ]]; \ 28 | then \ 29 | echo "OPENAPI is set to ${OPENAPI}, running OpenAPI generators" \ 30 | && dotnet build "Helldivers-2-API.csproj" /p:Version="$VERSION" --no-restore -c Debug \ 31 | && mkdir -p wwwroot \ 32 | && cp /docs/openapi/* wwwroot/ \ 33 | ; fi 34 | RUN dotnet build "Helldivers-2-API.csproj" --no-restore -r $BUILD_RUNTIME -c $BUILD_CONFIGURATION -o /app/build 35 | 36 | FROM build AS publish 37 | ARG BUILD_CONFIGURATION=Release 38 | ARG BUILD_RUNTIME=linux-musl-x64 39 | ARG VERSION=0.0.0 40 | RUN dotnet publish "Helldivers-2-API.csproj" /p:Version="$VERSION" --self-contained -r $BUILD_RUNTIME -c $BUILD_CONFIGURATION -o /app/publish 41 | 42 | FROM base AS final 43 | WORKDIR /app 44 | EXPOSE 8080 45 | COPY --from=publish /app/publish . 46 | ENTRYPOINT ["./Helldivers-2-API"] 47 | -------------------------------------------------------------------------------- /src/Helldivers-2-SourceGen/Parsers/BaseJsonParser.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.SourceGen.Contracts; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.Text; 4 | using System.Text; 5 | 6 | namespace Helldivers.SourceGen.Parsers; 7 | 8 | /// 9 | /// An abstract base class for parsing JSON data and generating corresponding C# source code. 10 | /// 11 | public abstract class BaseJsonParser : IJsonParser 12 | { 13 | private const string TEMPLATE = @"// 14 | #nullable enable 15 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 16 | using global::System.Collections.Generic; 17 | using global::Helldivers.Models.Domain.Localization; 18 | 19 | namespace Helldivers.Models; 20 | 21 | public static partial class Static 22 | {{ 23 | /// Public list of {0} entries from {1} 24 | public static {2} {0} = {3}; 25 | }}"; 26 | 27 | /// 28 | public SourceText Parse(AdditionalText file, CancellationToken cancellationToken = default) 29 | { 30 | var filename = Path.GetFileName(file.Path); 31 | var json = file.GetText(cancellationToken)?.ToString(); 32 | 33 | var name = Path.GetFileNameWithoutExtension(file.Path); 34 | name = $"{char.ToUpper(name[0])}{name.Substring(1)}"; 35 | 36 | if (string.IsNullOrWhiteSpace(json) is false) 37 | { 38 | var (type, csharp) = Parse(json!); 39 | 40 | var output = string.Format(TEMPLATE, name, filename, type, csharp); 41 | return SourceText.From(output, Encoding.UTF8); 42 | } 43 | 44 | return SourceText.From("// Could not read JSON file", Encoding.UTF8); 45 | } 46 | 47 | /// 48 | /// Convert the JSON string into C# code that can be injected. 49 | /// 50 | protected abstract (string Type, string Source) Parse(string json); 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a .NET project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | name: .NET CI 4 | 5 | on: 6 | push: 7 | branches: [ "master" ] 8 | pull_request: 9 | branches: [ "master" ] 10 | 11 | permissions: 12 | contents: read 13 | actions: write 14 | 15 | jobs: 16 | build: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Setup .NET 23 | uses: actions/setup-dotnet@v4 24 | with: 25 | dotnet-version: 10.0.x 26 | - name: download JSON submodule 27 | run: git submodule update --init ./src/Helldivers-2-Models/json 28 | - name: Restore dependencies 29 | run: dotnet restore 30 | - name: Build 31 | run: dotnet build --no-restore --no-incremental /p:TreatWarningsAsErrors=true 32 | - name: Check formatting 33 | run: dotnet format --no-restore --verify-no-changes --exclude-diagnostics IL2026 IL3050 34 | - name: Check tests 35 | run: dotnet test --no-build --verbosity normal 36 | 37 | - name: Upload artifacts 38 | uses: actions/upload-artifact@v4 39 | with: 40 | retention-days: 5 41 | name: openapi-schemas 42 | path: docs/openapi/ 43 | 44 | docker-build: 45 | runs-on: ubuntu-latest 46 | 47 | steps: 48 | - uses: actions/checkout@v4 49 | with: 50 | submodules: true 51 | - name: Set up Docker Buildx 52 | uses: docker/setup-buildx-action@v3 53 | - name: Build Docker image 54 | uses: docker/build-push-action@v6 55 | with: 56 | context: . 57 | file: ./src/Helldivers-2-API/Dockerfile 58 | push: false 59 | tags: helldivers-api:test 60 | cache-from: type=gha 61 | cache-to: type=gha,mode=max 62 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Mapping/V1/DispatchMapper.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models.ArrowHead; 2 | using Helldivers.Models.Domain.Localization; 3 | using Helldivers.Models.V1; 4 | 5 | namespace Helldivers.Core.Mapping.V1; 6 | 7 | /// 8 | /// Handles mapping for . 9 | /// 10 | public sealed class DispatchMapper 11 | { 12 | /// 13 | /// Maps ArrowHead's onto V1's es. 14 | /// 15 | public IEnumerable MapToV1(MappingContext context) 16 | { 17 | // Get a list of all items across all translations. 18 | var invariants = context.NewsFeeds 19 | .SelectMany(pair => pair.Value) 20 | .DistinctBy(assignment => assignment.Id); 21 | 22 | foreach (var item in invariants) 23 | { 24 | // Build a dictionary of all translations for this item 25 | var translations = context.NewsFeeds.Select(pair => 26 | new KeyValuePair( 27 | pair.Key, 28 | pair.Value.FirstOrDefault(a => a.Id == item.Id) 29 | ) 30 | ).Where(pair => pair.Value is not null) 31 | .ToDictionary(pair => pair.Key, pair => pair.Value!); 32 | 33 | yield return MapToV1(context, translations); 34 | } 35 | } 36 | 37 | private Dispatch MapToV1(MappingContext context, Dictionary translations) 38 | { 39 | var invariant = translations.Values.First(); 40 | var messages = translations.Select(pair => new KeyValuePair(pair.Key, pair.Value.Message)); 41 | 42 | return new Dispatch( 43 | Id: invariant.Id, 44 | Published: context.RelativeGameStart.AddSeconds(invariant.Published), 45 | Type: invariant.Type, 46 | Message: LocalizedMessage.FromStrings(messages) 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/V1/Statistics.cs: -------------------------------------------------------------------------------- 1 | namespace Helldivers.Models.V1; 2 | 3 | /// 4 | /// Contains statistics of missions, kills, success rate etc. 5 | /// 6 | /// The amount of missions won. 7 | /// The amount of missions lost. 8 | /// The total amount of time spent planetside (in seconds). 9 | /// The total amount of bugs killed since start of the season. 10 | /// The total amount of automatons killed since start of the season. 11 | /// The total amount of Illuminate killed since start of the season. 12 | /// The total amount of bullets fired 13 | /// The total amount of bullets hit 14 | /// The total amount of time played (including off-planet) in seconds. 15 | /// The amount of casualties on the side of humanity. 16 | /// The amount of revives(?). 17 | /// The amount of friendly fire casualties. 18 | /// A percentage indicating how many started missions end in success. 19 | /// A percentage indicating average accuracy of Helldivers. 20 | /// The total amount of players present (at the time of the snapshot). 21 | public sealed record Statistics( 22 | ulong MissionsWon, 23 | ulong MissionsLost, 24 | ulong MissionTime, 25 | ulong TerminidKills, 26 | ulong AutomatonKills, 27 | ulong IlluminateKills, 28 | ulong BulletsFired, 29 | ulong BulletsHit, 30 | ulong TimePlayed, 31 | ulong Deaths, 32 | ulong Revives, 33 | ulong Friendlies, 34 | ulong MissionSuccessRate, 35 | ulong Accuracy, 36 | ulong PlayerCount 37 | ); 38 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/Controllers/DevelopmentController.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.API.Configuration; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Extensions.Options; 4 | using Microsoft.IdentityModel.Tokens; 5 | using System.IdentityModel.Tokens.Jwt; 6 | using System.Security.Claims; 7 | 8 | namespace Helldivers.API.Controllers; 9 | 10 | /// 11 | /// Controller class that's only available in development, contains local debugging endpoints etc. 12 | /// 13 | public static class DevelopmentController 14 | { 15 | private static readonly JwtSecurityTokenHandler TokenHandler = new(); 16 | 17 | /// 18 | /// Creates a JWT token for the given and . 19 | /// 20 | public static IResult CreateToken([FromQuery] string name, [FromQuery] int limit, [FromServices] IOptions options) 21 | { 22 | var key = new SymmetricSecurityKey(Convert.FromBase64String(options.Value.Authentication.SigningKey)); 23 | var token = new JwtSecurityToken( 24 | issuer: options.Value.Authentication.ValidIssuers.First(), 25 | audience: options.Value.Authentication.ValidAudiences.First(), 26 | claims: [ 27 | new Claim("sub", name), 28 | new Claim(ClaimTypes.Name, name), 29 | new Claim("nbf", $"{DateTime.UtcNow.Subtract(DateTime.UnixEpoch).TotalSeconds:0}"), 30 | new Claim("iat", $"{DateTime.UtcNow.Subtract(DateTime.UnixEpoch).TotalSeconds:0}"), 31 | new Claim("exp", $"{DateTime.UtcNow.AddDays(30).Subtract(DateTime.UnixEpoch).TotalSeconds:0}"), 32 | new Claim("RateLimit", $"{limit}") 33 | ], 34 | expires: DateTime.UtcNow.AddDays(30), 35 | signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256) 36 | ); 37 | 38 | var jwt = TokenHandler.WriteToken(token); 39 | return Results.Ok(jwt); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Helldivers-2-SourceGen/Readme.md: -------------------------------------------------------------------------------- 1 | # Roslyn Source Generators Sample 2 | 3 | A set of three projects that illustrates Roslyn source generators. Enjoy this template to learn from and modify source generators for your own needs. 4 | 5 | ## Content 6 | ### Helldivers_2_SourceGen 7 | A .NET Standard project with implementations of sample source generators. 8 | **You must build this project to see the result (generated code) in the IDE.** 9 | 10 | - [SampleSourceGenerator.cs](SampleSourceGenerator.cs): A source generator that creates C# classes based on a text file (in this case, Domain Driven Design ubiquitous language registry). 11 | - [SampleIncrementalSourceGenerator.cs](SampleIncrementalSourceGenerator.cs): A source generator that creates a custom report based on class properties. The target class should be annotated with the `Generators.ReportAttribute` attribute. 12 | 13 | ### Helldivers_2_SourceGen.Sample 14 | A project that references source generators. Note the parameters of `ProjectReference` in [Helldivers_2_SourceGen.Sample.csproj](../Helldivers_2_SourceGen.Sample/Helldivers_2_SourceGen.Sample.csproj), they make sure that the project is referenced as a set of source generators. 15 | 16 | ### Helldivers_2_SourceGen.Tests 17 | Unit tests for source generators. The easiest way to develop language-related features is to start with unit tests. 18 | 19 | ## How To? 20 | ### How to debug? 21 | - Use the [launchSettings.json](Properties/launchSettings.json) profile. 22 | - Debug tests. 23 | 24 | ### How can I determine which syntax nodes I should expect? 25 | Consider installing the Roslyn syntax tree viewer plugin [Rossynt](https://plugins.jetbrains.com/plugin/16902-rossynt/). 26 | 27 | ### How to learn more about wiring source generators? 28 | Watch the walkthrough video: [Let’s Build an Incremental Source Generator With Roslyn, by Stefan Pölz](https://youtu.be/azJm_Y2nbAI) 29 | The complete set of information is available in [Source Generators Cookbook](https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md). -------------------------------------------------------------------------------- /src/Helldivers-2-Models/ArrowHead/Summary/PlanetStats.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models.ArrowHead.Info; 2 | 3 | namespace Helldivers.Models.ArrowHead.Summary; 4 | 5 | /// 6 | /// Similar to , but scoped to a specific planet. 7 | /// 8 | /// The for which planet these stats are. 9 | /// The amount of missions won. 10 | /// The amount of missions lost. 11 | /// The total amount of time spent planetside (in seconds). 12 | /// The total amount of bugs killed since start of the season. 13 | /// The total amount of automatons killed since start of the season. 14 | /// The total amount of Illuminate killed since start of the season. 15 | /// The total amount of bullets fired 16 | /// The total amount of bullets hit 17 | /// The total amount of time played (including off-planet) in seconds. 18 | /// The amount of casualties on the side of humanity. 19 | /// The amount of revives(?). 20 | /// The amount of friendly fire casualties. 21 | /// A percentage indicating how many started missions end in success. 22 | /// A percentage indicating average accuracy of Helldivers. 23 | public sealed record PlanetStats( 24 | int PlanetIndex, 25 | ulong MissionsWon, 26 | ulong MissionsLost, 27 | ulong MissionTime, 28 | ulong BugKills, 29 | ulong AutomatonKills, 30 | ulong IlluminateKills, 31 | ulong BulletsFired, 32 | ulong BulletsHit, 33 | ulong TimePlayed, 34 | ulong Deaths, 35 | ulong Revives, 36 | ulong Friendlies, 37 | ulong MissionSuccessRate, 38 | ulong Accurracy 39 | ); 40 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Mapping/V2/DispatchMapper.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Hdml; 2 | using Helldivers.Models.ArrowHead; 3 | using Helldivers.Models.Domain.Localization; 4 | using Helldivers.Models.V2; 5 | 6 | namespace Helldivers.Core.Mapping.V2; 7 | 8 | /// 9 | /// Handles mapping for . 10 | /// 11 | public sealed class DispatchMapper(HdmlParser parser) 12 | { 13 | /// 14 | /// Maps ArrowHead's onto V2's es. 15 | /// 16 | public IEnumerable MapToV2(MappingContext context) 17 | { 18 | // Get a list of all items across all translations. 19 | var invariants = context.NewsFeeds 20 | .SelectMany(pair => pair.Value) 21 | .DistinctBy(assignment => assignment.Id); 22 | 23 | foreach (var item in invariants) 24 | { 25 | // Build a dictionary of all translations for this item 26 | var translations = context.NewsFeeds.Select(pair => 27 | new KeyValuePair( 28 | pair.Key, 29 | pair.Value.FirstOrDefault(a => a.Id == item.Id) 30 | ) 31 | ).Where(pair => pair.Value is not null) 32 | .ToDictionary(pair => pair.Key, pair => pair.Value!); 33 | 34 | yield return MapToV2(context, translations); 35 | } 36 | } 37 | 38 | private Dispatch MapToV2(MappingContext context, Dictionary translations) 39 | { 40 | var invariant = translations.Values.First(); 41 | var messages = translations.Select(pair => 42 | new KeyValuePair(pair.Key, parser.Compile(pair.Value.Message))); 43 | 44 | return new Dispatch( 45 | Id: invariant.Id, 46 | Published: context.RelativeGameStart.AddSeconds(invariant.Published), 47 | Type: invariant.Type, 48 | Message: LocalizedMessage.FromStrings(messages) 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/Domain/Localization/LocalizedMessageConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Helldivers.Models.Domain.Localization; 6 | 7 | internal sealed class LocalizedMessageConverter : JsonConverter 8 | { 9 | /// 10 | public override LocalizedMessage? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 11 | { 12 | throw new InvalidOperationException($"Unable to deserialize {nameof(LocalizedMessage)} from JSON"); 13 | } 14 | 15 | /// 16 | public override void Write(Utf8JsonWriter writer, LocalizedMessage value, JsonSerializerOptions options) 17 | { 18 | // If the invariant culture is selected, serialize a dictionary with all languages. 19 | if (Equals(CultureInfo.CurrentCulture, LocalizedMessage.InvariantCulture)) 20 | { 21 | writer.WriteStartObject(); 22 | 23 | // We keep both country language and global language (eg. 'en-US' and 'en'), so we filter out the globals. 24 | foreach (var (language, message) in value.Messages.Where(culture => culture.Key.Name.Contains('-'))) 25 | writer.WriteString(language.Name, message); 26 | 27 | writer.WriteEndObject(); 28 | 29 | // Don't write out the translated string 30 | return; 31 | } 32 | 33 | if (value.Messages.TryGetValue(CultureInfo.CurrentCulture, out var result) is false) 34 | { 35 | if (value.Messages.TryGetValue(CultureInfo.CurrentCulture.Parent, out result) is false) 36 | { 37 | // Fall back to the configured default culture if one is available 38 | value.Messages.TryGetValue(LocalizedMessage.FallbackCulture, out result); 39 | } 40 | } 41 | 42 | if (string.IsNullOrWhiteSpace(result) is false) 43 | writer.WriteStringValue(result); 44 | else 45 | writer.WriteNullValue(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: [ "master" ] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | build: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | - name: Initial static JSON schema submodule 30 | run: git submodule update --init ./src/Helldivers-2-Models/json 31 | - name: Setup .NET 32 | uses: actions/setup-dotnet@v4 33 | with: 34 | dotnet-version: 10.0.x 35 | - name: Generate OpenAPI JSON files 36 | run: dotnet build -c Debug 37 | - name: Copy README 38 | run: cp README.md docs/README.md 39 | - name: Upload docsify website 40 | uses: actions/upload-artifact@master 41 | with: 42 | name: 'docs' 43 | path: './docs/' 44 | deploy: 45 | needs: build 46 | environment: 47 | name: github-pages 48 | url: ${{ steps.deployment.outputs.page_url }} 49 | runs-on: ubuntu-latest 50 | steps: 51 | - name: Download docsify website 52 | uses: actions/download-artifact@master 53 | with: 54 | name: 'docs' 55 | path: '.' 56 | - name: Setup Pages 57 | uses: actions/configure-pages@v4 58 | - name: Upload artifact 59 | uses: actions/upload-pages-artifact@v3 60 | with: 61 | path: '.' 62 | - name: Deploy to GitHub Pages 63 | id: deployment 64 | uses: actions/deploy-pages@v4 65 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Storage/StoreBase.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Contracts; 2 | using Helldivers.Core.Contracts.Collections; 3 | 4 | namespace Helldivers.Core.Storage; 5 | 6 | /// 7 | /// Base class for 8 | /// 9 | public abstract class StoreBase : IStore where T : class 10 | { 11 | private T _state = null!; 12 | private readonly TaskCompletionSource _syncState = new(); 13 | 14 | /// 15 | /// Updates the state of the store with the new value. 16 | /// 17 | public virtual ValueTask SetStore(T value) 18 | { 19 | _state = value; 20 | _syncState.TrySetResult(); 21 | 22 | return ValueTask.CompletedTask; 23 | } 24 | 25 | /// 26 | public virtual async Task Get(CancellationToken cancellationToken = default) 27 | { 28 | await _syncState.Task.WaitAsync(cancellationToken); 29 | 30 | return _state; 31 | } 32 | } 33 | 34 | /// 35 | /// Base class for 36 | /// 37 | public abstract class StoreBase : IStore where T : class 38 | { 39 | private List _state = null!; 40 | private readonly TaskCompletionSource _syncState = new(); 41 | 42 | /// 43 | /// Updates the state of the store with the new value. 44 | /// 45 | public virtual ValueTask SetStore(List value) 46 | { 47 | _state = value; 48 | _syncState.TrySetResult(); 49 | 50 | return ValueTask.CompletedTask; 51 | } 52 | 53 | /// 54 | public virtual async Task> AllAsync(CancellationToken cancellationToken = default) 55 | { 56 | await _syncState.Task.WaitAsync(cancellationToken); 57 | 58 | return _state; 59 | } 60 | 61 | /// 62 | public virtual async Task GetAsync(TKey key, CancellationToken cancellationToken = default) 63 | { 64 | await _syncState.Task.WaitAsync(cancellationToken); 65 | 66 | return _state.FirstOrDefault(value => GetAsyncPredicate(value, key)); 67 | } 68 | 69 | /// 70 | /// Handles the filter for 71 | /// 72 | protected abstract bool GetAsyncPredicate(T item, TKey key); 73 | } 74 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/V1/Planet.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models.Domain.Localization; 2 | using Helldivers.Models.V1.Planets; 3 | 4 | namespace Helldivers.Models.V1; 5 | 6 | /// 7 | /// Contains all aggregated information AH has about a planet. 8 | /// 9 | /// The unique identifier ArrowHead assigned to this planet. 10 | /// The name of the planet, as shown in game. 11 | /// The name of the sector the planet is in, as shown in game. 12 | /// The biome this planet has. 13 | /// All s that are applicable to this planet. 14 | /// A hash assigned to the planet by ArrowHead, purpose unknown. 15 | /// The coordinates of this planet on the galactic war map. 16 | /// A list of of all the planets to which this planet is connected. 17 | /// The maximum health pool of this planet. 18 | /// The current planet this planet has. 19 | /// Whether or not this planet is disabled, as assigned by ArrowHead. 20 | /// The faction that originally owned the planet. 21 | /// The faction that currently controls the planet. 22 | /// How much the planet regenerates per second if left alone. 23 | /// Information on the active event ongoing on this planet, if one is active. 24 | /// A set of statistics scoped to this planet. 25 | /// A list of integers that this planet is currently attacking. 26 | /// All regions on this planet, including their status (if any). 27 | public record Planet( 28 | int Index, 29 | LocalizedMessage Name, 30 | string Sector, 31 | Biome Biome, 32 | List Hazards, 33 | long Hash, 34 | Position Position, 35 | List Waypoints, 36 | long MaxHealth, 37 | long Health, 38 | bool Disabled, 39 | string InitialOwner, 40 | string CurrentOwner, 41 | double RegenPerSecond, 42 | Event? Event, 43 | Statistics Statistics, 44 | List Attacking, 45 | List Regions 46 | ); 47 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/Helldivers-2-API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | Helldivers.API 6 | Linux 7 | true 8 | true 9 | 10 | 11 | 12 | 13 | $(SolutionDir)/docs/openapi 14 | true 15 | true 16 | 17 | 18 | 19 | 20 | .dockerignore 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | all 43 | runtime; build; native; contentfiles; analyzers; buildtransitive 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /.github/workflows/sync.yml: -------------------------------------------------------------------------------- 1 | name: Validate sync 2 | 3 | permissions: 4 | contents: read 5 | pull-requests: write 6 | 7 | on: 8 | push: 9 | branches: ["master"] 10 | pull_request: 11 | branches: ["master"] 12 | schedule: 13 | - cron: '0 0 * * *' 14 | workflow_dispatch: 15 | 16 | jobs: 17 | validate-sync: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Setup .NET 24 | uses: actions/setup-dotnet@v4 25 | with: 26 | dotnet-version: '10.0.x' 27 | 28 | - name: Download JSON submodule 29 | run: git submodule update --init ./src/Helldivers-2-Models/json 30 | 31 | - name: Run sync and capture logs 32 | id: run_sync 33 | shell: bash 34 | continue-on-error: true 35 | run: | 36 | set -o pipefail 37 | dotnet build 38 | dotnet run --project ./src/Helldivers-2-CI/Helldivers-2-CI.csproj 2>&1 | tee sync.log 39 | 40 | - name: Upload artifacts 41 | if: always() 42 | uses: actions/upload-artifact@v4 43 | with: 44 | name: sync-artifacts 45 | path: | 46 | v1/*.json 47 | v2/*.json 48 | sync.log 49 | 50 | - name: Capture error log 51 | id: sync_log 52 | if: ${{ steps.run_sync.outcome == 'failure' && github.event_name == 'pull_request' }} 53 | run: | 54 | # open a multi-line output called "log" 55 | echo "log<> $GITHUB_OUTPUT 56 | cat sync.log >> $GITHUB_OUTPUT 57 | echo "EOF" >> $GITHUB_OUTPUT 58 | 59 | - name: Comment on PR on failure 60 | if: ${{ steps.run_sync.outcome == 'failure' && github.event_name == 'pull_request' }} 61 | uses: peter-evans/create-or-update-comment@v4 62 | with: 63 | token: ${{ secrets.GITHUB_TOKEN }} 64 | issue-number: ${{ github.event.pull_request.number }} 65 | body: | 66 | ⚠️ **Sync validation failed** (run #${{ github.run_number }} exited with ${{ steps.run_sync.outcome }}) 67 | 68 |
69 | Error log 70 | 71 | ```text 72 | ${{ steps.sync_log.outputs.log }} 73 | ``` 74 |
75 | 76 | **Artifacts** (JSON + log) here: 77 | ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 78 | 79 | - name: Fail job on error 80 | if: ${{ steps.run_sync.outcome == 'failure' }} 81 | run: exit 1 82 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Mapping/V1/StatisticsMapper.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models.V1; 2 | 3 | namespace Helldivers.Core.Mapping.V1; 4 | 5 | /// 6 | /// Handles mapping for . 7 | /// 8 | public sealed class StatisticsMapper 9 | { 10 | /// 11 | /// Maps galaxy wide statistics onto V1's statistics. 12 | /// 13 | public Statistics MapToV1(Models.ArrowHead.Summary.GalaxyStats statistics, List planets) 14 | { 15 | var playerCount = planets.Aggregate(0ul, (total, planet) => total + planet.Statistics.PlayerCount); 16 | 17 | return new Statistics( 18 | MissionsWon: statistics.MissionsWon, 19 | MissionsLost: statistics.MissionsLost, 20 | MissionTime: statistics.MissionTime, 21 | TerminidKills: statistics.BugKills, 22 | AutomatonKills: statistics.AutomatonKills, 23 | IlluminateKills: statistics.IlluminateKills, 24 | BulletsFired: statistics.BulletsFired, 25 | BulletsHit: statistics.BulletsHit, 26 | TimePlayed: statistics.TimePlayed, 27 | Deaths: statistics.Deaths, 28 | Revives: statistics.Revives, 29 | Friendlies: statistics.Friendlies, 30 | MissionSuccessRate: statistics.MissionSuccessRate, 31 | Accuracy: statistics.Accurracy, 32 | PlayerCount: playerCount 33 | ); 34 | } 35 | 36 | /// 37 | /// Maps statistics of a specific planet onto V1's statistics. 38 | /// 39 | public Statistics MapToV1(Models.ArrowHead.Summary.PlanetStats? statistics, Models.ArrowHead.Status.PlanetStatus status) 40 | { 41 | return new Statistics( 42 | MissionsWon: statistics?.MissionsWon ?? 0, 43 | MissionsLost: statistics?.MissionsLost ?? 0, 44 | MissionTime: statistics?.MissionTime ?? 0, 45 | TerminidKills: statistics?.BugKills ?? 0, 46 | AutomatonKills: statistics?.AutomatonKills ?? 0, 47 | IlluminateKills: statistics?.IlluminateKills ?? 0, 48 | BulletsFired: statistics?.BulletsFired ?? 0, 49 | BulletsHit: statistics?.BulletsHit ?? 0, 50 | TimePlayed: statistics?.TimePlayed ?? 0, 51 | Deaths: statistics?.Deaths ?? 0, 52 | Revives: statistics?.Revives ?? 0, 53 | Friendlies: statistics?.Friendlies ?? 0, 54 | MissionSuccessRate: statistics?.MissionSuccessRate ?? 0, 55 | Accuracy: statistics?.Accurracy ?? 0, 56 | PlayerCount: status.Players 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Facades/V1Facade.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Contracts; 2 | using Helldivers.Core.Contracts.Collections; 3 | using Helldivers.Core.Mapping; 4 | using Helldivers.Core.Mapping.V1; 5 | using Helldivers.Models.V1; 6 | 7 | namespace Helldivers.Core.Facades; 8 | 9 | /// 10 | /// Handles dispatching incoming data to all stores for V1. 11 | /// 12 | public sealed class V1Facade( 13 | IStore warStore, 14 | WarMapper warMapper, 15 | IStore planetStore, 16 | PlanetMapper planetMapper, 17 | IStore campaignStore, 18 | CampaignMapper campaignMapper, 19 | IStore assignmentStore, 20 | AssignmentMapper assignmentMapper, 21 | IStore dispatchStore, 22 | DispatchMapper dispatchMapper 23 | ) 24 | { 25 | /// 26 | public async ValueTask UpdateStores(MappingContext context) 27 | { 28 | // TODO: map warId 29 | 30 | await UpdatePlanetStore(context); 31 | // Some mappers need access to the list of planets, so we fetch it from the freshly-mapped store. 32 | var planets = await planetStore.AllAsync(); 33 | 34 | await UpdateWarStore(context, planets); 35 | await UpdateCampaignStore(context, planets); 36 | await UpdateAssignmentsStore(context); 37 | await UpdateDispatchStore(context); 38 | } 39 | 40 | private async ValueTask UpdateWarStore(MappingContext context, List planets) 41 | { 42 | var war = warMapper.MapToV1(context, planets); 43 | 44 | await warStore.SetStore(war); 45 | } 46 | 47 | private async ValueTask UpdatePlanetStore(MappingContext context) 48 | { 49 | var planets = planetMapper.MapToV1(context).ToList(); 50 | 51 | await planetStore.SetStore(planets); 52 | } 53 | 54 | private async ValueTask UpdateCampaignStore(MappingContext context, List planets) 55 | { 56 | var campaigns = campaignMapper.MapToV1(context, planets).ToList(); 57 | 58 | await campaignStore.SetStore(campaigns); 59 | } 60 | 61 | private async ValueTask UpdateAssignmentsStore(MappingContext context) 62 | { 63 | var assignments = assignmentMapper 64 | .MapToV1(context) 65 | .ToList(); 66 | 67 | await assignmentStore.SetStore(assignments); 68 | } 69 | 70 | private async ValueTask UpdateDispatchStore(MappingContext context) 71 | { 72 | var dispatches = dispatchMapper 73 | .MapToV1(context) 74 | .OrderByDescending(dispatch => dispatch.Id) 75 | .ToList(); 76 | 77 | await dispatchStore.SetStore(dispatches); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /src/Helldivers-2-Sync/Hosted/SteamSyncService.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core; 2 | using Helldivers.Sync.Configuration; 3 | using Helldivers.Sync.Services; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Extensions.Options; 8 | using Prometheus; 9 | 10 | namespace Helldivers.Sync.Hosted; 11 | 12 | /// 13 | /// Handles synchronization of Steam API information. 14 | /// 15 | public sealed partial class SteamSyncService( 16 | ILogger logger, 17 | StorageFacade storage, 18 | IServiceScopeFactory scopeFactory, 19 | IOptions configuration 20 | ) : BackgroundService 21 | { 22 | /// 23 | /// Timestamp the store was last updated successfully. 24 | /// 25 | public DateTime? LastUpdated { get; internal set; } 26 | 27 | private static readonly Histogram SteamSyncMetric = 28 | Metrics.CreateHistogram("helldivers_sync_steam", "All Steam synchronizations"); 29 | 30 | #region Source generated logging 31 | 32 | [LoggerMessage(Level = LogLevel.Error, Message = "An exception was thrown when synchronizing from Steam API")] 33 | private static partial void LogSyncThrewAnError(ILogger logger, Exception exception); 34 | 35 | [LoggerMessage(Level = LogLevel.Information, Message = "SteamSyncService finished processing, shutting down.")] 36 | private static partial void LogRunOnceCompleted(ILogger logger); 37 | 38 | #endregion 39 | 40 | /// 41 | protected override async Task ExecuteAsync(CancellationToken cancellationToken) 42 | { 43 | var delay = TimeSpan.FromHours(1); 44 | 45 | while (cancellationToken.IsCancellationRequested is false) 46 | { 47 | try 48 | { 49 | using var _ = SteamSyncMetric.NewTimer(); 50 | await using var scope = scopeFactory.CreateAsyncScope(); 51 | var api = scope.ServiceProvider.GetRequiredService(); 52 | 53 | var feed = await api.GetLatest(); 54 | 55 | await storage.UpdateStores(feed); 56 | LastUpdated = DateTime.UtcNow; 57 | } 58 | catch (Exception exception) 59 | { 60 | LogSyncThrewAnError(logger, exception); 61 | 62 | if (configuration.Value.RunOnce) 63 | throw; 64 | } 65 | 66 | 67 | // If we should only run once, we exit the loop (and thus service) after this. 68 | if (configuration.Value.RunOnce) 69 | { 70 | LogRunOnceCompleted(logger); 71 | return; 72 | } 73 | await Task.Delay(delay, cancellationToken); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Helldivers-2-Models/Domain/Localization/LocalizedMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Helldivers.Models.Domain.Localization; 5 | 6 | /// 7 | /// Represents a string that is available in multiple languages. 8 | /// 9 | /// A list of all messages and the in which they are available. 10 | [JsonConverter(typeof(LocalizedMessageConverter))] 11 | public sealed record LocalizedMessage(IReadOnlyDictionary Messages) 12 | { 13 | /// 14 | /// The culture to retrieve when the is not available. 15 | /// 16 | public static CultureInfo FallbackCulture { get; set; } = null!; 17 | 18 | /// 19 | /// Represents the as a named culture that can be assigned by the request 20 | /// localization middleware. 21 | /// Selecting this culture will result in all languages being serialized at once. 22 | /// 23 | public static readonly CultureInfo InvariantCulture = new CultureInfo("ivl-IV"); 24 | 25 | /// 26 | /// Factory method for creating a from a list of {language, value} pairs. 27 | /// 28 | public static LocalizedMessage FromStrings(IEnumerable> values) 29 | { 30 | var messages = values.SelectMany(pair => 31 | { 32 | var (key, value) = pair; 33 | var culture = new CultureInfo(key); 34 | var parent = culture.Parent; 35 | 36 | return new[] 37 | { 38 | new KeyValuePair(culture, value), 39 | new KeyValuePair(parent, value) 40 | }; 41 | }) 42 | .DistinctBy(pair => pair.Key) 43 | .ToDictionary(pair => pair.Key, pair => pair.Value); 44 | 45 | return new LocalizedMessage(messages); 46 | } 47 | 48 | /// 49 | /// Factory method for creating a from a list of {language, value} pairs. 50 | /// 51 | public static LocalizedMessage FromStrings(IEnumerable values) 52 | { 53 | var messages = values.SelectMany(pair => 54 | { 55 | var key = pair[0]; 56 | var value = pair[1]; 57 | 58 | var culture = new CultureInfo(key); 59 | var parent = culture.Parent; 60 | 61 | return new[] 62 | { 63 | new KeyValuePair(culture, value), 64 | new KeyValuePair(parent, value) 65 | }; 66 | }) 67 | .DistinctBy(pair => pair.Key) 68 | .ToDictionary(pair => pair.Key, pair => pair.Value); 69 | 70 | return new LocalizedMessage(messages); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Helldivers-2-SourceGen/Helldivers-2-SourceGen.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | false 6 | enable 7 | latest 8 | 9 | true 10 | true 11 | 12 | Helldivers.SourceGen 13 | Helldivers.SourceGen 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | @(NuGetPackageId -> Distinct()) 42 | 43 | 44 | 45 | 47 | 48 | 49 | 50 | @(PackageReferenceDependency -> '%(LocalSourceGenerators)') 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/Metrics/ClientMetric.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace Helldivers.API.Metrics; 4 | 5 | /// 6 | /// Handles the logic for generating the `Client` label in the Prometheus metrics for HTTP requests. 7 | /// 8 | public static partial class ClientMetric 9 | { 10 | /// 11 | /// The name of a client that couldn't be identified. 12 | /// 13 | private const string UNKNOWN_CLIENT_NAME = "Unknown"; 14 | 15 | /// 16 | /// The name for clients that are developing locally (e.g. localhost, ...). 17 | /// 18 | private const string LOCAL_DEVELOPMENT_CLIENT_NAME = "Development"; 19 | 20 | [GeneratedRegex(@"^[a-z0-9]{24}--.+\.netlify\.app$")] 21 | private static partial Regex IsNetlifyApp(); 22 | 23 | /// 24 | /// Extracts the name of the client for the incoming request. 25 | /// 26 | public static string GetClientName(HttpContext context) 27 | { 28 | // If we have an authenticated user, use their name instead 29 | if (context.User.Identity is { Name: { } name }) 30 | return name; 31 | 32 | // If the client sends `X-Super-Client` we use that name 33 | if (context.Request.Headers.TryGetValue(Constants.CLIENT_HEADER_NAME, out var superClient)) 34 | if (string.IsNullOrWhiteSpace(superClient) is false) 35 | return superClient!; 36 | 37 | if (GetBrowserClientName(context.Request) is { } clientName) 38 | return clientName; 39 | 40 | return UNKNOWN_CLIENT_NAME; 41 | } 42 | 43 | /// 44 | /// If the client is a browser it sends specific headers along. 45 | /// Attempt to parse out those headers to get the domain and perform some additional normalization. 46 | /// 47 | /// If we can't find any of those headers simply return null 48 | /// 49 | private static string? GetBrowserClientName(HttpRequest request) 50 | { 51 | string? result = null; 52 | if (string.IsNullOrWhiteSpace(request.Headers.Referer) is false) 53 | result = request.Headers.Referer!; 54 | if (string.IsNullOrWhiteSpace(request.Headers.Origin) is false) 55 | result = request.Headers.Origin!; 56 | 57 | if (result is not null && Uri.TryCreate(result, UriKind.Absolute, out var uri)) 58 | { 59 | // Localhost etc just return local development for a client name. 60 | if (uri.Host.Equals("localhost", StringComparison.InvariantCultureIgnoreCase)) 61 | return LOCAL_DEVELOPMENT_CLIENT_NAME; 62 | 63 | // Netlify seems to generate urls with random prefixes like following: 64 | // <24 characters random>--.netlify.app 65 | // we normalize to .netlify.app 66 | if (IsNetlifyApp().IsMatch(uri.Host)) 67 | return uri.Host[26..]; 68 | 69 | return uri.Host; 70 | } 71 | 72 | return result; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Mapping/V1/AssignmentMapper.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models.Domain.Localization; 2 | using Helldivers.Models.V1; 3 | using Helldivers.Models.V1.Assignments; 4 | using Task = Helldivers.Models.V1.Assignments.Task; 5 | 6 | namespace Helldivers.Core.Mapping.V1; 7 | 8 | /// 9 | /// Handles mapping for . 10 | /// 11 | public sealed class AssignmentMapper 12 | { 13 | /// 14 | /// Maps a set of multi-language s into a list of . 15 | /// 16 | public IEnumerable MapToV1(MappingContext context) 17 | { 18 | // Get a list of all assignments across all translations. 19 | var invariants = context.Assignments 20 | .SelectMany(pair => pair.Value) 21 | .DistinctBy(assignment => assignment.Id32); 22 | 23 | foreach (var assignment in invariants) 24 | { 25 | // Build a dictionary of all translations for this assignment 26 | var translations = context.Assignments.Select(pair => 27 | new KeyValuePair( 28 | pair.Key, 29 | pair.Value.FirstOrDefault(a => a.Id32 == assignment.Id32) 30 | ) 31 | ).Where(pair => pair.Value is not null) 32 | .ToDictionary(pair => pair.Key, pair => pair.Value!); 33 | 34 | yield return MapToV1(translations); 35 | } 36 | } 37 | 38 | /// 39 | /// Maps all translations of an into one assignment. 40 | /// 41 | private Assignment MapToV1(Dictionary translations) 42 | { 43 | var invariant = translations.First().Value; 44 | var titles = translations.Select(assignment => new KeyValuePair(assignment.Key, assignment.Value.Setting.OverrideTitle)); 45 | var briefings = translations.Select(assignment => new KeyValuePair(assignment.Key, assignment.Value.Setting.OverrideBrief)); 46 | var descriptions = translations.Select(assignment => new KeyValuePair(assignment.Key, assignment.Value.Setting.TaskDescription)); 47 | var expiration = DateTime.UtcNow.AddSeconds(invariant.ExpiresIn); 48 | 49 | return new Assignment( 50 | Id: invariant.Id32, 51 | Progress: invariant.Progress, 52 | Title: LocalizedMessage.FromStrings(titles), 53 | Briefing: LocalizedMessage.FromStrings(briefings), 54 | Description: LocalizedMessage.FromStrings(descriptions), 55 | Tasks: invariant.Setting.Tasks.Select(MapToV1).ToList(), 56 | Reward: MapToV1(invariant.Setting.Reward), 57 | Rewards: invariant.Setting.Rewards.Select(MapToV1).Where(reward => reward is not null).ToList()!, 58 | Expiration: expiration, 59 | Flags: invariant.Setting.Flags 60 | ); 61 | } 62 | 63 | private Task MapToV1(Models.ArrowHead.Assignments.Task task) 64 | { 65 | return new Task( 66 | Type: task.Type, 67 | Values: task.Values, 68 | ValueTypes: task.ValueTypes 69 | ); 70 | } 71 | 72 | private Reward? MapToV1(Models.ArrowHead.Assignments.Reward? reward) 73 | { 74 | if (reward is null) 75 | return null; 76 | 77 | return new Reward( 78 | Type: reward.Type, 79 | Amount: reward.Amount 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Helldivers-2-SourceGen/StaticJsonSourceGenerator.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.SourceGen.Contracts; 2 | using Helldivers.SourceGen.Parsers; 3 | using Microsoft.CodeAnalysis; 4 | 5 | namespace Helldivers.SourceGen; 6 | 7 | /// 8 | /// A sample source generator that creates C# classes based on the text file (in this case, Domain Driven Design ubiquitous language registry). 9 | /// When using a simple text file as a baseline, we can create a non-incremental source generator. 10 | /// 11 | [Generator] 12 | public class StaticJsonSourceGenerator : IIncrementalGenerator 13 | { 14 | private static readonly IJsonParser PlanetParser = new PlanetsParser(); 15 | private static readonly IJsonParser PlanetRegionParser = new PlanetRegionsParser(); 16 | private static readonly IJsonParser BiomesParser = new BiomesParser(); 17 | private static readonly IJsonParser EnvironmentalsParser = new EnvironmentalsParser(); 18 | private static readonly IJsonParser FactionsParser = new FactionsParser(); 19 | 20 | /// 21 | public void Initialize(IncrementalGeneratorInitializationContext context) 22 | { 23 | var generated = context 24 | .AdditionalTextsProvider 25 | .Where(static file => file.Path.EndsWith(".json")) 26 | .Select(static (file, cancellationToken) => 27 | { 28 | var parser = GetParserForFile(file); 29 | 30 | var name = Path.GetFileNameWithoutExtension(file.Path); 31 | 32 | return (parser.Parse(file, cancellationToken), name); 33 | }); 34 | 35 | context.RegisterSourceOutput(generated, static (context, pair) => 36 | { 37 | var (source, name) = pair; 38 | 39 | try 40 | { 41 | context.AddSource(name, source); 42 | } 43 | catch (Exception exception) 44 | { 45 | context.AddSource($"{name}.g.cs", $"// An exception was thrown processing {name}.json\n{exception.ToString()}"); 46 | context.ReportDiagnostic( 47 | Diagnostic.Create( 48 | new DiagnosticDescriptor( 49 | id: "HDJSON", // Unique ID for your error 50 | title: "JSON source generator failed", // Title of the error 51 | messageFormat: $"An error occured generating C# code from JSON files: {exception}", // Message format 52 | category: "HD2", // Category of the error 53 | DiagnosticSeverity.Error, // Severity of the error 54 | isEnabledByDefault: true // Whether the error is enabled by default 55 | ), 56 | Location.None // No specific location provided for simplicity 57 | ) 58 | ); 59 | } 60 | }); 61 | } 62 | 63 | private static IJsonParser GetParserForFile(AdditionalText file) 64 | { 65 | var name = Path.GetFileNameWithoutExtension(file.Path); 66 | name = $"{char.ToUpper(name[0])}{name.Substring(1)}"; 67 | 68 | return name.ToLowerInvariant() switch 69 | { 70 | "planets" => PlanetParser, 71 | "planetregion" => PlanetRegionParser, 72 | "biomes" => BiomesParser, 73 | "environmentals" => EnvironmentalsParser, 74 | "factions" => FactionsParser, 75 | _ => throw new Exception($"Generator does not know how to parse {name}") 76 | }; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Mapping/V2/SpaceStationMapper.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Hdml; 2 | using Helldivers.Models.V1; 3 | using Helldivers.Models.V2; 4 | using Helldivers.Models.V2.SpaceStations; 5 | 6 | namespace Helldivers.Core.Mapping.V2; 7 | 8 | /// 9 | /// Handles mapping for . 10 | /// 11 | public sealed class SpaceStationMapper(HdmlParser parser) 12 | { 13 | /// 14 | /// Maps all space stations in the . 15 | /// 16 | /// The mapping context containing the invariant war status and other relevant data. 17 | /// The list of planets to map with. 18 | /// An enumerable list of space stations mapped to the V1 model. 19 | public IEnumerable MapToV2(MappingContext context, List planets) 20 | { 21 | // Get a list of all assignments across all translations. 22 | var invariants = context.SpaceStations 23 | .SelectMany(pair => pair.Value) 24 | .DistinctBy(spaceStation => spaceStation.Id32); 25 | 26 | foreach (var spaceStation in invariants) 27 | { 28 | // Build a dictionary of all translations for this assignment 29 | var translations = context.SpaceStations.Select(pair => 30 | new KeyValuePair( 31 | pair.Key, 32 | pair.Value.FirstOrDefault(a => a.Id32 == spaceStation.Id32) 33 | ) 34 | ).Where(pair => pair.Value is not null) 35 | .ToDictionary(pair => pair.Key, pair => pair.Value!); 36 | 37 | yield return Map(translations, context, planets); 38 | } 39 | } 40 | 41 | private SpaceStation Map(Dictionary translations, MappingContext context, List planets) 42 | { 43 | var invariant = translations.First().Value; 44 | var planet = planets.First(p => p.Index == invariant.PlanetIndex); 45 | 46 | return new SpaceStation( 47 | Id32: invariant.Id32, 48 | Planet: planet, 49 | ElectionEnd: context.RelativeGameStart.AddSeconds(invariant.CurrentElectionEndWarTime), 50 | Flags: invariant.Flags, 51 | TacticalActions: invariant.TacticalActions.Select(rawAction => MapTacticalAction(context, rawAction)).ToList() 52 | ); 53 | } 54 | 55 | private TacticalAction MapTacticalAction(MappingContext context, Helldivers.Models.ArrowHead.SpaceStations.TacticalAction raw) 56 | { 57 | return new TacticalAction( 58 | Id32: raw.Id32, 59 | MediaId32: raw.MediaId32, 60 | Name: raw.Name, 61 | Description: raw.Description, 62 | StrategicDescription: parser.Compile(raw.StrategicDescription), 63 | Status: raw.Status, 64 | StatusExpire: context.RelativeGameStart.AddSeconds(raw.StatusExpireAtWarTimeSeconds), 65 | Costs: raw.Cost.Select(MapCost).ToList(), 66 | EffectIds: raw.EffectIds 67 | ); 68 | } 69 | 70 | private Cost MapCost(Models.ArrowHead.SpaceStations.Cost cost) 71 | { 72 | return new Cost( 73 | Id: cost.Id, 74 | ItemMixId: cost.ItemMixId, 75 | TargetValue: cost.TargetValue, 76 | CurrentValue: cost.CurrentValue, 77 | DeltaPerSecond: cost.DeltaPerSecond, 78 | MaxDonationAmmount: cost.MaxDonationAmmount, 79 | MaxDonationPeriodSeconds: cost.MaxDonationPeriodSeconds 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # helldivers-2/api 2 | 3 | The `helldivers-2/api` project is a community API around the popular video game 4 | [Helldivers 2](https://store.steampowered.com/app/553850/HELLDIVERS_2/?curator_clanid=33602140) by ArrowHead studios. 5 | It provides (most) information you can also find in-game in a neat JSON format so you can build awesome applications 6 | (see [Community](#community) below). 7 | 8 | Important to note is that this is **not** an officially endorsed API and is in **no** way affiliated with ArrowHead studios, 9 | but rather a community driven effort to provide the information of Helldivers 2 to it's community. 10 | 11 | ### Getting started 12 | The API does not require authentication (unless you'd like higher rate limits than the default, see [rate limits](#rate-limits)), 13 | so all you need to do is call it's publicly available endpoints. 14 | 15 | We provide an [OpenAPI](https://helldivers-2.github.io/api/openapi/Helldivers-2-API.json) specification of the community API as well as 16 | a [SwaggerUI](https://helldivers-2.github.io/api/openapi/swagger-ui.html) (which visualizes the OpenAPI document). We also provide an 17 | [OpenAPI](https://helldivers-2.github.io/api/openapi/Helldivers-2-API_arrowhead.json) of the official ArrowHead studio's API we use 18 | internally, however we strongly encourage you to use the `/raw` endpoints of the community wrapper instead of accessing 19 | the ArrowHead API directly, as it puts additional load on their servers (besides, we have shinier endpoints, I swear!). 20 | 21 | The root URL of the API is available here: https://api.helldivers2.dev 22 | > [!WARNING] 23 | > The root domain of the API recently changed, it's recommended you use the domain above to avoid problems in the future 24 | 25 | We ask that you send along a `X-Super-Client` header with the name of your application / domain 26 | (e.g. `X-Super-Client: api.helldivers2.dev`) and optionally a `X-Super-Contact` with some form of contact if your site 27 | does not link to any form of contact information we can find. We use this information in case we need to notify our users 28 | of important changes to the API that may cause disruption of service or when additional restrictions would be imposed 29 | on your app (to prevent abuse, unintentional or otherwise). 30 | 31 | > [!IMPORTANT] 32 | > While adding `X-Super-Client` and `X-Super-Contact` is currently not required, the `X-Super-Client` header **will** 33 | > be made obligatory in the future, causing clients who don't send it to fail. For more information see #94 34 | 35 | ### Rate limits 36 | Currently the rate limit is set at 5 requests/10 seconds. 37 | This limit will probably be increased in the future, and is used during the transition to our new version. 38 | 39 | To avoid hitting rate limits in your clients check the following headers in your response: 40 | - `X-Ratelimit-Limit` contains the total amount of requests you can make in the given timeframe 41 | - `X-RateLimit-Remaining` how many requests you can still make in the current window 42 | - `Retry-After` only added to 429 requests, the amount of seconds to wait before making a new request 43 | 44 | ### Contributing 45 | make sure to check out our [contribution](CONTRIBUTING.md) guidelines for more detailed information on how to 46 | help us contributing! 47 | 48 | ### Community 49 | Check out some awesome projects made by our community! 50 | - [sebsebmc/helldivers-history](https://github.com/sebsebmc/helldivers-history) 51 | Dashboard and graphs made by git scraping the community made API 52 | - [stonemercy/galactic-wide-web](https://github.com/Stonemercy/Galactic-Wide-Web) 53 | The Galactic Wide Web - a discord bot 54 | - [helldivers-2/api-wrapper](https://github.com/helldivers-2/api-wrapper) 55 | Typescript client code generated from OpenAPI 56 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/Controllers/ArrowHeadController.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Storage.ArrowHead; 2 | using Helldivers.Models.ArrowHead; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace Helldivers.API.Controllers; 6 | 7 | /// 8 | /// Contains API endpoints to mimic the endpoints exposed by the official ArrowHead API. 9 | /// 10 | public static class ArrowHeadController 11 | { 12 | /// 13 | /// Returns the currently active war season ID. 14 | /// 15 | [ProducesResponseType(StatusCodes.Status200OK)] 16 | public static async Task WarId(HttpContext context, ArrowHeadStore store) 17 | { 18 | var warId = await store.GetWarId(context.RequestAborted); 19 | 20 | return Results.Bytes(warId, contentType: "application/json"); 21 | } 22 | 23 | /// 24 | /// Get a snapshot of the current war status. 25 | /// 26 | [ProducesResponseType(StatusCodes.Status200OK)] 27 | public static async Task Status(HttpContext context, ArrowHeadStore store) 28 | { 29 | var status = await store.GetWarStatus(context.RequestAborted); 30 | 31 | return Results.Bytes(status, contentType: "application/json"); 32 | } 33 | 34 | /// 35 | /// Gets the current war info. 36 | /// 37 | [ProducesResponseType(StatusCodes.Status200OK)] 38 | public static async Task WarInfo(HttpContext context, ArrowHeadStore store) 39 | { 40 | var info = await store.GetWarInfo(context.RequestAborted); 41 | 42 | return Results.Bytes(info, contentType: "application/json"); 43 | } 44 | 45 | /// 46 | /// Gets the current war info. 47 | /// 48 | [ProducesResponseType(StatusCodes.Status200OK)] 49 | public static async Task Summary(HttpContext context, ArrowHeadStore store) 50 | { 51 | var summary = await store.GetWarSummary(context.RequestAborted); 52 | 53 | return Results.Bytes(summary, contentType: "application/json"); 54 | } 55 | 56 | /// 57 | /// Retrieves a list of news messages from Super Earth. 58 | /// 59 | [ProducesResponseType>(StatusCodes.Status200OK)] 60 | public static async Task NewsFeed(HttpContext context, ArrowHeadStore store) 61 | { 62 | var items = await store.GetNewsFeeds(context.RequestAborted); 63 | 64 | return Results.Bytes(items, contentType: "application/json"); 65 | } 66 | 67 | /// 68 | /// Retrieves a list of currently active assignments (like Major Orders). 69 | /// 70 | [ProducesResponseType>(StatusCodes.Status200OK)] 71 | [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] 72 | public static async Task Assignments(HttpContext context, ArrowHeadStore store) 73 | { 74 | var assignments = await store.GetAssignments(context.RequestAborted); 75 | 76 | return Results.Bytes(assignments, contentType: "application/json"); 77 | } 78 | 79 | /// 80 | /// Fetches THE specific (749875195). 81 | /// 82 | [ProducesResponseType>(StatusCodes.Status200OK)] 83 | [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] 84 | public static async Task SpaceStation(HttpContext context, ArrowHeadStore store, [FromRoute] long index) 85 | { 86 | var spaceStation = await store.GetSpaceStation(index, context.RequestAborted); 87 | if (spaceStation is { } bytes) 88 | return Results.Bytes(bytes, contentType: "application/json"); 89 | 90 | return Results.NotFound(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/StorageFacade.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Facades; 2 | using Helldivers.Core.Mapping; 3 | using Helldivers.Core.Storage.ArrowHead; 4 | using Helldivers.Models; 5 | using Helldivers.Models.Steam; 6 | using System.Text.Json; 7 | using System.Text.Json.Serialization.Metadata; 8 | 9 | namespace Helldivers.Core; 10 | 11 | /// 12 | /// Rather than having the sync service be aware of all mappings and storage versions, 13 | /// this facade class handles dispatching incoming data to the correct underlying stores. 14 | /// 15 | public sealed class StorageFacade(ArrowHeadStore arrowHead, SteamFacade steam, V1Facade v1, V2Facade v2) 16 | { 17 | /// 18 | /// Updates all stores that rely on . 19 | /// 20 | public ValueTask UpdateStores(SteamNewsFeed feed) 21 | => steam.UpdateStores(feed); 22 | 23 | /// 24 | /// Updates all stores that rely on ArrowHead's models. 25 | /// 26 | public async ValueTask UpdateStores(Memory rawWarId, 27 | Memory rawWarInfo, 28 | Dictionary> rawWarStatuses, 29 | Memory rawWarSummary, 30 | Dictionary> rawNewsFeeds, 31 | Dictionary> rawAssignments, 32 | Dictionary>> rawStations) 33 | { 34 | arrowHead.UpdateRawStore( 35 | rawWarId, 36 | rawWarInfo, 37 | rawWarSummary, 38 | rawWarStatuses, 39 | rawNewsFeeds, 40 | rawAssignments, 41 | rawStations 42 | ); 43 | 44 | var warId = DeserializeOrThrow(rawWarId, ArrowHeadSerializerContext.Default.WarId); 45 | var warInfo = DeserializeOrThrow(rawWarInfo, ArrowHeadSerializerContext.Default.WarInfo); 46 | var warStatuses = rawWarStatuses.ToDictionary( 47 | pair => pair.Key, 48 | pair => DeserializeOrThrow(pair.Value, ArrowHeadSerializerContext.Default.WarStatus) 49 | ); 50 | var warSummary = DeserializeOrThrow(rawWarSummary, ArrowHeadSerializerContext.Default.WarSummary); 51 | var newsFeeds = rawNewsFeeds.ToDictionary( 52 | pair => pair.Key, 53 | pair => DeserializeOrThrow(pair.Value, ArrowHeadSerializerContext.Default.ListNewsFeedItem) 54 | ); 55 | var assignments = rawAssignments.ToDictionary( 56 | pair => pair.Key, 57 | pair => DeserializeOrThrow(pair.Value, ArrowHeadSerializerContext.Default.ListAssignment) 58 | ); 59 | 60 | var spaceStations = new Dictionary>(); 61 | foreach (var (id, translations) in rawStations) 62 | { 63 | foreach (var (lang, translation) in translations) 64 | { 65 | if (spaceStations.ContainsKey(lang) is false) 66 | spaceStations.Add(lang, new List()); 67 | 68 | var stations = spaceStations[lang]; 69 | stations.Add(DeserializeOrThrow(translation, ArrowHeadSerializerContext.Default.SpaceStation)); 70 | } 71 | } 72 | 73 | var context = new MappingContext( 74 | warId, 75 | warInfo, 76 | warStatuses, 77 | warSummary, 78 | newsFeeds, 79 | assignments, 80 | spaceStations 81 | ); 82 | 83 | await v1.UpdateStores(context); 84 | await v2.UpdateStores(context); 85 | } 86 | 87 | private T DeserializeOrThrow(Memory memory, JsonTypeInfo typeInfo) 88 | => JsonSerializer.Deserialize(memory.Span, typeInfo) ?? throw new InvalidOperationException(); 89 | } 90 | -------------------------------------------------------------------------------- /src/Helldivers-2-CI/Program.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Contracts.Collections; 2 | using Helldivers.Core.Extensions; 3 | using Helldivers.Models.Domain.Localization; 4 | using Helldivers.Models.V1; 5 | using Helldivers.Models.V2; 6 | using Helldivers.Sync.Configuration; 7 | using Helldivers.Sync.Extensions; 8 | using Helldivers.Sync.Hosted; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Hosting; 11 | using Microsoft.Extensions.Logging; 12 | using System.Diagnostics; 13 | using System.Globalization; 14 | using System.Text.Json; 15 | using Dispatch = Helldivers.Models.V1.Dispatch; 16 | 17 | var maxRuntime = new CancellationTokenSource(TimeSpan.FromSeconds(30)); 18 | var builder = new HostApplicationBuilder(args); 19 | 20 | LocalizedMessage.FallbackCulture = new CultureInfo("en-US"); 21 | builder.Services.AddHelldivers(); 22 | builder.Services.AddHelldiversSync(); 23 | builder.Services.Configure(configuration => 24 | { 25 | configuration.RunOnce = true; 26 | // Add all languages we want to CI test here. 27 | configuration.Languages = 28 | [ 29 | "en-US", 30 | "de-DE", 31 | "es-ES", 32 | "ru-RU", 33 | "fr-FR", 34 | "it-IT", 35 | "pl-PL", 36 | "zh-Hans", 37 | "zh-Hant" 38 | ]; 39 | }); 40 | 41 | var stopwatch = new Stopwatch(); 42 | var app = builder.Build(); 43 | 44 | // Run our host, but make it shutdown after *max* 30 seconds. 45 | stopwatch.Start(); 46 | var arrowhead = app.Services.GetRequiredService(); 47 | var steam = app.Services.GetRequiredService(); 48 | await arrowhead.StartAsync(maxRuntime.Token); 49 | await steam.StartAsync(maxRuntime.Token); 50 | 51 | // now we await completion of both. 52 | await Task.WhenAll([ 53 | arrowhead.ExecuteTask!, 54 | steam.ExecuteTask!, 55 | ]).WaitAsync(maxRuntime.Token); 56 | stopwatch.Stop(); 57 | 58 | app.Services.GetRequiredService>().LogInformation("Sync succeeded in {}", stopwatch.Elapsed); 59 | 60 | // Fetch all information from the stores to write to disk. 61 | var assignments = await app.Services.GetRequiredService>().AllAsync(maxRuntime.Token); 62 | var campaigns = await app.Services.GetRequiredService>().AllAsync(maxRuntime.Token); 63 | var dispatchesv1 = await app.Services.GetRequiredService>().AllAsync(maxRuntime.Token); 64 | var planets = await app.Services.GetRequiredService>().AllAsync(maxRuntime.Token); 65 | var war = await app.Services.GetRequiredService>().Get(maxRuntime.Token); 66 | var dispatchesv2 = await app.Services.GetRequiredService>().AllAsync(maxRuntime.Token); 67 | var store = await app.Services.GetRequiredService>().AllAsync(maxRuntime.Token); 68 | 69 | Directory.CreateDirectory("v1"); 70 | Directory.CreateDirectory("v2"); 71 | 72 | var options = new JsonSerializerOptions { WriteIndented = true }; 73 | await File.WriteAllBytesAsync("v1/assignments.json", JsonSerializer.SerializeToUtf8Bytes(assignments, options), maxRuntime.Token); 74 | await File.WriteAllBytesAsync("v1/campaigns.json", JsonSerializer.SerializeToUtf8Bytes(campaigns, options), maxRuntime.Token); 75 | await File.WriteAllBytesAsync("v1/dispatches.json", JsonSerializer.SerializeToUtf8Bytes(dispatchesv1, options), maxRuntime.Token); 76 | await File.WriteAllBytesAsync("v1/planets.json", JsonSerializer.SerializeToUtf8Bytes(planets, options), maxRuntime.Token); 77 | await File.WriteAllBytesAsync("v1/war.json", JsonSerializer.SerializeToUtf8Bytes(war, options), maxRuntime.Token); 78 | await File.WriteAllBytesAsync("v2/dispatches.json", JsonSerializer.SerializeToUtf8Bytes(dispatchesv2, options), maxRuntime.Token); 79 | await File.WriteAllBytesAsync("v2/space-stations.json", JsonSerializer.SerializeToUtf8Bytes(store, options), maxRuntime.Token); 80 | 81 | return 0; 82 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Mapping/MappingContext.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Models.ArrowHead; 2 | 3 | namespace Helldivers.Core.Mapping; 4 | 5 | /// 6 | /// Contains the context of the ArrowHead models currently being mapped. 7 | /// It's main purpose is to provide a convenient way to pass around all ArrowHead data 8 | /// at once without having to pass 7-8 parameters to each function (or when adding more data, 9 | /// having to update all dependent signatures). 10 | /// 11 | public sealed class MappingContext 12 | { 13 | /// The currently being mapped. 14 | public WarId WarId { get; private init; } 15 | 16 | /// The currently being mapped. 17 | public WarInfo WarInfo { get; private init; } 18 | 19 | /// The es currently being mapped. 20 | public Dictionary WarStatuses { get; private init; } 21 | 22 | /// A that can be used when accessing non-localized data. 23 | public WarStatus InvariantWarStatus { get; private init; } 24 | 25 | /// The currently being mapped. 26 | public WarSummary WarSummary { get; private init; } 27 | 28 | /// The s currently being mapped. 29 | public Dictionary> NewsFeeds { get; private init; } 30 | 31 | /// The s currently being mapped. 32 | public Dictionary> Assignments { get; private init; } 33 | 34 | /// The s currently being mapped. 35 | public Dictionary> SpaceStations { get; private init; } 36 | 37 | /// 38 | /// A that represents the 'start' of the time in Helldivers 2. 39 | /// This accounts for the and . 40 | /// 41 | public DateTime RelativeGameStart { get; private init; } 42 | 43 | /// 44 | /// There's a deviation between gametime and real world time. 45 | /// When calculating dates using ArrowHead timestamps, add this to compensate for the deviation. 46 | /// 47 | public TimeSpan GameTimeDeviation { get; private init; } 48 | 49 | /// Initializes a new . 50 | internal MappingContext(WarId warId, 51 | WarInfo warInfo, 52 | Dictionary warStatuses, 53 | WarSummary warSummary, 54 | Dictionary> newsFeeds, 55 | Dictionary> assignments, 56 | Dictionary> spaceStations) 57 | { 58 | WarId = warId; 59 | WarInfo = warInfo; 60 | WarStatuses = warStatuses; 61 | WarSummary = warSummary; 62 | NewsFeeds = newsFeeds; 63 | Assignments = assignments; 64 | SpaceStations = spaceStations; 65 | 66 | InvariantWarStatus = warStatuses.FirstOrDefault().Value 67 | ?? throw new InvalidOperationException("No warstatus available"); 68 | 69 | 70 | var gameTime = DateTime.UnixEpoch.AddSeconds(warInfo.StartDate + InvariantWarStatus.Time); 71 | GameTimeDeviation = TruncateToSeconds(DateTime.UtcNow).Subtract(gameTime); 72 | RelativeGameStart = DateTime.UnixEpoch.Add(GameTimeDeviation).AddSeconds(warInfo.StartDate); 73 | } 74 | 75 | /// 76 | /// ArrowHead doesn't send timestamps more accurate than seconds, so we truncate our relative time to seconds. 77 | /// This prevents timestamps for the same value from being different (due to milli/micro second differences). 78 | /// 79 | private static DateTime TruncateToSeconds(DateTime dateTime) => dateTime.AddTicks(-(dateTime.Ticks % TimeSpan.TicksPerSecond)); 80 | } 81 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Contracts; 2 | using Helldivers.Core.Contracts.Collections; 3 | using Helldivers.Core.Facades; 4 | using Helldivers.Core.Hdml; 5 | using Helldivers.Core.Mapping.Steam; 6 | using Helldivers.Core.Mapping.V1; 7 | using Helldivers.Core.Storage.ArrowHead; 8 | using Helldivers.Core.Storage.Steam; 9 | using Helldivers.Core.Storage.V1; 10 | using Helldivers.Models.V1; 11 | using Microsoft.Extensions.DependencyInjection; 12 | 13 | namespace Helldivers.Core.Extensions; 14 | 15 | /// 16 | /// Contains extension methods for . 17 | /// 18 | public static class ServiceCollectionExtensions 19 | { 20 | /// 21 | /// Adds all services related to the Helldivers API to the service container. 22 | /// 23 | public static IServiceCollection AddHelldivers(this IServiceCollection services) 24 | { 25 | services.AddSingleton(); 26 | services.AddSingleton(); 27 | 28 | return services 29 | .AddArrowHeadStores() 30 | .AddSteamStores() 31 | .AddV1Stores() 32 | .AddV2Stores(); 33 | } 34 | 35 | /// 36 | /// Registers all & for ArrowHead models. 37 | /// 38 | public static IServiceCollection AddArrowHeadStores(this IServiceCollection services) 39 | { 40 | // Register all stores 41 | services.AddSingleton(); 42 | 43 | return services; 44 | } 45 | 46 | /// 47 | /// Registers all & for Steam models. 48 | /// 49 | public static IServiceCollection AddSteamStores(this IServiceCollection services) 50 | { 51 | // Register facade for all stores below 52 | services.AddSingleton(); 53 | 54 | // Register stores 55 | services.AddSingleton, SteamNewsStore>(); 56 | 57 | // Register mappers 58 | services.AddSingleton(); 59 | 60 | return services; 61 | } 62 | 63 | /// 64 | /// Registers all & for V1 models. 65 | /// 66 | public static IServiceCollection AddV1Stores(this IServiceCollection services) 67 | { 68 | // Register facade for all stores below 69 | services.AddSingleton(); 70 | 71 | // Register stores 72 | services.AddSingleton, PlanetStore>(); 73 | services.AddSingleton, WarStore>(); 74 | services.AddSingleton, CampaignStore>(); 75 | services.AddSingleton, Storage.V1.AssignmentStore>(); 76 | services.AddSingleton, DispatchStore>(); 77 | 78 | // Register mappers 79 | services.AddSingleton(); 80 | services.AddSingleton(); 81 | services.AddSingleton(); 82 | services.AddSingleton(); 83 | services.AddSingleton(); 84 | services.AddSingleton(); 85 | 86 | return services; 87 | } 88 | 89 | /// 90 | /// Registers all & for V2 models. 91 | /// 92 | public static IServiceCollection AddV2Stores(this IServiceCollection services) 93 | { 94 | // Register facade for all stores below 95 | services.AddSingleton(); 96 | 97 | services.AddSingleton, Storage.V2.DispatchStore>(); 98 | services.AddSingleton, Storage.V2.SpaceStationStore>(); 99 | 100 | // Register mappers 101 | services.AddSingleton(); 102 | services.AddSingleton(); 103 | 104 | return services; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Helldivers-2-Core/Storage/ArrowHead/ArrowHeadStore.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.Core.Localization; 2 | using Helldivers.Models.ArrowHead; 3 | 4 | namespace Helldivers.Core.Storage.ArrowHead; 5 | 6 | /// 7 | /// Dedicated storage for the ArrowHead payloads, as we want to be able to return those byte-by-byte. 8 | /// 9 | public sealed class ArrowHeadStore 10 | { 11 | private Memory _warId; 12 | private Memory _warInfo; 13 | private Memory _warSummary; 14 | private CultureDictionary> _statuses = null!; 15 | private CultureDictionary> _feeds = null!; 16 | private CultureDictionary> _assignments = null!; 17 | private Dictionary>> _spaceStations = null!; 18 | private readonly TaskCompletionSource _syncState = new(); 19 | 20 | /// 21 | /// Updates the with the updated raw values. 22 | /// 23 | public void UpdateRawStore( 24 | Memory warId, 25 | Memory warInfo, 26 | Memory warSummary, 27 | IEnumerable>> statuses, 28 | IEnumerable>> feeds, 29 | IEnumerable>> assignments, 30 | Dictionary>> spaceStations 31 | ) 32 | { 33 | _warId = warId; 34 | _warInfo = warInfo; 35 | _warSummary = warSummary; 36 | _statuses = new(statuses); 37 | _feeds = new(feeds); 38 | _assignments = new(assignments); 39 | _spaceStations = 40 | spaceStations.ToDictionary(pair => pair.Key, pair => new CultureDictionary>(pair.Value)); 41 | 42 | _syncState.TrySetResult(); 43 | } 44 | 45 | /// 46 | /// returns the raw payload for . 47 | /// 48 | public async Task> GetWarId(CancellationToken cancellationToken) 49 | { 50 | await _syncState.Task.WaitAsync(cancellationToken); 51 | 52 | return _warId; 53 | } 54 | 55 | /// 56 | /// returns the raw payload for . 57 | /// 58 | public async Task> GetWarStatus(CancellationToken cancellationToken) 59 | { 60 | await _syncState.Task.WaitAsync(cancellationToken); 61 | 62 | return _statuses.Get(); 63 | } 64 | 65 | /// 66 | /// returns the raw payload for . 67 | /// 68 | public async Task> GetWarInfo(CancellationToken cancellationToken) 69 | { 70 | await _syncState.Task.WaitAsync(cancellationToken); 71 | 72 | return _warInfo; 73 | } 74 | 75 | /// 76 | /// returns the raw payload for . 77 | /// 78 | public async Task> GetWarSummary(CancellationToken cancellationToken) 79 | { 80 | await _syncState.Task.WaitAsync(cancellationToken); 81 | 82 | return _warSummary; 83 | } 84 | 85 | /// 86 | /// returns the raw payload for s. 87 | /// 88 | public async Task> GetNewsFeeds(CancellationToken cancellationToken) 89 | { 90 | await _syncState.Task.WaitAsync(cancellationToken); 91 | 92 | return _feeds.Get(); 93 | } 94 | 95 | /// 96 | /// returns the raw payload for s. 97 | /// 98 | public async Task> GetAssignments(CancellationToken cancellationToken) 99 | { 100 | await _syncState.Task.WaitAsync(cancellationToken); 101 | 102 | return _assignments.Get(); 103 | } 104 | 105 | /// 106 | /// returns the raw payload for s. 107 | /// 108 | public async Task?> GetSpaceStation(long id, CancellationToken cancellationToken) 109 | { 110 | await _syncState.Task.WaitAsync(cancellationToken); 111 | 112 | return _spaceStations.TryGetValue(id, out var spaceStations) ? spaceStations.Get() : null; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | branches: [ "master" ] 19 | schedule: 20 | - cron: '18 5 * * 3' 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze (${{ matrix.language }}) 25 | # Runner size impacts CodeQL analysis time. To learn more, please see: 26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 27 | # - https://gh.io/supported-runners-and-hardware-resources 28 | # - https://gh.io/using-larger-runners (GitHub.com only) 29 | # Consider using larger runners or machines with greater resources for possible analysis time improvements. 30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 31 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 32 | permissions: 33 | # required for all workflows 34 | security-events: write 35 | 36 | # required to fetch internal or private CodeQL packs 37 | packages: read 38 | 39 | # only required for workflows in private repositories 40 | actions: read 41 | contents: read 42 | 43 | strategy: 44 | fail-fast: false 45 | matrix: 46 | include: 47 | - language: csharp 48 | build-mode: autobuild 49 | # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' 50 | # Use `c-cpp` to analyze code written in C, C++ or both 51 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both 52 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 53 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, 54 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. 55 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how 56 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages 57 | steps: 58 | - name: Checkout repository 59 | uses: actions/checkout@v4 60 | with: 61 | submodules: 'recursive' 62 | 63 | # Initializes the CodeQL tools for scanning. 64 | - name: Initialize CodeQL 65 | uses: github/codeql-action/init@v3 66 | with: 67 | languages: ${{ matrix.language }} 68 | build-mode: ${{ matrix.build-mode }} 69 | # If you wish to specify custom queries, you can do so here or in a config file. 70 | # By default, queries listed here will override any specified in a config file. 71 | # Prefix the list here with "+" to use these queries and those in the config file. 72 | 73 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 74 | # queries: security-extended,security-and-quality 75 | 76 | # If the analyze step fails for one of the languages you are analyzing with 77 | # "We were unable to automatically build your code", modify the matrix above 78 | # to set the build mode to "manual" for that language. Then modify this step 79 | # to build your code. 80 | # ℹ️ Command-line programs to run using the OS shell. 81 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 82 | - if: matrix.build-mode == 'manual' 83 | run: | 84 | echo 'If you are using a "manual" build mode for one or more of the' \ 85 | 'languages you are analyzing, replace this with the commands to build' \ 86 | 'your code, for example:' 87 | echo ' make bootstrap' 88 | echo ' make release' 89 | exit 1 90 | 91 | - name: Perform CodeQL Analysis 92 | uses: github/codeql-action/analyze@v3 93 | with: 94 | category: "/language:${{matrix.language}}" 95 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F41E Bug report" 2 | description: Report something that isn't working as it should or is outright broken 3 | title: "[BUG]: " 4 | labels: ["bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | **Before You Start...** 10 | 11 | This form is only for submitting bug reports. If you have a usage question 12 | or are unsure if this is really a bug, make sure to: 13 | 14 | - Read the [docs](https://helldivers-2.github.io/api/) 15 | - Ask on [Discord Chat](https://discord.gg/E8UUfWYmf9) 16 | - Ask on [GitHub Discussions](https://github.com/helldivers-2/api/discussions) 17 | 18 | Also try to search for your issue - it may have already been answered or even fixed in the development branch. 19 | However, if you find that an old, closed issue still persists in the latest version, 20 | you should open a new issue using the form below instead of commenting on the old issue. 21 | - type: input 22 | id: reproduction-link 23 | attributes: 24 | label: Link to faulty endpoint 25 | description: | 26 | Provide a link to the endpoint that is malfunctioning. 27 | 28 | Please do not just fill in a random link. The issue will be closed if no valid reproduction is provided. 29 | placeholder: Reproduction Link 30 | validations: 31 | required: true 32 | - type: input 33 | id: client-name 34 | attributes: 35 | label: The name of your client/application 36 | description: | 37 | This helps us identifying the errors in our logs that are related to the problem you are reporting. 38 | For websites that access the API directly you should put the domain of your website here, if you are writing 39 | a server side application (aka an API, Discord bot or if your website only accesses the API server side) put 40 | the name of the application here that it reports in the `User-Agent` header. 41 | placeholder: Client name 42 | validations: 43 | required: true 44 | - type: textarea 45 | id: request-data 46 | attributes: 47 | label: Request information 48 | description: | 49 | Provide as much information about your request as you can. 50 | If you are accessing the API from the browser you can right click the request in the network tab of your devtools 51 | see [this link](https://everything.curl.dev/cmdline/copyas.html) for more information on how to do this in your 52 | browser. 53 | 54 | If you are writing a server side application include all headers and the URL you are sending, in case of doubt 55 | feel free to put the code you use to call the API here. 56 | placeholder: Request information 57 | validations: 58 | required: true 59 | - type: textarea 60 | id: steps-to-reproduce 61 | attributes: 62 | label: Steps to reproduce 63 | description: | 64 | What exactly is your application doing that causes the API to malfunction? 65 | If we have to investigate the issue, what would we have to do to reproduce the issue? 66 | placeholder: Steps to reproduce 67 | validations: 68 | required: true 69 | - type: textarea 70 | id: expected 71 | attributes: 72 | label: What is expected? 73 | description: | 74 | This can range from a textual explanation of what you were expecting to see/happen to providing JSON snippets 75 | that illustrate what should be outputted. 76 | validations: 77 | required: true 78 | - type: textarea 79 | id: actually-happening 80 | attributes: 81 | label: What is actually happening? 82 | description: | 83 | Explain what is happening that deviates from the expected output. 84 | You can use screenshots (in the comments), but please make sure to provide the JSON output that the API returned 85 | and if possible the JSON output of the official ArrowHead API (can be fetched through the `/raw` endpoints). 86 | render: json 87 | validations: 88 | required: true 89 | - type: textarea 90 | id: system-info 91 | attributes: 92 | label: System Info 93 | description: | 94 | Provide as much information about the environment you are using, this can range from the browser you are accessing 95 | the API with to the programming language of your app/website, your editor/IDE, ... 96 | render: shell 97 | placeholder: System, browser, programming language, editor, ... 98 | - type: textarea 99 | id: additional-comments 100 | attributes: 101 | label: Any additional comments? 102 | description: e.g. some background/context of how you ran into this bug. 103 | -------------------------------------------------------------------------------- /src/Helldivers-2-API/Middlewares/RateLimitMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Helldivers.API.Configuration; 2 | using Helldivers.API.Extensions; 3 | using Microsoft.Extensions.Caching.Memory; 4 | using Microsoft.Extensions.Options; 5 | using Microsoft.Net.Http.Headers; 6 | using System.Net; 7 | using System.Security.Claims; 8 | using System.Text; 9 | using System.Text.Json; 10 | using System.Threading.RateLimiting; 11 | 12 | namespace Helldivers.API.Middlewares; 13 | 14 | /// 15 | /// Handles applying rate limit logic to the API's requests. 16 | /// 17 | public sealed partial class RateLimitMiddleware( 18 | ILogger logger, 19 | IOptions options, 20 | IMemoryCache cache 21 | ) : IMiddleware 22 | { 23 | [LoggerMessage(Level = LogLevel.Debug, Message = "Retrieving rate limiter for {Key}")] 24 | private static partial void LogRateLimitKey(ILogger logger, IPAddress key); 25 | 26 | [LoggerMessage(Level = LogLevel.Information, Message = "Retrieving rate limit for {Name} ({Limit})")] 27 | private static partial void LogRateLimitForUser(ILogger logger, string name, int limit); 28 | 29 | /// 30 | public async Task InvokeAsync(HttpContext context, RequestDelegate next) 31 | { 32 | if (IsValidRequest(context) is false) 33 | { 34 | await RejectRequest(context); 35 | return; 36 | } 37 | 38 | var limiter = GetRateLimiter(context); 39 | using var lease = await limiter.AcquireAsync(permitCount: 1, context.RequestAborted); 40 | if (limiter.GetStatistics() is { } statistics) 41 | { 42 | context.Response.Headers["X-RateLimit-Limit"] = $"{options.Value.RateLimit}"; 43 | context.Response.Headers["X-RateLimit-Remaining"] = $"{statistics.CurrentAvailablePermits}"; 44 | if (lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter)) 45 | context.Response.Headers["Retry-After"] = $"{retryAfter.Seconds}"; 46 | } 47 | 48 | if (lease.IsAcquired is false) 49 | { 50 | context.Response.StatusCode = StatusCodes.Status429TooManyRequests; 51 | return; 52 | } 53 | 54 | await next(context); 55 | } 56 | 57 | /// 58 | /// Checks if the request is "valid" (contains the correct X-Super-* headers). 59 | /// 60 | private bool IsValidRequest(HttpContext context) 61 | { 62 | if (options.Value.ValidateClients is false || context.Request.Path.StartsWithSegments("/metrics")) 63 | return true; 64 | 65 | return context.Request.Headers.ContainsKey(Constants.CLIENT_HEADER_NAME) 66 | && context.Request.Headers.ContainsKey(Constants.CONTACT_HEADER_NAME); 67 | } 68 | 69 | private RateLimiter GetRateLimiter(HttpContext http) 70 | { 71 | if (http.User.Identity?.IsAuthenticated ?? false) 72 | return GetRateLimiterForUser(http.User); 73 | 74 | var key = http.Connection.RemoteIpAddress ?? IPAddress.Loopback; 75 | LogRateLimitKey(logger, key); 76 | 77 | return cache.GetOrCreate(key, entry => 78 | { 79 | entry.SlidingExpiration = TimeSpan.FromSeconds(options.Value.RateLimitWindow); 80 | return new TokenBucketRateLimiter(new() 81 | { 82 | AutoReplenishment = true, 83 | TokenLimit = options.Value.RateLimit, 84 | TokensPerPeriod = options.Value.RateLimit, 85 | QueueLimit = 0, 86 | ReplenishmentPeriod = TimeSpan.FromSeconds(options.Value.RateLimitWindow) 87 | }); 88 | }) ?? throw new InvalidOperationException($"Creating rate limiter failed for {key}"); 89 | } 90 | 91 | private RateLimiter GetRateLimiterForUser(ClaimsPrincipal user) 92 | { 93 | var name = user.Identity?.Name!; 94 | var limit = user.GetIntClaim("RateLimit"); 95 | 96 | LogRateLimitForUser(logger, name, limit); 97 | return cache.GetOrCreate(name, entry => 98 | { 99 | entry.SlidingExpiration = TimeSpan.FromSeconds(options.Value.RateLimitWindow); 100 | return new TokenBucketRateLimiter(new() 101 | { 102 | AutoReplenishment = true, 103 | TokenLimit = limit, 104 | TokensPerPeriod = limit, 105 | QueueLimit = 0, 106 | ReplenishmentPeriod = TimeSpan.FromSeconds(options.Value.RateLimitWindow) 107 | }); 108 | }) ?? throw new InvalidOperationException($"Creating rate limiter failed for {name}"); 109 | } 110 | 111 | private async Task RejectRequest(HttpContext context) 112 | { 113 | context.Response.StatusCode = StatusCodes.Status400BadRequest; 114 | context.Response.Headers.WWWAuthenticate = "X-Super-Client"; 115 | context.Response.ContentType = "application/json"; 116 | 117 | var writer = new Utf8JsonWriter(context.Response.Body); 118 | writer.WriteStartObject(); 119 | writer.WritePropertyName("message"); 120 | writer.WriteStringValue("The X-Super-Client and X-Super-Contact headers are required"); 121 | writer.WriteEndObject(); 122 | 123 | await writer.FlushAsync(context.RequestAborted); 124 | } 125 | } 126 | --------------------------------------------------------------------------------