Dota2GSI.Net.Core 0.1.0

dotnet add package Dota2GSI.Net.Core --version 0.1.0
                    
NuGet\Install-Package Dota2GSI.Net.Core -Version 0.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Dota2GSI.Net.Core" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Dota2GSI.Net.Core" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Dota2GSI.Net.Core" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Dota2GSI.Net.Core --version 0.1.0
                    
#r "nuget: Dota2GSI.Net.Core, 0.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Dota2GSI.Net.Core@0.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Dota2GSI.Net.Core&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Dota2GSI.Net.Core&version=0.1.0
                    
Install as a Cake Tool

Dota2GSI.Net

Dota 2 CI GitHub Release NuGet Version NuGet Downloads

Modern, Native AOT-ready, performance-focused .NET library for Dota 2 Game State Integration.

中文文档

Build overlays, companion apps, and match tools with typed game state and events on .NET 8, 9, and 10.

Why Dota2GSI.Net?

  • Modern .NET APIs: immutable typed snapshots, nullable protocol values, typed events, and IAsyncEnumerable<DotaGsiUpdate>. Integrate with ASP.NET Core, Generic Host, DI, and Microsoft.Extensions.Logging.
  • Native AOT and trimming: source-generated System.Text.Json metadata removes reflection-based JSON serialization from the library's parsing path. CI publishes and runs trimmed and Native AOT consumers for .NET 8/9/10 on Windows x64.
  • Built for frequent updates: UTF-8 parsing, lazy dynamic maps, pooled HTTP read buffers, source-generated logging, and bounded processing queues. See the benchmark comparison for measured workload costs.
  • Dota-aware models: local and spectator views, inventory slots, modern map and hero fields, snapshot diffs, and domain events. Unknown fields remain accessible through extension data where supported.
  • Easy setup: use the built-in Kestrel listener or add a route to your server; discover Steam libraries and generate the Dota 2 GSI configuration through the same API.

Start with the standalone listener, ASP.NET Core integration, or the runnable model example.

Benchmark Comparison

In the following JIT workloads, Dota2GSI.Net used 25–38% less total replay time than Dota2GSI 2.1.1.8897 (antonpup). Each case replays 2,000 synthetic frames generated from the repository's sanitized fixtures, with five independent cold process launches per library. Lower is better.

Workload Dota2GSI.Net 0.1.0 JIT (ms) Dota2GSI 2.1.1.8897 JIT (ms) Less replay time
Local player 923.3 ± 72.73 1,255.3 ± 157.35 26.4%
Ten-player spectator 3,002.4 ± 116.25 4,845.2 ± 158.71 38.0%
Heartbeat 828.5 ± 94.45 1,100.2 ± 60.78 24.7%

Windows 11 x64 · Ryzen 7 5800X3D · .NET 8.0.23 (JIT) · BenchmarkDotNet 0.15.8 · 2026-09-06. Values are mean ± 99.9% confidence-interval half-width. Timing includes process startup, input loading, HTTP replay, complete event dispatch, and exit; model and event coverage differ between libraries.

.NET 8 / .NET 10 / Native AOT / JVM

All seven configurations below were measured together with the same complete synthetic local-player input: 2,000 frames, one common HTTP client, and five fresh server processes per configuration, in rotating order. Timing covers server startup through the final processed update; input loading and shutdown are excluded.

Library Runtime version Build Mean ± standard deviation (ms)
Dota2GSI.Net 0.1.0 .NET 8.0.22 JIT 1,336.9 ± 9.0
Dota2GSI.Net 0.1.0 .NET 8.0.22 Native AOT 554.5 ± 20.8
Dota2GSI.Net 0.1.0 .NET 10.0.1 JIT 1,328.4 ± 62.2
Dota2GSI.Net 0.1.0 .NET 10.0.1 Native AOT 521.9 ± 32.0
Dota2GSI 2.1.1.8897 .NET 8.0.22 JIT 1,808.4 ± 51.9
Dota2GSI 2.1.1.8897 .NET 10.0.1 JIT 1,903.9 ± 125.0
dota2-gsi 3.0.0 (Java API / Kotlin implementation) Temurin JDK 21.0.12.1 JVM 1,943.0 ± 40.7

