ValminEye
Server-only plugin that writes joins, leaves, boss fights, first boss kills and raids to the server log as JSON lines for Valmin. Install it on the dedicated server; players need nothing.ValminEye
A server-only BepInEx plugin for Valheim dedicated servers that reports game events Valmin cannot see in the vanilla log: who joined and left, boss fights and who fought them, the first time each boss is defeated, raids, and when the world has started.
Status: in development, not released. The event format below is a draft and may change before the first release. No event has been checked on a live server yet.
How it works
ValminEye opens no port and accepts no input. It writes one line per event to the BepInEx log, which reaches the server's standard output. Valmin already follows that stream for readiness, saves and joins, so the plugin adds no new transport, credentials or firewall rules.
Players keep a vanilla client. Nothing needs to be installed on their machines.
Commands sent into the game, such as chat warnings, are not part of this plugin. Valmin
sends those over RCON through the Tristan-ValheimRcon mod. Messages posted to Discord are
written by Valmin from these events, and their wording is set in Valmin, not here.
Event format (draft, version 1)
Every event is one log line from the BepInEx source ValminEye, with one JSON object as the
message:
[Info : ValminEye] {"v":1,"event":"join","player":"Alex","id":"Steam_76561198000000000"}
| Field | Meaning |
|---|---|
v |
Format version. A consumer ignores lines with a version it does not know. |
event |
Event name, from the table below. |
| Event | Fields | When |
|---|---|---|
join |
player, id |
A player's character entered the world. |
leave |
player, id |
A player who had joined left: quit, lost connection, kick or ban. |
boss_first_kill |
key, boss, day, duration, wipes, players |
A boss was defeated for the first time in this world. |
boss_fight_start |
key, boss, first, day, players |
A boss noticed players and a fight started. |
boss_fight_end |
key, boss, first, outcome, day, duration, players |
The fight ended. |
raid_start |
raid, name, players |
A raid started, named by the game's event name, for example army_eikthyr. |
raid_end |
raid, name, players |
The raid ended. |
world_start |
world, day |
The world has loaded and the server is about to open to players. |
playeris the character name.idis the platform id in theSteam_<id>form Valmin's player lists use.keyis the world key the game sets, for exampledefeated_eikthyr.bossis the boss's English name from the game's own text, for exampleEikthyr. It is left out when the game has no name for the key, for example for a key added through the config.dayis the in-game day, the number players see as "Day N".playerslists everyone in the fight: each player the boss recorded as hitting it, and each player who died near it. Every entry hasplayer,idanddeaths, the number of times that player died near this boss since it was last defeated. Players who never died show0.durationis how long the fight lasted, in whole seconds. Onboss_first_killit is the winning fight, and it is left out when no fight was seen.wipesis how many fights against this boss ended without a kill since the server started.firstistruewhen the boss had never been defeated in this world when the fight started. By default only these fights are reported (FirstOnly).outcomeiskill,wipe(the boss lost track of the players, because everyone died or ran, or everyone left its area) orgone(the boss disappeared without dying, for example removed by a command). Onboss_fight_end,playerslists everyone who came withinDeathRadiusof the boss during the fight, with the deaths in that fight. Onboss_fight_startthe entries have nodeaths.nameon a raid is the message the game shows players when it starts, in English, for exampleThe ground is shaking. The game has no other name for a raid. It is left out when the game has no text for the raid.playerson a raid lists the players inside the raid area when the line is written, each withplayerandid. The area is the circle the game itself uses for the raid. A raid ended with thestopeventcommand can have no players.worldis the world's name, as in the server's-worldoption.- A raid already running when the server restarts is not reported again.
Every field except v, event, key, raid, outcome, player and world can be turned
off in the config, and so can every event.
There is no event for the plugin loading. BepInEx itself logs
[Info : BepInEx] Loading [ValminEye 0.2.0], and Valmin reads that line to tell that the
plugin runs and which version.
A boss fight that ended in a wipe:
[Info : ValminEye] {"v":1,"event":"boss_fight_end","key":"defeated_gdking","boss":"The Elder","first":true,"outcome":"wipe","day":31,"duration":184,"players":[{"player":"Alex","id":"Steam_76561198000000000","deaths":2}]}
A boss kill example:
[Info : ValminEye] {"v":1,"event":"boss_first_kill","key":"defeated_eikthyr","boss":"Eikthyr","day":12,"players":[{"player":"Alex","id":"Steam_76561198000000000","deaths":2},{"player":"Bo","id":"Steam_76561198000000001","deaths":0}]}
The plugin also writes a few plain-text lines from the same source, such as the list of bosses
it found at the first boss check. Event lines are the ones whose message starts with {.
Lines stay well under 16 KB, the length at which Docker splits a log line.
Planned
death, key_set, key_removed, day and a snapshot after the world loads.
How bosses are found
The plugin needs no list of bosses. When the server starts, it reads the bosses from the
game's own data, with the world key each one sets when defeated and its name, so a boss added
by a game update is picked up without a plugin release. A key also counts as a boss kill when
it starts with defeated_, or when it is listed in BossKeys, for modded bosses that use a
different key.
A kill is a first kill when the world did not have the key before. Removing the key with the
removekey console command makes the next kill a first kill again.
A player's death counts for a boss when it happens within DeathRadius metres of that boss
while the boss is alive. The count for a boss starts over each time that boss is defeated.
A fight starts when a boss turns alert, the state in which it hunts players, with a player
within DeathRadius metres. It ends when the boss dies, or when it calms down again: a boss
calms down 30 seconds after it last sensed a player, and when every player has left its area.
So a wipe is reported about half a minute after the last player died or ran. A restart ends
no fight; the game forgets a boss's alert state when the server stops.
Configuration
The config file is BepInEx/config/io.valmin.eye.cfg. It is created with the defaults the
first time the server starts with the plugin. Restart the server after editing it.
| Section | Setting | Default | Meaning |
|---|---|---|---|
Join |
Enabled |
true |
Write join lines. |
IncludeId |
true |
Add id. |
|
Leave |
Enabled |
true |
Write leave lines. |
IncludeId |
true |
Add id. |
|
BossFirstKill |
Enabled |
true |
Write boss_first_kill lines. |
IncludeBossName |
true |
Add boss. |
|
IncludeDay |
true |
Add day. |
|
IncludePlayers |
true |
Add players. |
|
IncludePlayerIds |
true |
Add id to each player. |
|
IncludeDeaths |
true |
Add deaths to each player. |
|
IncludeDuration |
true |
Add duration. |
|
IncludeWipes |
true |
Add wipes. |
|
AnyDefeatedKey |
true |
Count any key starting with defeated_ as a boss kill. |
|
BossKeys |
empty | More keys that count as a boss kill, separated by commas. | |
DeathRadius |
100 |
Metres from a boss within which a death counts for it and a player counts as in its fight. The default is the distance at which the game shows a boss health bar. | |
BossFight |
StartEnabled |
true |
Write boss_fight_start lines. |
EndEnabled |
true |
Write boss_fight_end lines. |
|
FirstOnly |
true |
Report only fights against bosses not yet defeated in this world. | |
IncludeBossName |
true |
Add boss. |
|
IncludeFirst |
true |
Add first. |
|
IncludeDay |
true |
Add day. |
|
IncludeDuration |
true |
Add duration to the end line. |
|
IncludePlayers |
true |
Add players. |
|
IncludePlayerIds |
true |
Add id to each player. |
|
IncludeDeaths |
true |
Add deaths to each player on the end line. |
|
Raid |
StartEnabled |
true |
Write raid_start lines. |
EndEnabled |
true |
Write raid_end lines. |
|
IncludeName |
true |
Add name. |
|
IncludePlayers |
true |
Add players. |
|
IncludePlayerIds |
true |
Add id to each player. |
|
World |
StartEnabled |
true |
Write world_start lines. |
IncludeDay |
true |
Add day. |
Build
The plugin compiles against the game's own assemblies, not a stub package, so a build always
matches a real server version. The game files are never committed. You need the .NET 8 SDK,
make, and either SteamCMD or Docker (for the steamcmd/steamcmd image).
make server # download the dedicated server (Steam app 896660) into .cache/valheim-server
make # build src/bin/Release/netstandard2.1/ValminEye.dll
make package # also write the Thunderstore zip to dist/
make check-update # tell whether Steam ships a newer server build than the installed one
make install PLUGINS_DIR=/path/to/server/BepInEx/plugins
To build against a server you already have, pass its directory: make SERVER_DIR=/path/to/server.
After a game update, run make server && make: a hook whose target the game renamed or
removed fails the build.
GitHub builds the same way: the Build workflow downloads the current dedicated server, compiles against it when a push or pull request changes the code or build files, and once a week, and keeps the package as an artifact.
To release, set Version in src/Plugin.cs, then publish a release on GitHub with a new tag
of that version and a leading v (v0.2.0). The Release workflow builds the package,
attaches it to the release and publishes it to Thunderstore and
Hexium. A pre-release gets the package but is not published to
either. It needs two repository secrets: THUNDERSTORE_TOKEN, a
service account token of the Thunderstore team named in thunderstore.toml, and
HEXIUM_TOKEN, a Hexium API token of the team with the same name.
The plugin runs on a server with denikson-BepInExPack_Valheim. Copy ValminEye.dll to its
BepInEx/plugins/ directory.
Credits
The choice of game hooks was informed by OdinEye by sparcopt and its maintained fork js-ferguson/odin-eye, both under the MIT License.
