You are viewing a potentially older version of this package. View all versions.
Proudlock_Technology-GsValheimStatsEmitter-0.2.6 icon

GsValheimStatsEmitter

Server-side companion for the gs dashboard: world milestones, per-player boss damage, live online players + in-game day, and server-owned combat. Pairs with the gs client companion.

Date uploaded a week ago
Version 0.2.6
Download link Proudlock_Technology-GsValheimStatsEmitter-0.2.6.zip
Downloads 51
Dependency string Proudlock_Technology-GsValheimStatsEmitter-0.2.6

This mod requires the following mods to function

denikson-BepInExPack_Valheim-5.4.2333 icon
denikson-BepInExPack_Valheim

BepInEx pack for Valheim. Preconfigured with the correct entry point for mods and preferred defaults for the community.

Preferred version: 5.4.2333

README

GsValheimStatsEmitter (server-side)

A dedicated-server mod that posts the stats only the server can see to an HTTP endpoint - by default the self-hosted gs dashboard, but it'll POST to any URL.

Pairs with the GsValheimStatsClient companion (which each player installs for their personal stats). Both POST to the same endpoint; a receiver merges them by world + player name.

Full payload schema, key conventions, and merge rules are documented on the Data format & self-hosting wiki.

Why a server mod at all? Valheim hands creature simulation to whichever client is nearest, so most combat is captured client-side. The server still owns idle / distant creatures (e.g. a sleeping mob you sneak-attack), so this mod captures those - notably the true post-backstab hardest hit - plus world-progression milestones the server is authoritative for.


What it is

A BepInEx plugin for a Valheim dedicated server. It patches Character.RPC_Damage and Character.OnDeath, polls the world's global keys, and POSTs a cumulative JSON snapshot to the gs dashboard on a timer. Bosses are identified with Character.IsBoss() rather than a hardcoded list, so new bosses work without a code change. Cumulative counters persist to BepInEx/config/net.cproudlock.gsvalheimstats.state.tsv and survive restarts.

What it tracks

  • World milestones ("quests") - boss defeats, Hildir's Request bounties, first troll/surtling, etc. (from the world's global keys)
  • Per-player boss damage for bosses the server owns
  • Weapon damage + hardest hit for server-owned creatures (the sleeping-backstab case)
  • Live presence - connected players + in-game day (onlinePlayers / worldDay), force-emitted on join/leave for near-instant "online now"
  • Active mod list + world identity (name, host, uptime)

Setup

  1. Install on your dedicated server (depends on denikson-BepInExPack_Valheim). Drop the DLL in BepInEx/plugins/.

  2. Launch once to generate BepInEx/config/net.cproudlock.gsvalheimstats.cfg, then set:

    [General]
    World = vhserver3          # your server's -world name
    EmitIntervalSeconds = 120
    
    [Ingest]
    Url = http://localhost:3001/api/valheim/ingest   # dashboard reachable from the server
    Token = <bearer token>
    
  3. Restart the server. Cumulative counters persist to BepInEx/config/<guid>.state.tsv.


Run it

dotnet build -c Release

There is no local dev loop. The build needs the Valheim dedicated-server assemblies and BepInEx core, so it only compiles on the gameserver VM, where GsValheimStatsEmitter.csproj points $(Managed) and $(BepInExCore). Nothing in this repo builds on a workstation.

Success looks like Build succeeded. 0 Error(s), and the DeployToServer target copying the DLL into BepInEx/plugins.

Deploy

The build deploys itself: the DeployToServer target runs after every successful Release build and copies the DLL into $(DeployDir). So the whole recipe is copy the source over, build there, restart.

scp src/Plugin.cs *.csproj manifest.json CHANGELOG.md \
    [email protected]:/home/cproudlock/mod-valheim/
ssh [email protected] 'cd ~/mod-valheim && dotnet build -c Release'

Then restart the server. Use the safe-restart script, never vhserver restart, which never returns through the qemu guest agent and leaves a stale lgsm/lock/vhserver-stopping.lock that silently blocks every later start:

setsid /home/cproudlock/vhserver3/vh-safe-restart.sh >/dev/null 2>&1 &
# then poll /tmp/vh-safe-restart.log for the RESULT: line

Afterwards confirm Loading [GsValheimStatsEmitter <version>] and [gs] ingest status: 200 in BepInEx/LogOutput.log.

