游戏事件 SDK · 协议 v1

向 Hovermate 发送游戏事件

游戏或模组可以报告击杀、击败首领、圈数、进球或自定义时刻。Hovermate 会像处理内置游戏卡片一样,在时间轴上添加标记并自动保存精彩片段。

提供 C#、C++、Java、Python、JavaScript 和 Lua 客户端 · 各一个文件,无依赖 · MIT 许可证

工作原理

  1. 模组报告一个时刻。只需一次调用,例如 event("bossKill")。客户端在后台发送,不会阻塞游戏。
  2. Hovermate 添加标记。每个事件都会带着你的标签,出现在录制和会话时间轴上。
  3. 玩家选择精彩时刻。游戏会在“片段”中获得独立卡片。玩家开启的事件类型还会自动保存为精彩片段。

SDK 开启时,Hovermate 会将地址、令牌和收件文件夹写入 connection.json:

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"
}

从此文件读取 url 并按原样使用;切勿在代码中写死或更改其主机、端口或路径。

客户端读取 %LOCALAPPDATA%\Hovermate\Sdk\connection.json;测试或隔离环境可使用 HOVERMATE_SDK_CONNECTION 指定的路径。收到 401 后会重新读取,因为关闭再开启 SDK 会更换令牌。文件不存在表示未安装 Hovermate 或 SDK 已关闭,此时客户端保持静默。

发送事件的三种方式

使用模组环境允许的第一种方式。

HTTP

模组可以建立本地网络连接。

使用 Authorization: Bearer <token> 将事件 POST 到指定 URL。带有 Origin 请求头的请求会被拒绝,因此网页不能发送事件。

C#、C++、Java、Python、JavaScript;GDScript 可使用 HTTPRequest。

文件

模组可以写入文件,但不能访问网络。

向 {inbox}\<game.id>.jsonl 追加 JSON,每行一个事件,不需要令牌。如果沙箱只能写入模组目录,例如 Cyber Engine Tweaks,则写入其中的 hovermate-events.jsonl,由玩家将该文件添加到游戏卡片。

所有客户端都可回退到收件文件夹;Lua 客户端写入模组目录中的文件。

日志行

脚本只能向游戏自身的日志输出内容。

输出 [HOVERMATE] ,后接一个 JSON 事件对象;标记之前的内容会被忽略。游戏运行时会自动读取 The Witcher 3 的 scriptslog.txt,以及 Skyrim 和 Fallout 4 的 Papyrus.0.log。

Witcher 3 脚本、Papyrus,或任何具有脚本日志的游戏。

HTTP 请求

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"}

响应:202 {"accepted":1}。

日志行示例

游戏日志中的一行
[12:00:01] [Script] [HOVERMATE] {"v":1,"game":{"id":"com.example.mygame","name":"My Game"},"type":"bossKill","context":"Act 2"}

其他日志可由玩家在“片段”› 游戏卡片 › 从文件读取事件中添加,也可通过 hello 提议。

快速入门

每个示例都会发送 hello、kill、bossKill 和自定义 parry 事件。这些是 SDK 的 examples 文件夹中可直接运行的示例。

C#

运行环境
netstandard2.0、.NET Framework 4.7.2+ 和 .NET,使用 C# 7.3:Unity、BepInEx、MelonLoader。
设置
将 HovermateEvents.cs 添加到模组项目。
csharp/Program.cs
// 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();

在 examples/csharp 中运行(示例面向 .NET 8 或更高版本):

dotnet run

C++

运行环境
Windows 上的 C++17。仅需头文件;使用 WinHTTP 并链接 winhttp.lib。
设置
包含 hovermate_events.h。在 DLL 卸载前调用 hovermate::shutdown(),切勿在 DllMain 内调用。
cpp/example.cpp
// 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;
}

在开发者命令提示符中,从 examples/cpp 运行:

cl /std:c++17 /EHsc /utf-8 /I..\..\cpp example.cpp

Java

运行环境
Java 8 或更高版本:ModTheSpire、Forge、Fabric。
设置
将 HovermateEvents.java 复制到模组并修改包名,或在 shading 时重定位,避免与其他模组中的副本冲突。
java/Example.java
// 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);
    }
}