Same machine and date as above. Dota2GSI.Net uses identical worker source and default Release settings in all four configurations; hero names, health, and event counts were verified. The JVM library delivers state callbacks; the .NET libraries also compute snapshot diffs and domain events. This table uses a different measurement boundary from the first table.

Install

dotnet add package Dota2GSI.Net

Dota2GSI.Net is the recommended package. It references Dota2GSI.Net.Core and includes the built-in listener plus ASP.NET Core endpoint extensions. Install Dota2GSI.Net.Core directly only when an application needs parsing, diffing, events, Steam discovery, or config writing without ASP.NET Core hosting APIs.

Choose An Integration Mode

Use Dota2GSI.Net when the application should receive Dota 2 HTTP POST payloads through ASP.NET Core or the built-in Kestrel listener. Use Dota2GSI.Net.Core when another framework already gives you the raw HTTP POST body, or when you only need parsing, diffing, events, Steam discovery, or cfg generation.

Native AOT

Both packages declare IsAotCompatible and IsTrimmable. The smoke consumer exercises parsing, diffs, events, configuration generation, endpoint mapping, and the standalone listener with JSON reflection disabled. CI runs that consumer on Windows, Linux, and macOS, and additionally publishes and executes trimmed and native binaries on Windows x64 for all three target frameworks.

To publish your application, enable Native AOT in its project:

<PropertyGroup>
  <PublishAot>true</PublishAot>
  <JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
</PropertyGroup>

Then run dotnet publish -c Release -r win-x64 on Windows with the Native AOT build prerequisites installed. For ASP.NET Core applications, use WebApplication.CreateSlimBuilder(args). Custom JSON types need their own generated JsonTypeInfo<T>; the model example demonstrates passing it to GsiValue. Your application's other dependencies must also support AOT.

Use An Existing ASP.NET Core Server

Choose this when your application already has a WebApplication or ASP.NET Core host. The library adds a route to that existing server and registers processor services in DI.

using Dota2GSI.Net.AspNetCore;

var builder = WebApplication.CreateSlimBuilder(args);
builder.WebHost.UseUrls("http://127.0.0.1:3000");

builder.Services.AddDotaGsi(options =>
{
    options.AuthToken = "change-me";
});
builder.Services.AddHostedService<DotaGsiConsumer>();

WebApplication app = builder.Build();

app.MapDotaGsi("/dota2-gsi");
await app.RunAsync();

AddDotaGsi is required before MapDotaGsi; missing registration fails during mapping with a clear exception.

In the same ASP.NET Core project, add DotaGsiConsumer.cs to consume parsed updates and events from the endpoint registry:

using Dota2GSI.Net.AspNetCore;
using Dota2GSI.Net.Events;
using Dota2GSI.Net.Processing;
using Microsoft.Extensions.Hosting;

public sealed class DotaGsiConsumer : IHostedService
{
    private readonly DotaGsiEndpoint endpoint;

    public DotaGsiConsumer(DotaGsiEndpointRegistry endpoints)
    {
        endpoint = endpoints.GetEndpoint("/dota2-gsi");
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        endpoint.GameStateReceived += OnGameStateReceived;
        endpoint.Events.HeroDied += OnHeroDied;
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        endpoint.GameStateReceived -= OnGameStateReceived;
        endpoint.Events.HeroDied -= OnHeroDied;
        return Task.CompletedTask;
    }

    private static void OnGameStateReceived(object? sender, DotaGameStateReceivedEventArgs args)
    {
        Console.WriteLine(args.State.Map?.GameState);
    }

    private static void OnHeroDied(object? sender, HeroDiedEvent args)
    {
        Console.WriteLine(args.Hero?.Name);
    }
}

MapDotaGsi accepts POST requests only, validates the mapped path, enforces a maximum request body size, parses JSON, validates auth and lazy dynamic sections, queues the payload, and returns 200 OK before endpoint processing and event handlers complete. Malformed JSON or lazy validation failures return 400, auth failures return 401, oversized bodies return 413, full or stopped endpoint queues return 503, and request-aborted cancellations return 499. Processing failures after queue acceptance are logged rather than changing the already returned HTTP response. Each mapped endpoint owns its processor state so multiple MapDotaGsi routes do not share diff baselines, events, or auth behavior. A singleton DotaGsiProcessor remains registered for direct DI use, but mapped endpoints are consumed through DotaGsiEndpointRegistry.

