How it works
- Your mod reports a moment. One call, such as
event("bossKill"). The client sends it in the background and never blocks the game. - Hovermate marks it. Every event becomes a marker on the recording and session timeline, with your label.
- The player chooses highlights. Your game gets its own card in Clips. For every type the player turns on, the event also saves a highlight clip.
While its SDK is on, Hovermate publishes its address, a token and its inbox folder in connection.json:
{
"version": 1,
"url": "http://local.hovermate.gg/hovermate/v1/events",
"token": "<random, base64url, 32 bytes>",
"inbox": "C:\\Users\\<user>\\AppData\\Local\\Hovermate\\Sdk\\Inbox"
}Read url from this file and use it exactly as written; never hard-code or change its host, port or path.
Clients read %LOCALAPPDATA%\Hovermate\Sdk\connection.json, or the path in HOVERMATE_SDK_CONNECTION for tests and isolated setups. They read it again after a 401, because the token changes whenever the SDK is switched off and on. No file means Hovermate is not installed or its SDK is off; clients then stay silent.
Three ways to send events
Use the first one your mod framework allows.
HTTP
Your mod can open a local network connection.
POST the event to the URL with Authorization: Bearer <token>. Requests with an Origin header are refused, so web pages cannot send events.
C#, C++, Java, Python, JavaScript; GDScript can use HTTPRequest.
Files
Your mod can write files but has no network.
Append one JSON event per line to {inbox}\<game.id>.jsonl; no token is needed. A sandbox that writes only inside the mod's folder, such as Cyber Engine Tweaks, appends to hovermate-events.jsonl there instead, and the player adds that file in the game's card.
Every client falls back to the inbox; the Lua client writes the mod-folder file.
Log lines
Your scripts can only print to the game's own log.
Print [HOVERMATE] followed by one JSON event; anything before the marker is ignored. The Witcher 3 scriptslog.txt and the Skyrim and Fallout 4 Papyrus.0.log are read automatically while the game runs.
Witcher 3 scripts, Papyrus, or any game with a script log.
An HTTP request
POST /hovermate/v1/events HTTP/1.1
Host: local.hovermate.gg
Authorization: Bearer <token from connection.json>
Content-Type: application/json
Content-Length: 95
{"v":1,"game":{"id":"com.example.mygame","name":"My Game"},"type":"kill","label":"Double kill"}Answer: 202 {"accepted":1}.
A log line
[12:00:01] [Script] [HOVERMATE] {"v":1,"game":{"id":"com.example.mygame","name":"My Game"},"type":"bossKill","context":"Act 2"}For another log, the player adds it under Clips › the game's card › Read events from a file, or your hello proposes it.
Quick start
Each example sends hello, kill, bossKill and a custom parry event. These are the runnable files from the SDK's examples folder.
C#
- Client
- HovermateEvents.cs
- Runs in
- netstandard2.0, .NET Framework 4.7.2+ and .NET, with C# 7.3: Unity, BepInEx, MelonLoader.
- Set up
- Add HovermateEvents.cs to your mod project.
// Sends hello, kill, bossKill and a custom "parry" event to Hovermate (or to tools/mock-server.py).
// Run: dotnet run
using System;
using Hovermate.Sdk;
HovermateEvents.Log = Console.WriteLine;
HovermateEvents.Init("com.example.sdkdemo", "SDK Demo", "SdkDemo.exe");
HovermateEvents.Hello(new HovermateOffer[] { "kill", "bossKill", HovermateOffer.Custom("parry", "Parry") });
HovermateEvents.Event("kill", label: "Double kill", tags: new[] { "headshot" }, context: "Act 2");
HovermateEvents.Event("bossKill", label: "The Warden", context: "Act 2");
HovermateEvents.Custom("parry", "Parry", label: "Perfect parry");
Console.WriteLine(HovermateEvents.Flush(2000) ? "sent" : "timed out");
HovermateEvents.Shutdown();Run it in examples/csharp (the example targets .NET 8 or later):
dotnet runC++
- Client
- hovermate_events.h
- Runs in
- C++17 on Windows. Header-only; uses WinHTTP and links winhttp.lib.
- Set up
- Include hovermate_events.h. Call hovermate::shutdown() before your DLL is unloaded, never inside DllMain.
// Sends hello, kill, bossKill and a custom "parry" event to Hovermate (or to tools/mock-server.py).
// Build (Developer Command Prompt): cl /std:c++17 /EHsc /utf-8 /I..\..\cpp example.cpp
#include <cstdio>
#include "hovermate_events.h"
int main() {
hovermate::set_log([](const std::string& message) { std::printf("%s\n", message.c_str()); });
hovermate::init("com.example.sdkdemo", "SDK Demo", "SdkDemo.exe");
hovermate::hello({ "kill", "bossKill", hovermate::Offer::custom("parry", "Parry") });
hovermate::event("kill", { "Double kill", { "headshot" }, "Act 2" });
hovermate::event("bossKill", { "The Warden", {}, "Act 2" });
hovermate::custom("parry", "Parry", { "Perfect parry", {}, {} });
std::printf("%s\n", hovermate::flush(2000) ? "sent" : "timed out");
hovermate::shutdown();
return 0;
}Run it in a Developer Command Prompt, in examples/cpp:
cl /std:c++17 /EHsc /utf-8 /I..\..\cpp example.cppJava
- Client
- HovermateEvents.java
- Runs in
- Java 8 and later: ModTheSpire, Forge, Fabric.
- Set up
- Copy HovermateEvents.java into your mod and change its package (or relocate it when shading) so copies in other mods cannot collide.
// Sends hello, kill, bossKill and a custom "parry" event to Hovermate (or to tools/mock-server.py).
// Run (Java 8+): javac -d out ../../java/HovermateEvents.java Example.java && java -cp out Example
import gg.hovermate.sdk.HovermateEvents;
import gg.hovermate.sdk.HovermateEvents.Offer;
import java.util.Arrays;
public class Example {
public static void main(String[] args) {
HovermateEvents.log = System.out::println;
HovermateEvents.init("com.example.sdkdemo", "SDK Demo", "SdkDemo.exe");
HovermateEvents.hello(Offer.of("kill"), Offer.of("bossKill"), Offer.custom("parry", "Parry"));
HovermateEvents.event("kill", "Double kill", Arrays.asList("headshot"), "Act 2");
HovermateEvents.event("bossKill", "The Warden", null, "Act 2");
HovermateEvents.custom("parry", "Parry", "Perfect parry", null, null);
System.out.println(HovermateEvents.flush(2000) ? "sent" : "timed out");
HovermateEvents.shutdown(1000);
}
}Run it in examples/java:
javac -d out ../../java/HovermateEvents.java Example.java && java -cp out ExamplePython
- Client
- hovermate_events.py
- Runs in
- Python 3.7 and later, standard library only.
- Set up
- Put hovermate_events.py next to your script.
"""Sends hello, kill, bossKill and a custom "parry" event to Hovermate (or to tools/mock-server.py).
Run from any folder: python example.py
"""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'python'))
import hovermate_events as hovermate # noqa: E402
hovermate.set_log(print)
hovermate.init('com.example.sdkdemo', 'SDK Demo', 'SdkDemo.exe')
hovermate.hello(['kill', 'bossKill', hovermate.custom_offer('parry', 'Parry')])
hovermate.event('kill', label='Double kill', tags=['headshot'], context='Act 2')
hovermate.event('bossKill', label='The Warden', context='Act 2')
hovermate.custom('parry', 'Parry', label='Perfect parry')
print('sent' if hovermate.flush(2.0) else 'timed out')
hovermate.shutdown()Run it in examples/python:
python example.pyJavaScript
- Client
- hovermate-events.mjs
- Runs in
- Node.js 18 and later, as an ES module.
- Set up
- Import hovermate-events.mjs.
// Sends hello, kill, bossKill and a custom "parry" event to Hovermate (or to tools/mock-server.py).
// Run with Node.js 18+: node example.mjs
import * as hovermate from '../../js/hovermate-events.mjs';
hovermate.setLog(console.log);
hovermate.init('com.example.sdkdemo', 'SDK Demo', 'SdkDemo.exe');
hovermate.hello(['kill', 'bossKill', hovermate.customOffer('parry', 'Parry')]);
hovermate.event('kill', { label: 'Double kill', tags: ['headshot'], context: 'Act 2' });
hovermate.event('bossKill', { label: 'The Warden', context: 'Act 2' });
hovermate.custom('parry', 'Parry', { label: 'Perfect parry' });
console.log((await hovermate.flush(2000)) ? 'sent' : 'timed out');
await hovermate.shutdown();Run it in examples/js:
node example.mjsLua
- Client
- hovermate_events.lua
- Runs in
- Lua 5.1+ and LuaJIT: Cyber Engine Tweaks and other sandboxes without network.
- Set up
- Put hovermate_events.lua next to your init.lua. Events go to hovermate-events.jsonl in your mod folder: tell players to add that file in Hovermate.
-- Sends hello, kill, bossKill and a custom "parry" event to Hovermate (or to tools/mock-server.py --watch).
-- Run from the folder that stands in for your mod folder: lua path/to/example.lua
-- It writes hovermate-events.jsonl into the current folder.
--
-- In a Cyber Engine Tweaks mod, copy hovermate_events.lua next to init.lua and call it from your events:
-- local hovermate = require("hovermate_events")
-- registerForEvent("onInit", function()
-- hovermate.init("com.example.mymod", "My Mod", "Cyberpunk2077.exe", { log = print })
-- hovermate.hello({ "kill", "bossKill", hovermate.customOffer("parry", "Parry") })
-- end)
-- Then tell your users to add <game>\bin\x64\plugins\cyber_engine_tweaks\mods\<your mod>\hovermate-events.jsonl
-- in Hovermate: Clips > the game's card > Read events from a file.
local here = (arg and arg[0] or ''):match('^(.*[\\/])') or './'
package.path = here .. '../../lua/?.lua;' .. package.path
local hovermate = require('hovermate_events')
hovermate.init('com.example.sdkdemo', 'SDK Demo', 'SdkDemo.exe', { log = print, writer = print })
print('transport', hovermate.transport())
hovermate.hello({ 'kill', 'bossKill', hovermate.customOffer('parry', 'Parry') })
hovermate.event('kill', { label = 'Double kill', tags = { 'headshot' }, context = 'Act 2' })
hovermate.event('bossKill', { label = 'The Warden', context = 'Act 2' })
hovermate.custom('parry', 'Parry', { label = 'Perfect parry' })
print('log line for script-only mods:', hovermate.logLine('kill', { label = 'Double kill' }))
hovermate.shutdown()Run it in the folder that stands in for your mod folder:
lua example.luaWhat the clients do
| Call | What it does |
|---|---|
init(gameId, gameName, exe?) | Starts the client. gameId is [a-z0-9._-]{3,64} and stays the same across versions of your mod; exe is the game's executable file name. |
hello(offers, log?, files?) | Announces the types your game can send, and optionally a log or mod-folder files to read. |
event(type, {label, tags, context}) | Sends a standard event type. |
custom(customType, customLabel, {…}) | Sends a type of your own; customLabel names its highlight toggle. |
flush() / shutdown() | Waits for queued events; stops the background worker when the mod unloads. |
- Calls return at once and never throw into the game.
- A background worker sends events with timeouts of 250 ms and retries a failed request once.
- The queue holds 256 events and drops the oldest.
- When HTTP is unreachable, events go to the inbox for a few seconds.
- Without
connection.jsonthe client stays silent and checks again every 5 seconds. - Every event carries
game.name. Text is cut to the limits below and invalid tags are dropped instead of failing.
The Lua client (hovermate_events.lua) writes each event at once. It prefers hovermate-events.jsonl in the mod's folder, then the inbox where absolute paths are allowed, then [HOVERMATE] lines through a writer function you pass, such as print. logLine(type, options) returns such a line for you to print.
The event object
{ "v": 1,
"game": { "id": "com.example.mygame", "name": "My Game", "exe": "MyGame.exe" },
"type": "kill",
"label": "Double kill",
"tags": ["headshot"],
"context": "Act 2 · The City",
"time": "2026-11-09T21:15:03.120Z" }| Field | Required | Rule |
|---|---|---|
v | yes | 1 |
game.id | yes | [a-z0-9._-]{3,64}: reverse-DNS or a slug, stable across versions of the mod |
game.name | first event | In the first event or hello of a session; at most 64 characters |
game.exe | no | A file name only. Ties events to the detected game folder and lets that game start the replay buffer |
type | yes | A standard type below, or custom |
customType, customLabel | for custom | [a-z0-9_.-]{1,32}, and a toggle name of at most 40 characters |
label | no | Text for this one event (marker tooltip, clip title), at most 80 characters |
tags | no | At most 8, each [a-z0-9_-]{1,24} |
context | no | Map, level or area used in highlight titles, at most 64 characters |
time | no | ISO-8601 UTC. Hovermate stamps the arrival time; a time within 5 s of it refines the stamp |
Lengths count UTF-16 code units, so an emoji counts as 2. Text fields refuse control characters. Unknown fields are ignored, so later versions only add fields. Do not send personal data: labels and contexts can appear in clip titles and file names.
Standard types
29 types shared with Hovermate's built-in game cards. The list only grows; an unknown standard type is refused.
killdeathassistmultikillroundWonroundLostmatchWonmatchLostmvpgoalsaveobjectivemissionlapbestLappositioncrashincidentpenaltypitfinishraceWonlandingtakeofffightWonlevelUpbossKillzoneachievement
Custom types
For moments the standard list does not cover. Each custom type becomes its own highlight toggle, named by customLabel; a game can have up to 200.
{ "v": 1, "game": { "id": "com.example.mygame" },
"type": "custom", "customType": "parry", "customLabel": "Parry", "label": "Perfect parry" }hello
Optional and recommended. It creates your game's card with its highlight toggles before the first real event, so the player can set it up early.
{ "v": 1,
"game": { "id": "com.example.mygame", "name": "My Game", "exe": "MyGame.exe" },
"type": "hello",
"offers": ["kill", "death", "bossKill", { "customType": "parry", "customLabel": "Parry" }],
"log": "%LOCALAPPDATA%\\MyGame\\Saved\\Logs\\MyGame.log",
"files": ["C:\\Games\\MyGame\\mods\\my_mod\\hovermate-events.jsonl"] }offerslists standard types and{ "customType", "customLabel" }objects.logproposes a game log with[HOVERMATE]lines;filesproposes mod-folder event files. Hovermate asks the player before it reads any proposed path.helloworks over every transport, including a[HOVERMATE]log line.
Limits and responses
| Limit | Value |
|---|---|
| Events per game | 20 per second. More gives 429 and nothing in that request is accepted; hello does not count |
| Batch | { "events": [ … ] } with 1 to 100 events, all or nothing. A new game must be named in the first event of the batch that mentions it |
| HTTP body | 64 KB |
| Inbox and file lines | 4 KB; longer lines are dropped |
| Custom types | 200 per game |
| Client timeouts | 250 ms or less, at most one retry, a bounded queue that drops the oldest event |
| Status | Meaning |
|---|---|
202 | Accepted: {"accepted": n} |
400 | Invalid: {"error", "detail", "index"}. index names the first invalid event of a batch |
401 | Missing or wrong token: read connection.json again |
403 | The request has an Origin header |
413 | Body over 64 KB |
429 | More than 20 events per second for one game |
405, 411, 415 | Not a POST, no Content-Length, or not a JSON content type |
Error codes: invalid_json, version, game, type, custom, label, tags, context, offers, log, files, batch. Lines that fail in files are counted and shown in the game's card as invalid events; Hovermate never logs them raw.
Test without Hovermate
The mock receiver mock-server.py (Python 3.8+, standard library) answers exactly like Hovermate. It writes its own connection.json into a temporary folder, applies the same rules and error codes, tails its inbox and the files you pass with --watch, and prints every event and every error.
Unlike Hovermate, the mock listens on a numeric loopback port and writes that address into its connection.json. It is only a test tool: clients use whatever url the file holds.
python tools/mock-server.py
# In another terminal, with the path it printed:
$env:HOVERMATE_SDK_CONNECTION = "C:\Users\you\AppData\Local\Temp\hovermate-sdk-mock-…\connection.json"
python examples/python/example.pyHovermate SDK mock listening on http://127.0.0.1:52777/hovermate/v1/events
[21:15:03.120] http com.example.sdkdemo hello offers=kill,bossKill,custom:parry
[21:15:03.131] http com.example.sdkdemo kill "Double kill" tags=headshot context="Act 2"
[21:15:03.140] http com.example.sdkdemo bossKill "The Warden" context="Act 2"
[21:15:03.148] http com.example.sdkdemo custom:parry "Parry" "Perfect parry"--watch FILEtails a mod-folderhovermate-events.jsonlor a game log with[HOVERMATE]lines.--follow-proposalsreads what ahelloproposes, as if the player allowed it.--record FILEappends every accepted and refused event as a JSON line, for automated checks.
HOVERMATE_SDK_CONNECTION works the same with Hovermate itself, so tests never touch the real connection file.
Conformance cases
The SDK download includes conformance/cases.json: requests with the response Hovermate must give, from valid events to every error code. conformance/run.py sends them to the mock or to any receiver. Hovermate runs the same file in its own test suite.
python conformance/run.py
# or against a running receiver:
python conformance/run.py --connection path\to\connection.jsonWhat the player sees
- The first valid event or
helloof a newgame.idcreates an SDK card in Clips, with your game's name, its folder and cover whenexematches an installed game, and a highlight toggle for every offered or seen type. - Highlight toggles start off: the player chooses which types save clips. Markers are always written.
- Events follow the same path as built-in cards: journal, markers in clips and sessions, the highlight window and notifications.
- When
game.exematches the game in the foreground, events count for that game. Otherwise they are accepted while a recording or the replay buffer is running. - Invalid events are counted in the card's status, so players can tell you something is wrong.
Protocol and files
The protocol v1 is the full contract: connection discovery, the four ways to deliver events, the event object, hello and Hovermate's behaviour.
The SDK (clients, examples, tools, conformance cases and protocol) is MIT licensed, so you can ship a client inside your mod. Hovermate itself is not covered by that license.