在 examples/java 中运行:

javac -d out ../../java/HovermateEvents.java Example.java && java -cp out Example

Python

运行环境
Python 3.7 或更高版本,仅使用标准库。
设置
将 hovermate_events.py 放在脚本旁边。
python/example.py
"""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()

在 examples/python 中运行:

python example.py

JavaScript

运行环境
Node.js 18 或更高版本,以 ES 模块形式使用。
设置
导入 hovermate-events.mjs。
js/example.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();

在 examples/js 中运行:

node example.mjs

Lua

运行环境
Lua 5.1+ 和 LuaJIT:Cyber Engine Tweaks 及其他无网络沙箱。
设置
将 hovermate_events.lua 放在 init.lua 旁边。事件写入模组目录中的 hovermate-events.jsonl;请告知玩家在 Hovermate 中添加该文件。
lua/example.lua
-- 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()

在用作模组目录的文件夹中运行:

lua example.lua

客户端行为

调用作用
init(gameId, gameName, exe?)启动客户端。gameId 必须符合 [a-z0-9._-]{3,64},并在模组版本间保持稳定;exe 为游戏可执行文件名。
hello(offers, log?, files?)声明游戏可发送的事件类型,并可提议要读取的日志或模组目录文件。
event(type, {label, tags, context})发送标准类型事件。
custom(customType, customLabel, {…})发送自定义类型;customLabel 用作其精彩片段开关的名称。
flush() / shutdown()等待队列中的事件,并在模组卸载时停止后台工作线程。
  • 调用立即返回,绝不向游戏抛出异常。
  • 后台工作线程使用 250 ms 超时发送事件,请求失败时重试一次。
  • 队列最多容纳 256 个事件,超出时丢弃最早的事件。
  • HTTP 不可用时,接下来的几秒内会将事件写入收件文件夹。
  • 没有 connection.json 时客户端保持静默,每 5 秒重新检查。
  • 每个事件都包含 game.name。文本按以下限制截断,无效标签会被丢弃,不导致调用失败。

Lua 客户端(hovermate_events.lua)会立即写入事件。依次优先使用模组目录中的 hovermate-events.jsonl、允许绝对路径时的收件文件夹,以及通过提供的 writer 函数(例如 print)输出 [HOVERMATE] 日志行。logLine(type, options) 会返回可自行输出的日志行。

事件对象

{ "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" }
字段必填规则
v是1
game.id是[a-z0-9._-]{3,64}:反向 DNS 名称或简短标识符,在模组版本间保持稳定
game.name首个事件会话中的首个事件或 hello 中必填;最多 64 个字符
game.exe否仅文件名。将事件关联到检测到的游戏目录,并允许该可执行文件启动回放缓冲
type是下列标准类型,或 custom
customType, customLabel当类型为 custom 时[a-z0-9_.-]{1,32},以及最多 40 个字符的开关名称
label否此事件的显示文本,用于标记提示和片段标题;最多 80 个字符
tags否最多 8 个标签,每个符合 [a-z0-9_-]{1,24}
context否用于精彩片段标题的地图、关卡或区域;最多 64 个字符
time否ISO-8601 UTC。Hovermate 记录接收时间;与其相差不超过 5 秒的 time 可修正时间戳

长度按 UTF-16 代码单元计算,因此一个表情符号计为 2。文本字段不允许控制字符。未知字段会被忽略,后续版本仅新增字段。不要发送个人数据:标签和上下文可能出现在片段标题与文件名中。

标准类型

与 Hovermate 内置游戏卡片共用 29 种类型。列表只会扩充;未知标准类型将被拒绝。

  • kill
  • death
  • assist
  • multikill
  • roundWon
  • roundLost
  • matchWon
  • matchLost
  • mvp
  • goal
  • save
  • objective
  • mission
  • lap
  • bestLap
  • position
  • crash
  • incident
  • penalty
  • pit
  • finish
  • raceWon
  • landing
  • takeoff
  • fightWon
  • levelUp
  • bossKill
  • zone
  • achievement

自定义类型

用于标准列表之外的时刻。每种类型都有独立开关,名称由 customLabel 指定;每个游戏最多 200 种。

{ "v": 1, "game": { "id": "com.example.mygame" },
  "type": "custom", "customType": "parry", "customLabel": "Parry", "label": "Perfect parry" }

hello

可选,建议使用。首次实际事件发生前即可创建游戏卡片及开关,方便玩家提前设置。

{ "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"] }
  • offers 列出标准类型及 { "customType", "customLabel" } 对象。
  • log 提议包含 [HOVERMATE] 行的游戏日志;files 提议模组目录中的事件文件。Hovermate 在读取任何提议路径前都会征得玩家同意。
  • hello 可通过所有传输方式发送,包括 [HOVERMATE] 日志行。

限制与响应

限制值
每个游戏的事件数每秒 20 个。超出时返回 429,整个请求均不接收;hello 不计入限制
批次{ "events": [ … ] } 包含 1–100 个事件,全部接收或全部拒绝。批次首次提到新游戏的事件必须提供游戏名称
HTTP 请求体64 KB
收件和文件行4 KB;更长的行将被丢弃
自定义类型每个游戏 200 种
客户端超时不超过 250 ms,最多重试一次;有界队列会丢弃最早的事件
状态含义
202已接收:{"accepted": n}
400无效:{"error", "detail", "index"}。index 指出批次中首个无效事件
401令牌缺失或错误:重新读取 connection.json
403请求包含 Origin 请求头
413请求体超过 64 KB
429单个游戏每秒超过 20 个事件
405, 411, 415不是 POST、缺少 Content-Length,或内容类型不是 JSON

错误码:invalid_json、version、game、type、custom、label、tags、context、offers、log、files、batch。文件中的错误行会计为游戏卡片中的无效事件;Hovermate 绝不会原样记录这些内容。

不启动 Hovermate 进行测试

测试接收器 mock-server.py(Python 3.8+,标准库)的响应与 Hovermate 一致。它在临时目录创建自己的 connection.json,使用相同规则与错误码,监视收件目录和通过 --watch 指定的文件,并输出每个事件及错误。

与 Hovermate 不同,测试接收器监听一个数字环回端口,并把该地址写入自己的 connection.json。它只是测试工具:客户端使用文件中的 url,无论它是什么。

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.py
测试接收器输出
Hovermate 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 FILE 监视模组目录中的 hovermate-events.jsonl,或包含 [HOVERMATE] 行的游戏日志。
  • --follow-proposals 读取 hello 提议的路径,模拟玩家已授权的情况。
  • --record FILE 将每个接收或拒绝的事件追加为一行 JSON,供自动检查使用。

HOVERMATE_SDK_CONNECTION 同样适用于 Hovermate 本体,因此测试不会接触真实的连接文件。

一致性测试用例

SDK 包含 conformance/cases.json:从有效事件到所有错误码的请求及 Hovermate 应有的响应。conformance/run.py 将这些请求发送给测试接收器或其他接收器。Hovermate 自身的测试也使用同一文件。

python conformance/run.py
# or against a running receiver:
python conformance/run.py --connection path\to\connection.json

玩家看到的内容

  • 新 game.id 的首个有效事件或 hello 会在“片段”中创建 SDK 卡片,显示游戏名称;若 exe 匹配已安装游戏,还会显示目录与封面,并为每种声明或收到的事件提供开关。
  • 精彩片段开关初始关闭:由玩家选择哪些类型保存为片段。标记始终记录。
  • 事件走与内置卡片相同的流程:事件记录、片段和会话标记、精彩时刻窗口及通知。
  • 当 game.exe 匹配前台游戏时,事件归属于该游戏;否则只有录制或回放缓冲正在运行时才会接收。
  • 无效事件数会显示在卡片状态中,便于玩家报告问题。

协议与文件

协议 v1 定义完整约定:连接发现、四种投递方式、事件对象、hello 和 Hovermate 行为。

SDK 的客户端、示例、工具、一致性用例及协议均采用 MIT 许可证,因此可将客户端包含在模组中。该许可证不适用于 Hovermate 本体。