Grouped routes can be retrieved by their effective route after ASP.NET Core has built endpoints:

app.MapGroup("/radiant").MapDotaGsi("/dota2-gsi");
app.MapGroup("/dire").MapDotaGsi("/dota2-gsi");

// Resolve from app.Services after startup, or inject the registry into a service.
var endpoints = app.Services.GetRequiredService<DotaGsiEndpointRegistry>();
DotaGsiEndpoint radiant = endpoints.GetEndpoint("/radiant/dota2-gsi");
DotaGsiEndpoint dire = endpoints.GetEndpoint("/dire/dota2-gsi");

For deterministic lookup before startup, or for route patterns that repeat, assign an endpoint key:

app.MapGroup("/radiant").MapDotaGsi("/dota2-gsi", options => options.EndpointKey = "radiant");

var endpoints = app.Services.GetRequiredService<DotaGsiEndpointRegistry>();
DotaGsiEndpoint radiant = endpoints.GetEndpoint("radiant");

Endpoint options can override processor auth per mapped endpoint while keeping the global AddDotaGsi defaults for endpoints that do not override them:

app.MapDotaGsi("/team-a", options => options.AuthToken = "token-a");
app.MapDotaGsi("/team-b", options => options.AuthToken = "token-b");

Use An Independent Listener

Choose this when the application does not already expose an ASP.NET Core endpoint for Dota 2. The library starts a small local Kestrel listener at the configured Uri. There are two ways to use it:

  • Create and start DotaGsiListener yourself.
  • Register it in a Generic Host and let DI manage its lifecycle.

Manual Listener Instance

Use this when you do not have a Generic Host, or when the surrounding code owns startup and shutdown explicitly.

Create a console project with dotnet new console -n DotaGsiApp -f net10.0, run dotnet add package Dota2GSI.Net from that project's directory, and replace Program.cs with:

using Dota2GSI.Net.Hosting;
using Dota2GSI.Net.Processing;

using var shutdown = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) =>
{
    e.Cancel = true;
    shutdown.Cancel();
};
CancellationToken cancellationToken = shutdown.Token;

await using var listener = new DotaGsiListener(new DotaGsiListenerOptions
{
    Uri = new Uri("http://127.0.0.1:3000/dota2-gsi/"),
    AuthToken = "change-me",
    MaxRequestBodyBytes = 1024 * 1024
});

listener.GameStateReceived += (_, args) =>
{
    Console.WriteLine(args.State.Map?.GameState);
};

listener.Events.HeroDied += (_, args) =>
{
    Console.WriteLine(args.Hero?.Name);
};

await listener.StartAsync(cancellationToken);
Console.WriteLine("Listening. Press Ctrl+C to stop.");
try
{
    await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
}

Run the app, then configure Dota 2 with the matching URI and token. To consume updates as an async stream, replace the Task.Delay inside the try block with:

await foreach (DotaGsiUpdate update in listener.ReadAllAsync(cancellationToken))
{
    foreach (var change in update.Changes)
    {
        Console.WriteLine(change.Path);
    }
}

Start the listener before enumerating. ReadAllAsync supports one active reader; when its output buffer fills, DropOldest discards the oldest unread update. Stopping completes the current stream. After restarting, finish/dispose the old enumerator and call ReadAllAsync again. StreamCapacity controls both input and output buffers. Event callbacks run synchronously and delay processing until they return.

Generic Host And DI

Use this when the application already uses HostApplicationBuilder, Host.CreateDefaultBuilder, BackgroundService, or other Generic Host patterns. AddDotaGsiListener(...) registers DotaGsiListener as a singleton and starts/stops it through IHostedService.

using Dota2GSI.Net.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddDotaGsiListener(options =>
{
    options.Uri = new Uri("http://127.0.0.1:3000/dota2-gsi/");
    options.AuthToken = "change-me";
});

