B·BUGGCRAFTDEVELOPER GUIDE
LAUNCHER INTEGRATION

把联机带进
你的启动器。

通过标准 JSON-RPC 2.0 接口添加节点、查询房间并连接游戏。启动器只需管理一个本地桥接进程,不必实现 ICE 打洞或中继协议。

HTTPS · 公开状态JSON-RPC 2.0 · UTF-8P2P 优先 · 中继回退
01 / OVERVIEW

一个桥接层,覆盖不同连接环境

推荐通过启动器随附或安装的 Buggcraft bridge 子进程接入。进程负责节点身份、房间信令、P2P 与中继切换。

STATUS

读取公开状态

无需账号即可显示节点在线状态、容量和公开房间。

查看目录接口 →
ROOMS

加入游戏房间

导入接入码后搜索房间。游戏连接默认尝试 P2P,失败时经节点中转。

查看连接流程 →
FRIENDS

使用账号与好友

个人接入码用于识别登录账号;匿名接入可玩联机,但没有好友功能。

查看账号说明 →
02 / QUICK START

用 JSON-RPC 启动桥接进程

每行发送一个 JSON-RPC 2.0 请求,每个请求对应一行响应。先初始化协议,再添加节点,之后创建或加入房间。

  1. 启动本地进程

    开发环境可用 Python 3.11+ 执行 python -m hookup bridge;发布时把桥接组件随启动器一起安装或打包。

  2. 初始化并导入节点

    调用 bridge.initialize,再用 node.add 导入用户从可信节点取得的接入码。桥接进程负责凭据,不要把凭据写入日志。

  3. 连接房间

    用 room.list 展示公开房间,或让用户输入邀请码后调用 room.join。加入者可以选择 P2P 优先或仅中继。

  4. 启动游戏并清理会话

    把返回的 127.0.0.1:端口 交给 Minecraft。退出时调用 room.close;进程关闭时会清理剩余会话。

开发环境命令 · 需安装 Buggcraft bridge 依赖
PYTHONPATH=buggcraft/src/buggcraft python -m hookup bridge
请求示例 · node.add 与 room.join
{"jsonrpc":"2.0","id":1,"method":"bridge.initialize","params":{"protocol":1}}
{"jsonrpc":"2.0","id":2,"method":"node.add","params":{"code":"buggnode1-<用户接入码>"}}
{"jsonrpc":"2.0","id":3,"method":"room.join","params":{"node":"<返回的节点句柄>","invite":"bugg1-<房间邀请码>","transport":"auto"}}
桥接接口的版本边界

标准报文采用 JSON-RPC 2.0;方法名、参数、限制和错误码由 Buggcraft 的 v1 profile 定义。该参考实现目前不支持 JSON-RPC batch 请求。发布第三方启动器时,请自行部署节点并用真实客户端验证。

03 / DIRECTORY

查询节点与房间

公开状态走 HTTPS。桥接接口提供有凭据的目录操作;游戏数据不经过目录查询接口。

目录能力
接口用途
GET /api/status读取节点公开状态、容量、在线人数和注册开关。
node.add验证并导入接入码,返回进程内使用的不透明节点句柄。
room.list分页获取公开房间;传入 offset,最多返回 50 项。
session.list读取本进程活动会话及当前连接状态。
account_lookup个人身份可按用户名或账号 ID 查询;匿名凭据没有好友查询权限。

限流与错误

单行输入上限 8 KiB;错误使用 JSON-RPC 标准错误结构。超时、节点不可达或凭据失效时提示用户,并采用有上限的退避重试。

节点故障

其他联邦入口可以继续提供目录。房间归属节点不可用时房主需重建房间;已经建立的 Minecraft TCP 会话不会自动迁移到另一台服务器。

04 / GAME CONNECTION

同一房间可以混合使用 P2P 和中继

路径由每位加入者分别选择和协商。一个访客 P2P 失败或主动选中继,不会让房间里的其他访客改变路径。

