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
<PackageReference Include="Dota2GSI.Net.Core" Version="0.1.0" />
<PackageVersion Include="Dota2GSI.Net.Core" Version="0.1.0" />
<PackageReference Include="Dota2GSI.Net.Core" />
paket add Dota2GSI.Net.Core --version 0.1.0
#r "nuget: Dota2GSI.Net.Core, 0.1.0"
#:package Dota2GSI.Net.Core@0.1.0
#addin nuget:?package=Dota2GSI.Net.Core&version=0.1.0
#tool nuget:?package=Dota2GSI.Net.Core&version=0.1.0
Dota2GSI.Net
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, andMicrosoft.Extensions.Logging. - Native AOT and trimming: source-generated
System.Text.Jsonmetadata 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
DotaGsiListeneryourself. - 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 | Versions 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. |
-
net10.0
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Options (>= 8.0.0)
-
net8.0
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Options (>= 8.0.0)
-
net9.0
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Options (>= 8.0.0)
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 |