using IHost host = builder.Build();
var listener = host.Services.GetRequiredService<DotaGsiListener>();
listener.GameStateReceived += (_, update) => Console.WriteLine(update.State.Map?.GameState);
await host.RunAsync();

The listener owns a small Kestrel server. It accepts POST requests only, validates the configured path, enforces a maximum request body size, and returns 404/405/413 for HTTP-level invalid input. It parses JSON, validates auth and lazy dynamic sections, and writes the payload to a bounded processing channel before returning 200 OK. Malformed JSON or lazy validation failures return 400, auth failures return 401, oversized bodies return 413, and listener shutdown or full queues return 503. Omit AuthToken when token validation is not needed, or set IgnoreAuthTokenValidation = true to keep the configured token while accepting payloads with missing or different auth.token values.

Check GSI Stream Freshness

DotaGsiListener, DotaGsiEndpoint, and DotaGsiProcessor expose IsGsiStreaming() to check whether accepted GSI payloads are still arriving recently:

if (listener.IsGsiStreaming())
{
    Console.WriteLine("Dota GSI is active.");
}

The check is based on the last successfully accepted payload and an adaptive timeout derived from recent accepted-payload intervals. It does not query Dota 2 and does not require access to the GSI cfg file, so it also works when the listener runs on a different machine. When it returns false, the last accepted state may still be available, but it should be treated as stale.

Use Core With Raw HTTP POST Bodies

Choose this when you do not need the built-in host, or when another HTTP framework receives the request. Install the Core package and pass the raw UTF-8 request body to DotaGsiProcessor.

dotnet add package Dota2GSI.Net.Core
using Dota2GSI.Net.Processing;

var processor = new DotaGsiProcessor(new DotaGsiProcessorOptions
{
    AuthToken = "change-me"
});

processor.GameStateReceived += (_, args) =>
{
    Console.WriteLine(args.State.Map?.GameState);
};

processor.Events.HeroDied += (_, args) =>
{
    Console.WriteLine(args.Hero?.Name);
};

// In an HTTP application, replace this sample with the raw UTF-8 request body.
byte[] body = """{"auth":{"token":"change-me"},"map":{"clock_time":42}}"""u8.ToArray();
DotaGsiUpdate update = processor.Process(body, DateTimeOffset.UtcNow);

For one-off parsing without stateful diffing or event publication, call the parser directly:

using Dota2GSI.Net.GameState;

DotaGameState state = DotaGameStateJsonParser.Parse("""{"map":{"clock_time":42}}"""u8);

DotaGameStateJsonParser uses source-generated metadata from DotaGsiJsonContext; it does not depend on reflection serialization.

Logging

The listener uses Microsoft.Extensions.Logging and does not enable any log provider by default. This keeps the default listener path quiet and avoids forcing a logging framework on applications. Configure providers when logs are needed:

using Dota2GSI.Net.Hosting;
using Microsoft.Extensions.Logging;

await using var listener = new DotaGsiListener(new DotaGsiListenerOptions
{
    Uri = new Uri("http://127.0.0.1:3000/dota2-gsi/"),
    ConfigureLogging = logging =>
    {
        logging.SetMinimumLevel(LogLevel.Information);
        logging.AddConsole();
    }
});

Applications that already own an ILoggerFactory can pass it directly:

await using var listener = new DotaGsiListener(
    new DotaGsiListenerOptions
    {
        Uri = new Uri("http://127.0.0.1:3000/dota2-gsi/")
    },
    loggerFactory);

Detailed game event logs are disabled by default because Dota 2 can update GSI payloads frequently. Enable them explicitly and use Trace when event-level diagnostics are needed:

await using var listener = new DotaGsiListener(new DotaGsiListenerOptions
{
    Uri = new Uri("http://127.0.0.1:3000/dota2-gsi/"),
    LogGameEvents = true,
    ConfigureLogging = logging =>
    {
        logging.SetMinimumLevel(LogLevel.Trace);
        logging.AddConsole();
    }
});