LAUNCHER标准 JSON-RPC启动或退出会话
LOCAL BRIDGE按访客选路径P2P 优先 / 中继
MINECRAFT127.0.0.1:端口原版 TCP 接口
房间接口 v1
方法参数与结果
room.hostnode、game_port、transport;返回会话 ID、邀请码和房间状态。
room.joinnode、invite、transport;返回会话 ID、本地回环地址和连接状态。
room.close使用会话 ID 关闭监听、P2P 连接、中继连接与后台任务。
session.list返回本进程中的活动会话、角色与当前传输状态。

连接方式

transport:auto(默认)会先尝试 WebRTC ICE / DTLS / SCTP 直连,连不上时该玩家自动走 SSH 中继。transport:relay 会跳过 P2P,仅使用中继。

网络边界

P2P 需要双方网络允许 ICE 建连;对称 NAT、防火墙、UDP 阻断等环境可能只能中继。当前节点提供 STUN 地址发现和流量中继,不提供 TURN 服务,也不能保证所有网络都可直连。

隐私与路径选择

P2P 建连会让通信双方获知彼此的网络候选地址。若用户不愿向房间对端提供自己的网络地址,选“仅通过节点中转”。中继能隐藏玩家之间的直接地址,但中继节点仍会看到连接端点和转发流量。

05 / ACCOUNTS & FRIENDS

登录账号与匿名联机分开

匿名接入可以添加节点和加入房间,但不显示好友入口。好友需要账号身份、对方同意以及可达的联邦目录。

01节点网页登录注册或登录
02领取个人接入码主动导入启动器
03搜索并添加好友双方确认

跨节点用户名搜索

联邦节点同步公开用户名、账号 ID 和公钥。用户名目录不包含密码;查询结果不代表已经获得好友或房间权限。

登录与备份

跨节点登录需要用户启用并配置加密身份备份。没有备份时,账号登录仍依赖注册节点在线;仅同步用户名不能恢复账号。

高级接入:SSH 目录协议示例,需要个人身份授权;当前不属于 bridge 的 JSON-RPC 方法
{"op":"account_lookup","identifier":"alex"}
个人接入码是授权凭据

有效期最长 7 天。持有该码的设备可代表账号操作;使用系统安全存储保护接入码,不要写入日志、崩溃报告或同步到公开配置文件。

06 / CODE EXAMPLES

各语言复用同一个接口

下列为应用内调用片段;接入码、邀请码和游戏启动函数由你的启动器提供。C/C++ 例子中的 BridgeProcess 是启动器自身的双向进程管道封装,Java 例子使用 Jackson,C# 使用 System.Text.Json。协议请求的格式和顺序相同。

Pythonsubprocess + JSON
import json, subprocess
p = subprocess.Popen(["python", "-m", "hookup", "bridge"], stdin=subprocess.PIPE,
                     stdout=subprocess.PIPE, text=True, encoding="utf-8")
def rpc(i, method, params):
    p.stdin.write(json.dumps({"jsonrpc":"2.0", "id":i,
        "method":method, "params":params}) + "\n")
    p.stdin.flush(); reply = json.loads(p.stdout.readline())
    if "error" in reply: raise RuntimeError(reply["error"]["message"])
    return reply["result"]