Note the version lives in three places that must agree: <Version> in the csproj, version_number in manifest.json, and the VERSION constant in src/Plugin.cs. The dashboard records the last of these, so a mismatch is visible in the mod list.

Ingesting the stats yourself

Same contract as the client mod - an authenticated POST of a JSON snapshot (see that mod's README for a minimal receiver + curl test).

Payload (source: "server"):

{
  "schemaVersion": 1,
  "game": "valheim",
  "source": "server",
  "world": "vhserver3",
  "hostName": "Proudlock VH Server 3",
  "serverStartedAtUtc": "2026-06-09T11:32:00Z",
  "onlinePlayers": ["Erebus", "Pridetoes"],
  "worldDay": 12,
  "emittedAtUtc": "2026-06-09T18:00:00Z",
  "snapshotIdLocal": "9a6b...",
  "players": [{
    "name": "Erebus",
    "boss":    [{ "boss": "Eikthyr", "kills": 1, "damageDealt": 1450, "fightSec": 95 }],
    "weapons": [{ "weapon": "Knives", "damageDealt": 80, "kills": 2, "hardestHit": 79 }]
  }],
  "bossKillEvents": [{ "boss": "Eikthyr", "fightSec": 95, "firstBlood": "Erebus", "topDamagePlayer": "Erebus", "topDamage": 1450, "participants": 2, "tsUtc": "..." }],
  "milestones": [{ "key": "defeated_eikthyr", "label": "Eikthyr defeated", "kind": "boss", "tsUtc": "..." }],
  "mods": [{ "guid": "Azumatt.AzuCraftyBoxes", "name": "AzuCraftyBoxes", "version": "1.8.14", "author": "Azumatt", "folder": "Azumatt-AzuCraftyBoxes-1.8.14" }]
}

Merging client + server feeds

Each feed reports the creatures it simulates, so they're disjoint. A receiver should:

  • sum damageDealt / kills per (player, weapon) across the two sources (exact total, no double-count, since a single hit is only ever processed by one side), and
  • take the max of hardestHit.

The gs dashboard does this with per-source rows + SUM / MAX(...) at read time.

CHANGELOG

Changelog

0.2.6

  • Bosses are detected by the game, not a hardcoded list. The prefab map stopped at whatever shipped when it was written, so Valheim 1.0's Kall Fimbulbringer was invisible to every boss stat. Character.IsBoss() replaces it, and the prefab name becomes the boss key (which is what the old map produced anyway). Any future boss works with no code change.
  • The state file is finally written. SaveState() was called from exactly one place, the end of OnBossKilled. Valheim hands creature simulation to the nearest client, so the server almost never owns a boss death and that call almost never ran: the file had never been created once, and every cumulative counter reset to zero on restart. State now saves on a 60s timer and on shutdown.

0.2.5

  • Milestone labels fixed. Valheim lower-cases global keys before storing them, so GetGlobalKeys() returns killedtroll, never the KilledTroll spelling the label map used. KeyLabels now compares with StringComparer.OrdinalIgnoreCase, which restores the labels for troll and Hildir keys.
  • Hildir bounties are bounties again. The keys now arrive prefixed (bosshildir3), which failed the StartsWith("Hildir") test and filed them under progression. The prefix is stripped before both the label lookup and the kind test.
  • Season and boss-count keys no longer become milestones. Seasonality writes season_<name> on every season change, and vanilla's activeBosses <n> carries a changing count in the key itself, so each new value was landing as a permanent milestone. Both are now ignored.
  • Added a label for KilledBat.

0.2.4

  • Docs: plain-text README (removed em-dashes and emoji).

0.2.3

0.2.2

  • Docs: refreshed README ingest examples (onlinePlayers, worldDay, boss fightSec) and bundled this changelog.

0.2.1

  • Live presence: reports connected players (onlinePlayers) and the in-game day (worldDay), force-emitted on join/leave for a near-instant "online now".

0.2.0

  • Fixed hardest hit for server-owned creatures: now hooks Character.RPC_Damage (where backstab/crit/armor are applied) instead of the Damage forwarder.

0.1.0

  • Initial release: world-progression milestones (boss defeats, bounties), per-player boss damage, weapon damage / hardest hit for server-owned creatures (the sleeping-backstab case), and the active mod list + world identity.