The constructor loggerFactory parameter and ConfigureLogging are mutually exclusive. The library logs through the standard provider model, so Serilog, NLog, log4net, console, OpenTelemetry, and other Microsoft.Extensions.Logging providers can be used by configuring the provider in the consuming application. Hot-path listener logs use source-generated logging methods to avoid message-template formatting overhead when the relevant level is disabled.

Configuration Reference

AddDotaGsi(...) and direct DotaGsiProcessor construction use DotaGsiProcessorOptions:

Option Default Description
AuthToken null Expected Dota GSI auth.token. When null, token validation is not required.
IgnoreAuthTokenValidation false Skips token validation even when AuthToken is set. This is useful for temporarily accepting payloads while keeping the configured token value.
LogGameEvents false Enables detailed domain event logging through DotaGsiEventHub. Event logs are emitted at Trace level.

MapDotaGsi(...) uses DotaGsiHttpOptions:

Option Default Description
EndpointKey mapped route pattern Registry key used by DotaGsiEndpointRegistry.GetEndpoint(...). Use this when repeated local route patterns need deterministic lookup before startup.
MaxRequestBodyBytes 1048576 Maximum accepted request body size in bytes. Must be greater than zero and no greater than int.MaxValue. Larger requests return 413 Payload Too Large.
PendingPayloadCapacity 128 Maximum accepted payloads that can wait for endpoint processing. Must be greater than zero. Full queues return 503 Service Unavailable before body parsing when no slot is available.
AuthToken global AddDotaGsi value Endpoint-specific expected Dota GSI auth.token. Set to null explicitly to clear a global token for this endpoint.
IgnoreAuthTokenValidation global AddDotaGsi value Endpoint-specific token validation bypass.
LogGameEvents global AddDotaGsi value Endpoint-specific domain event logging.

Example:

app.MapDotaGsi("/dota2-gsi", options =>
{
    options.MaxRequestBodyBytes = 2 * 1024 * 1024;
    options.AuthToken = "endpoint-token";
});

DotaGsiListenerOptions, used by new DotaGsiListener(...) and AddDotaGsiListener(...), includes all DotaGsiProcessorOptions plus:

Option Default Description
Uri http://127.0.0.1:3000/ Absolute listener URI. Only http is currently supported. The path portion is used as the GSI endpoint path.
MaxRequestBodyBytes 1048576 Maximum accepted request body size in bytes. Must be greater than zero and no greater than int.MaxValue.
StreamCapacity 128 Bounded capacity for received payloads and processed update streams. Must be greater than zero.
ConfigureLogging null Callback for configuring logging providers on the dedicated listener host. Mutually exclusive with the DotaGsiListener constructor loggerFactory parameter.

Write Dota 2 GSI Config

In Steam, open Dota 2 → Properties → General → Launch Options, add -gamestateintegration, and restart Dota 2 after writing the cfg. Valve requires this launch option for GSI to send data; see the March 11, 2022 update. The cfg URI (including the path) and auth token must match your listener or mapped endpoint. For the ASP.NET Core example above, use http://127.0.0.1:3000/dota2-gsi.

Use DotaGsiConfigInstaller for user-facing setup flows. It can install the cfg into the local Dota 2 installation, write it to an explicit directory, and check whether the existing cfg matches the expected configuration.

using Dota2GSI.Net.Configuration;

var config = new DotaGsiConfig
{
    Name = "dota2gsi_net",
    Uri = new Uri("http://127.0.0.1:3000/dota2-gsi/"),
    AuthToken = "change-me"
};

var installer = new DotaGsiConfigInstaller();

// 1. Configure the local Dota 2 installation discovered from Steam.
string localPath = await installer.InstallLocalAsync(config);

// 2. Write the cfg to an explicit gamestate_integration directory.
string explicitPath = await installer.WriteToDirectoryAsync(
    @"D:\SteamLibrary\steamapps\common\dota 2 beta\game\dota\cfg\gamestate_integration",
    config);

// 3. Check whether the local cfg is already correctly configured.
DotaGsiConfigCheckResult check = await installer.CheckLocalAsync(config);
if (!check.IsConfigured)
{
    Console.WriteLine($"{check.Status}: {check.ConfigPath}");
}

InstallLocalAsync(...) searches common Steam locations. If the application already knows the Steam root, pass it as steamRoot.