rpc(1, "bridge.initialize", {"protocol":1})
node = rpc(2, "node.add", {"code": access_code})["node"]
room = rpc(3, "room.join", {"node":node, "invite":invite, "transport":"auto"})
launch_minecraft(room["address"])
rpc(4, "room.close", {"session":room["session"]})
C / C++OS pipes + nlohmann/json
// Use CreateProcess pipes (Windows) or pipe/posix_spawn (Unix) for the bridge process.
// Keep stderr separate; write_line/read_line exchange one UTF-8 JSON frame each.
#include <nlohmann/json.hpp>
using json = nlohmann::json;
json rpc(BridgeProcess& bridge, int id, const char* method, json params) {
  json request={{"jsonrpc","2.0"},{"id",id},{"method",method},{"params",params}};
  bridge.write_line(request.dump());
  auto response=json::parse(bridge.read_line(8192));
  if (response.contains("error")) throw std::runtime_error(response["error"]["message"].get<std::string>());
  return response.at("result");
}
auto init=rpc(bridge,1,"bridge.initialize",{{"protocol",1}});
auto node=rpc(bridge,2,"node.add",{{"code",access_code}}).at("node");
auto room=rpc(bridge,3,"room.join",{{"node",node},{"invite",invite},{"transport","auto"}});
launch_minecraft(room.at("address").get<std::string>());
rpc(bridge,4,"room.close",{{"session",room.at("session")}});
JavaProcessBuilder + Jackson
var process = new ProcessBuilder(pythonPath, "-m", "hookup", "bridge").start();
var in = new java.io.BufferedWriter(new java.io.OutputStreamWriter(process.getOutputStream(), java.nio.charset.StandardCharsets.UTF_8));
var out = new java.io.BufferedReader(new java.io.InputStreamReader(process.getInputStream(), java.nio.charset.StandardCharsets.UTF_8));
var json = new com.fasterxml.jackson.databind.ObjectMapper();
var sequence = new java.util.concurrent.atomic.AtomicInteger();
java.util.function.BiFunction<String, java.util.Map<String,Object>, com.fasterxml.jackson.databind.JsonNode> rpc = (method, params) -> {
  try {
    var q=java.util.Map.of("jsonrpc","2.0", "id",sequence.incrementAndGet(), "method",method, "params",params);
    in.write(json.writeValueAsString(q)); in.newLine(); in.flush();
    var reply=json.readTree(out.readLine());
    if (reply.has("error")) throw new RuntimeException(reply.get("error").toString());
    return reply.get("result");
  } catch (java.io.IOException e) { throw new RuntimeException(e); }
};
rpc.apply("bridge.initialize", java.util.Map.of("protocol",1));
var node=rpc.apply("node.add", java.util.Map.of("code",accessCode)).get("node").asText();
var room=rpc.apply("room.join", java.util.Map.of("node",node, "invite",invite, "transport","auto"));
launchMinecraft(room.get("address").asText());
rpc.apply("room.close", java.util.Map.of("session",room.get("session").asText()));
C#Process + System.Text.Json
var start = new System.Diagnostics.ProcessStartInfo(pythonPath, "-m hookup bridge") {
    RedirectStandardInput=true, RedirectStandardOutput=true, UseShellExecute=false, CreateNoWindow=true };
using var bridge = System.Diagnostics.Process.Start(start)!;
async System.Threading.Tasks.Task<System.Text.Json.JsonElement> Rpc(int id, string method, object parameters) {
  var q=System.Text.Json.JsonSerializer.Serialize(new { jsonrpc="2.0", id, method, @params=parameters });
  await bridge.StandardInput.WriteLineAsync(q); await bridge.StandardInput.FlushAsync();
  using var doc=System.Text.Json.JsonDocument.Parse((await bridge.StandardOutput.ReadLineAsync())!);
  var root=doc.RootElement; if (root.TryGetProperty("error",out var error)) throw new Exception(error.ToString());
  return root.GetProperty("result").Clone();
}
await Rpc(1,"bridge.initialize",new { protocol=1 });
var node=(await Rpc(2,"node.add",new { code=accessCode })).GetProperty("node").GetString();
var room=await Rpc(3,"room.join",new { node, invite, transport="auto" });
LaunchMinecraft(room.GetProperty("address").GetString());
await Rpc(4,"room.close",new { session=room.GetProperty("session").GetString() });
测试请使用自己部署的节点

连接能否直达取决于两端网络、路由器、防火墙及 UDP 状态。请分别测试 P2P 成功、UDP 被阻断后中继、手动仅中继、断线清理和多名玩家混合路径。

阅读协议细节 →
07 / RELEASE CHECK

上线前逐项验证

请在自己部署的节点上完成端到端测试,并明确告诉用户连接状态和路径。

  • 验证 SSH 节点公钥和 HTTPS 证书;身份不匹配时中止连接。
  • 接入码放入操作系统安全存储;日志隐藏 token、邀请码和签名私钥。
  • 本地游戏地址仅监听回环接口;房间退出后释放端口与后台任务。
  • 测试 P2P 自动连接、P2P 失败回退、仅中继和同房间混合连接。
  • 测试容量上限、邀请码错误、节点失联、登录过期、匿名用户权限和好友双方确认。
  • 说明中继不会使房间会话自动迁移;节点恢复或更换后需重新加入。