For lower-level scenarios, DotaGsiConfigWriter can generate the cfg text or write a specific file path. Written cfg files use UTF-8 without BOM because Valve GSI configuration files are not loaded reliably when a UTF-8 BOM is present.

string cfg = DotaGsiConfigWriter.WriteToString(config);
await DotaGsiConfigWriter.WriteAsync("gamestate_integration_dota2gsi_net.cfg", config);

SteamLibraryLocator can locate Steam libraries and the Dota 2 game/dota/cfg/gamestate_integration directory on common Windows, Linux, and macOS installations.

using Dota2GSI.Net.Steam;

var locator = new SteamLibraryLocator();
string? dotaPath = locator.FindDotaInstallPath();

DotaGsiConfig controls generated gamestate_integration_*.cfg files:

Option Default Description
Name Required Configuration block name. GetConfigFileName(...) derives the file name from this value.
Uri Required HTTP endpoint Dota 2 should POST payloads to.
AuthToken null Optional token written to the cfg auth block.
Timeout 5s Dota 2 GSI timeout value. Must not be negative.
Buffer 100ms Dota 2 GSI buffer value. Must not be negative.
Throttle 100ms Dota 2 GSI throttle value. Must not be negative.
Heartbeat 10s Dota 2 GSI heartbeat value. Must not be negative.
Components DotaGsiDataComponents.AllKnown Data sections written under the cfg data block. Available flags are Provider, Map, Player, Hero, Abilities, Items, Events, Buildings, League, Draft, Wearables, Minimap, Roshan, Couriers, and NeutralItems. Auth is configured separately through AuthToken.

Model Coverage

The typed model currently covers the recorder-observed top-level GSI sections:

auth, provider, map, player, hero, abilities, items, events, buildings, league, draft, wearables, minimap, roshan, couriers, and neutralitems.

Finite Dota protocol values are exposed as C# enums or dedicated value types with custom JSON converters. Unrecognized enum tokens map to Unknown.

Typed protocol values include map.game_state, map.win_team, dynamic teamN keys, player.activity, player.team_name, events[].event_type, event team fields, map.roshan_state, map.tormentor_state, map.tormentor_state_location, watcher.capture_state, minimap team values, and item contains_rune. Names and identifiers such as player names, hero/unit names, item and ability names, map names, match IDs, and auth tokens remain strings.

Modeled Dota 2 fields include Tormentor, Watchers, Wisdom Shrines, Lotus Pools, scan/glyph data, radiant win chance, hero facet compatibility, hero permanent_buffs, player rune counts, damage breakdowns, neutral enchantments, and preserved neutral item slots.

Unknown fields are preserved through extension data on the typed sections where that is practical. The top-level previously and added payloads are retained as raw JsonElement values. The listener also emits GameStatePathChangedEvent for path-level protocol deltas so future Dota fields can be observed before a strongly typed event exists.

Local And Spectator Sections

Local payloads expose local objects:

var hero = state.Hero?.LocalHero;
var inventory = state.Items?.LocalInventory;

Spectator payloads expose team/player maps:

if (state.Hero?.Heroes?.TryGetPlayer(DotaTeam.Radiant, 0, out var hero) == true)
{
    Console.WriteLine(hero.Name);
}

Dynamic keys such as team2, player0, ability0, slot0, courier0, and item0 are parsed by centralized converters.

See model usage for safe team/player traversal, inventory containers, empty slots, coordinates, and custom AOT value conversion.

Nullable Values

Most model properties are nullable because Dota 2 only sends fields enabled in the cfg and visible for the current game perspective:

if (state.Map?.ClockTime is int clockTime)
{
    Console.WriteLine(clockTime);
}

This preserves the difference between an absent field and a field present with a real default value.

Absent fields and explicit JSON null both become a nullable model value of null. Original protocol deltas and extension JSON can distinguish them through GameStateChange.CurrentValue / PreviousValue. Current / Previous are CLR projections and map both Missing and Null to null.

DotaHeroState.BuybackCooldown preserves the raw hero.buyback_cooldown expiry timestamp on the map.clock_time timeline. For a positive expiry and an available ClockTime, remaining seconds are Math.Max(0, expiry - clockTime). Zero reports no active cooldown; an absent expiry, or a positive expiry without a map clock, leaves the remaining duration unknown. Recorded live-game frames at clock 1065 and 1096 both report expiry 1545, corresponding to 480 and 449 remaining seconds. The library does not convert the stored value into a countdown.

Events And Diffs

Strongly typed events are available for common changes:

listener.Events.GameStarted += (_, _) => Console.WriteLine("Game started");
listener.Events.GameReturnedToMenu += (_, _) => Console.WriteLine("Returned to menu");
listener.Events.PlayerJoinedTeam += (_, e) => Console.WriteLine(e.Team);
listener.Events.HeroDied += (_, e) => Console.WriteLine(e.Hero?.Name);
listener.Events.InventoryItemAdded += (_, e) => Console.WriteLine(e.ItemName);
listener.Events.BuildingDestroyed += (_, e) => Console.WriteLine(e.BuildingName);

GameStarted and GameReturnedToMenu are derived from local player.activity transitions. PlayerJoinedTeam and PlayerBecameSpectator are derived from local player.team_name; GSI does not expose a reliable separate mode for live spectating versus caster/commentator spectating.

HeroDied / HeroRespawned describe local alive transitions. The PlayerHeroDied / PlayerHeroRespawned events cover local and team/player paths; their team/player identifiers can be null for local updates. Inventory Added and Removed events describe slot changes, including moves between slots, and do not by themselves establish purchases or sales.

Every diff also produces a generic path event:

listener.Events.PathChanged += (_, e) =>
{
    Console.WriteLine($"{e.Path}: {e.Previous} -> {e.Current}");
};

This covers newly added Dota fields before a dedicated domain event exists. Throwing event handlers are isolated; subscribe to listener.DispatchException for GameStateReceived handler failures and to listener.Events.DispatchException for domain event handler failures.

Raw And Unknown Fields

Unknown top-level and section fields are preserved through extension data where practical:

JsonElement raw = state.Map!.ExtensionData!["future_map_field"];

Dota's protocol delta nodes are retained:

JsonElement? previously = state.Previously;
JsonElement? added = state.Added;

GameStateDiffer.DiffProtocolDelta can convert those raw nodes to path-level changes.

Unknown enum tokens map to Unknown and cannot be serialized back to their original token; serialization throws JsonException. Keep the original request body for lossless archival. Path and external API coverage counts measure mapping coverage, not exhaustive protocol values or end-to-end behavioral coverage.

Quality Gates

Run these commands from the repository root using PowerShell 7 and the .NET 10 SDK. Install the .NET 8, 9, and 10 runtimes to execute the full test matrix. All required fixtures and model examples are committed:

dotnet test tests/Dota2GSI.Net.Tests/Dota2GSI.Net.Tests.csproj -c Release -p:UseSharedCompilation=false --verbosity minimal
dotnet build tests/Dota2GSI.Net.Benchmarks/Dota2GSI.Net.Benchmarks.csproj -c Release -p:UseSharedCompilation=false --verbosity minimal
dotnet run --project tests/Dota2GSI.Net.ModelUsage/Dota2GSI.Net.ModelUsage.csproj -c Release -f net10.0
dotnet run --project tests/Dota2GSI.Net.AotSmoke/Dota2GSI.Net.AotSmoke.csproj -c Release -f net10.0

The last command runs the smoke app with JSON reflection disabled. To publish and execute an actual native binary on Windows x64, install the Native AOT C++ build tools and Windows SDK, then run:

dotnet publish tests/Dota2GSI.Net.AotSmoke/Dota2GSI.Net.AotSmoke.csproj -c Release -f net10.0 -r win-x64 --self-contained true -p:PublishAot=true -p:TreatWarningsAsErrors=true -p:UseSharedCompilation=false --verbosity minimal -o artifacts/smoke/nativeaot/net10.0
./artifacts/smoke/nativeaot/net10.0/Dota2GSI.Net.AotSmoke.exe

CI contains the full matrix: semantic tests, reflection-disabled examples, and package consumers on Windows, Linux, and macOS; trimmed and Native AOT consumers on Windows x64 for .NET 8, 9, and 10. The release workflow also verifies clean committed sources, a matching remote version tag, and SourceLink.

Benchmarks

To reproduce the first .NET table above, generate the synthetic inputs locally with the input generator (PowerShell 7.3+) and run from the repository root:

pwsh -NoProfile -File tests/Dota2GSI.Net.Benchmarks/New-ComparisonInputs.ps1
dotnet run --project tests/Dota2GSI.Net.Benchmarks -c Release -f net8.0 -- --suite end-to-end --jsonl artifacts/readme-comparison/corpora/playing/events.jsonl --jsonl artifacts/readme-comparison/corpora/spectating-10/events.jsonl --jsonl artifacts/readme-comparison/corpora/heartbeat/events.jsonl --launchCount 5 --artifacts artifacts/readme-comparison/results

Generated inputs and reports stay in the ignored artifacts/ directory. The BenchmarkDotNet project provides the following suites:

--suite What it measures Comparison
end-to-end (default) Isolated worker startup, corpus loading, listener startup, sequential HTTP replay, complete event dispatch, and process exit. Dota2GSI.Net versus Dota2GSI 2.1.1.8897.
startup Construct, start, and dispose a listener. Dota2GSI.Net only.
single-payload Repeated HTTP POST of one corpus payload to an already running listener, waiting for complete event dispatch. Dota2GSI.Net only.
json-parser Local/team hero section parsing and direct hero deserialization with System.Text.Json. Internal parsing paths; no other GSI library.

Use --suite all to run all four. End-to-end replay uses the same process boundary for both libraries, while their model and event coverage differ. It measures complete workload cost, including startup, rather than pure parser speed or steady-state throughput. Worker allocations and CPU utilization are not measured. Other suites report managed allocations through MemoryDiagnoser.

For a smoke run from a clean checkout, use the committed synthetic fixture:

dotnet run --project tests/Dota2GSI.Net.Benchmarks/Dota2GSI.Net.Benchmarks.csproj -c Release -f net10.0 -- --suite end-to-end --jsonl tests/Dota2GSI.Net.Benchmarks/Fixtures/replay-dispatch.jsonl --job dry

This small fixture and --job dry verify execution only; they do not establish a performance baseline. The benchmark host targets net8.0 and net10.0. Choose the host with -f, or use --runtimes for a runtime matrix. This example also uses the smoke input:

dotnet run --project tests/Dota2GSI.Net.Benchmarks/Dota2GSI.Net.Benchmarks.csproj -c Release -f net10.0 -- --suite end-to-end --runtimes net8.0 net10.0 --jsonl tests/Dota2GSI.Net.Benchmarks/Fixtures/replay-dispatch.jsonl --job dry

Install the requested SDKs and runtimes where the dotnet executable launching the benchmark can resolve them. Both library workers support these two runtimes. Add --language zh-CN for Chinese startup messages, numeric culture, and the project result summary.

For your own workload, pass each existing recording with --jsonl; repeat the option for multiple files. Use UTF-8 JSONL with one record per line, a timestamp in receivedAt, and the GSI JSON object in payload, for example:

{"receivedAt":"2026-09-06T00:00:00Z","payload":{"hero":{"name":"npc_dota_hero_axe","health":100,"alive":true}}}
{"receivedAt":"2026-09-06T00:00:01Z","payload":{"hero":{"name":"npc_dota_hero_axe","health":0,"alive":false},"previously":{"hero":{"health":100,"alive":true}}}}

Supply representative recordings and use a measurement job when evaluating your application. Payloads counts recorded frames; Recorded/s describes the source recording cadence. Elapsed/recording % divides measured replay elapsed time by the original recording duration, including waiting and scheduling; it is not CPU utilization. Replay does not reproduce the gaps between recording timestamps.

License

Dota2GSI.Net is licensed under the Apache License 2.0.

Copyright (c) 2026 Dota2GSI.Net Contributors.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Dota2GSI.Net.Core:

Package Downloads
Dota2GSI.Net

Modern .NET Dota 2 Game State Integration library.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.0 98 9/6/2026