Imported from RavoxX/MCVoice (
AGENTS.md). Install upstream withnpx skills add RavoxX/MCVoice. Copyright stays with the author.
AGENTS.md — working on MCVoice
Guide for AI agents (and humans) contributing to this repository. Read it
before changing anything. User-facing docs live in README.md and docs/;
this file is about how to work here.
What this is
A proximity voice chat for Minecraft Java Edition (1.8 → 26.3) that needs no server plugin:
- Backend (
backend/rust,backend/go): two interchangeable implementations of one protocol. The Minecraft server is never on the voice path. - Client: a version-independent Java 8 core (
client/), plus thin adapters per Minecraft API family and loader (client/platform/<family>/). - Simple Voice Chat (SVC) interop: an independent re-implementation of the
SVC client protocol (
client/svc-compat), verified against the real SVC server in CI.
Normative protocol: protocol/specification/mcvoice-protocol-v1.md +
protocol/test-vectors/. If code and spec disagree, fix one of them, then
regenerate the vectors.
Non-negotiable rules
- Local entity rule. Never play positional audio from a speaker who
is not a currently tracked player entity in the listener's current local
world, in range.
PlaybackValidatoris checked on every positional frame of every transport. Never rely on server address, dimension name or coordinates alone (proxy sub-servers share all three). The only non-positional audio is voice groups: played centred, and only from members of the listener's current group (spec 9.1). Seedocs/proximity-security.md. - Never fake success.
- A Minecraft version/loader counts as supported only when CI built and
validated its jar (
versions/build-status.json). - Report exact failures. Never mark failing builds as done.
- Never claim a publish that did not happen.
- Never skip or disable tests to get green.
- A Minecraft version/loader counts as supported only when CI built and
validated its jar (
- No secrets in git.
- Never commit
.env(only.env.example), keys, tokens or certificates. CI runs gitleaks and a tracked-file check.
- Never commit
- Privacy.
- No voice recording and no raw audio retention.
- No exact positions in normal logs.
- Never expose player IPs to other clients.
- Never put Minecraft/Microsoft access tokens in MCVoice packets. The token
only goes to Mojang's
joinServer.
- No Simple Voice Chat code, assets, icons or sounds in this repository. The SVC server used by CI is downloaded at run time.
- No custom crypto. AES-128-GCM via standard libraries only.
client/core modules stay Java 8 (--release 8, no Minecraft classes). Onlyclient/platform/**touches Minecraft.
Repository map
| Path | Purpose |
|---|---|
protocol/ |
Spec, dedup state machine, test vectors (generate.py --check needs Python cryptography) |
backend/go |
Go backend, black-box conformance suite (pkg/conformance, cmd/mcvoice-conformance), Go test client |
backend/rust |
Rust backend (Tokio/axum) |
client/{common,network,audio,svc-compat,ui,core} |
Java 8 client core (Gradle 8.14 wrapper in client/) |
client/platform/families.json |
Which adapter sources + build setup serve which Minecraft versions |
client/platform/mojang/ |
Official-mappings family, 1.16.1–26.3: common/, fabric/, forge/ (EventBus 7, 1.21.6+), forge-eb6/ (1.19–1.21.5), forge-fml/ (1.16.1–1.18.2) |
client/platform/legacy/ |
1.8–1.12.2: mcp/ + forge/ (MCP names), yarn/ + fabric/ (Legacy Fabric, Legacy Yarn names) |
client/platform/mcp13/ |
1.13.2 Forge: common/ + forge/ (MCP stable_47-1.13.2 names, 1.13 API) |
tools/port-version/ |
port.py (generate minecraft/), preprocess.py, validate_jar.py, make-branch.sh, build templates, class remap tables |
tools/versions/ |
generate_matrix.py (official metadata → versions/versions.json), matrix.py, collect_status.sh |
tools/release/report.py |
Renders release-report.md from release artifacts |
tools/svc-interop/ |
Node bot for the real-SVC-server probe (the harness is client/svc-compat/src/test/.../tools/SvcProbe.java) |
tools/load-test/, tools/protocol-tests/ |
Load generator, cross-implementation runner |
deployment/ |
Compose (+ Caddy TLS) and Kubernetes |
versions/ |
versions.json, build-status.json, supported.md (generated; don't hand-edit) |
Branch model
main: everything above plus the latest client. Development happens here (or on feature branches merged into it).mc/<version>(one per supported version):main+ a generatedminecraft/directory. Never edit these by hand. Refresh them withtools/port-version/make-branch.sh <mc>|--all-supported [--push]. The script mergesmainand regenerates; history is never rewritten.tooling/probe-output: CI writes diagnostics here (probe output, build logs, per-loader status JSON, SVC probe results).tools/versions/collect_status.shreads it. It is not a release branch.
Everyday commands
python3 protocol/test-vectors/generate.py --check # vectors reproducible
python3 tools/versions/matrix.py validate
(cd backend/go && gofmt -l . && go vet ./... && go test -race ./...)
(cd backend/rust && cargo fmt --check && cargo clippy --all-targets --release -- -D warnings && cargo test --release)
# conformance (30 scenarios) against either backend binary
backend/go/mcvoice-conformance -name rust -- backend/rust/target/release/mcvoice-backend
# client core + headless end-to-end tests (needs a backend binary)
(cd client && MCVOICE_BACKEND_BIN=$PWD/../dist/mcvoice-backend-go ./gradlew build)
The Minecraft builds need network access to the loader mavens and are normally run in CI, not locally:
python3 tools/port-version/port.py --list # plan for every version (loaders + reasons)
python3 tools/port-version/port.py 1.20.1 # writes ./minecraft/<loader>/ standalone Gradle builds
(cd minecraft/fabric && ./gradlew build) # jar in build/libs/, validate with tools/port-version/validate_jar.py
CI workflows (.github/workflows/)
| Workflow | Trigger | What |
|---|---|---|
ci.yml |
push/PR | vectors, secrets scan, Go, Rust, conformance vs both, load smoke, client + e2e vs both backends, Java 8 bytecode check |
backend.yml |
backend changes on main, workflow_call |
images, container smoke test, GHCR push (edge/sha-*; release: <version>, latest only for stable) |
mc-build.yml |
push mc/**, dispatch {minecraft} |
builds each loader of one version with its own JDK, validates the jar, publishes log + status JSON to tooling/probe-output:builds/<mc>/ |
mc-probe.yml |
dispatch | API lookup for porting: official signatures (Mojang mappings or unobfuscated jar), extra jars (javap), Forge MDK build files, tiny/CSV mapping blocks, arbitrary URLs. Output → tooling/probe-output:<mc>/latest.txt |
mc-smoke.yml |
dispatch {minecraft} (dispatch only: runs third-party code) |
builds the versions, then starts the real client headlessly (headlesshq/mc-runtime-test 4.5.1 + HeadlessMC under Xvfb), joins a world and quits; passes only if MCVoice logged initialised and no MCVoice error; results → tooling/probe-output:smoke/<mc>/ |
svc-interop.yml |
svc-compat changes, weekly, dispatch {minecraft} |
Paper + real SVC plugin, bot + SvcProbe; verdict JSON → job summary and tooling/probe-output:svc-interop/<mc>/ |
version-matrix.yml |
tools/versions changes, weekly | regenerates versions/versions.json and commits to main (pull before pushing!) |
release.yml |
dispatch {version, minecraft, prerelease, images} |
full release; see docs/releasing.md |
Triggering from an agent session without gh: POST
https://api.github.com/repos/RavoxX/MCVoice/actions/workflows/<file>/dispatches
with Content-Type: application/json and
{"ref":"main","inputs":{...}}. Poll .../actions/workflows/<file>/runs.
Artifact downloads may be blocked in sandboxes. That is why diagnostics are
pushed to tooling/probe-output (git fetch origin tooling/probe-output && git show FETCH_HEAD:<path>).
How to port a Minecraft version (the loop that works)
python3 tools/port-version/port.py --list: is it in a family range, and does the loader exist upstream (versions/versions.json)?- Look up real APIs, never guess. Dispatch
mc-probe.ymlwith the classes you use (Mojang names), plusjars(loader API / Forge universal jar),jar_filter,mdk(the Forge version, to see the ForgeGradle and Gradle it expects) andmappings(a regex over Yarn/tiny files for Legacy Fabric). - Add
//#if MC >= x///#elif///#else///#endifblocks, andFABRIC/FORGE/LEGACYFABRICflags where needed. Inactive lines are blanked, so line numbers stay stable. Prefer a boundary you have seen in a probe or a compile error. - Add or extend a loader build setup in
client/platform/families.json:buildgen,gradle,plugin_version;- optionally
gradle_jdk,mappings,sources,parts,reobf,class_remap,gradle_properties.
- Dispatch
mc-build.ymlfor representative versions, readtooling/probe-output:builds/<mc>/<loader>.log, fix, repeat. Then build the whole range. bash tools/versions/collect_status.sh, then commitversions/. Refresh the branches withmake-branch.sh.
Hard-won facts (don't rediscover them)
Fabric and Loom
- Loom 1.18 (
fabric-loomfor unobfuscated 26.x,fabric-loom-remapfor older versions) needs Gradle ≥ 9.7 and a Java 25 Gradle JVM. That is thegradle_jdkfield; the game can still target 17 or 21. - Fabric API was split by minor line until 1.18 (
0.42.0+1.16is for 1.16.5 only). Its mod id wasfabricbefore 1.19.2 andfabric-apiafter (templates set${fabric_api_id}). Versions without an exact Fabric API release are listed as not built.
Forge 1.16.x and 1.17+
- Forge 1.16.x needs ForgeGradle 5.1 on Gradle 7.3.3. FG6 sets up the workspace, but the game never reaches the compile classpath.
- Before 1.17, Forge's official mappings rename members only. Classes
keep their MCP names; see
remap/forge-mcp-classes-1.16.txt. EventNetworkChannel.isRemotePresentexists from Forge 1.16.5 only. SVC interop is disabled below that: never send SVC messages to servers that might not have SVC.
Forge 1.20.x and 1.21.x
- Forge < 1.20.6 runs on SRG names, so
reobfJaris needed; 1.20.6+ runs on official names (reobf: false). - Forge 1.20.6–1.21.7 have no HUD layer registration. The HUD draws from
CustomizeGuiOverlayEvent.Chat. - EventBus 7 (1.21.6+): mod-bus events use
getBus(context.getModBusGroup())before 1.21.9 and staticBUSfrom then on. - The EventBus 7 adapter also needs
pack.mcmeta: without it, Forge 26.1.2 shows a loading warning and cannot load the mod's resources. Its format branches are verified against Mojang's clientversion.json; the jar validator requires this metadata.
Legacy Forge (1.8–1.12.x)
- 1.8.9–1.12.1 build with Essential's architectury-loom; 1.8 and 1.8.8 with Unimined 1.4.1.
- 1.12.2 builds with RetroFuturaGradle 2.0.4 (no userdev jar exists; RFG 2 needs a Java 25 Gradle JVM).
- MCP renames:
getConnectionfrom 1.9;player/world/fontRendererfrom 1.10. The font is read viaingameGUI.getFontRenderer().
Legacy Fabric
- Without Legacy Fabric API (1.8.1–1.8.8) the adapter uses its own mixins:
MinecraftClient#tick/stop/connect(null,…),GameOptions#load(keys intoallKeys). Preprocessor flagLEGACYFABRIC_API; Legacy Yarn build 604 (603 for 1.8.6).Window(MinecraftClient)exists from 1.8.2, not 1.8.1. - Legacy Fabric API exists only for 1.8, 1.8.9, 1.9.4, 1.10.2, 1.11.2 and 1.12.2.
- Its API classes live in the
-commonmodule artifacts. - It has no HUD callback, so the HUD is a mixin on
InGameHud#render.
Forge 1.13.2 (mcp13)
- No Mojang mappings before 1.14.4: MCP
stable_47-1.13.2(fg6 template, FG 5.1 on Gradle 7.3.3,mappings: "stable_47-1.13.2"). MCP class names are 1.12-style (GuiScreen,EntityPlayerSP,WorldClient,NetHandlerPlayClient). Translate Mojang member names through SRG ids (Mojang 1.14.4 mappings → MCPConfigjoined.tsrg→ MCP CSVs), matching methods by descriptor, never by obfuscated name alone. - 1.13 API:
GuiScreen()has no title,mouseScrolled(double delta),onGuiClosed,doesGuiPauseGame;Gui.drawRect;World#playerEntities;TickEventis innet.minecraftforge.fml.common.gameevent; noClientPlayerNetworkEvent(a lost world counts as a disconnect).FMLEnvironment/FMLPathslive in the Forgelauncherartifact.
Routing scope
- Routing never compares
network_id(the joined address): the same server has many addresses (aliases, IPs, tunnels, several proxies, LAN). Scope isworld_id(+ attested sub-server when both sides are attested); mutual visibility is mandatory and is what ties routing to the actual game. Recipients come from the sender's visible set via the UUID index.
Voice groups (protocol 1.1)
- Backend-only state (
backend/*/…/groups), up to 15 members, 5-char ids fromABCDEFGHJKLMNPQRSTUVWXYZ23456789, optional password (salted SHA-256, constant-time compare, 5 wrong tries/min), search viagroup_list.query.group_pagingadds id-ordered pages (limit 1–20, cursor, request_id); legacy lists retain popularity order and the 100-entry cap. Per-connection group budgets: list 2/s burst 4, create 0.1/s burst 3, join 1/s burst 6; leave has no extra limiter. Limits run in the control task before hub locking. Membership needsin_worldand ends on disconnect or after 10 s out of a world. Group messages are queued in the huboutboxunder the lock and sent after it (never send under the hub lock). - One frame serves both channels: mode 2 = group only, flag bit 1 on mode 0/1 = also to the group. Proximity routing skips members of the sender's group when group delivery applied (no double audio).
- Client: group channel = open mic gated by VAD while unmuted; proximity
keeps push-to-talk. Group frames are mixed centred and validated against
the current member list (
PlaybackValidatormode 2). - HUD clicks:
GuiAdapter.chatPointer()polls the cursor while the chat is open (MojangmouseHandler.xpos/ypos/isLeftPressedstable 1.14.4-26.3; 1.13.2mouseHelper.getMouseX/isLeftDown; legacy LWJGL 2Mouse).
Releases
- GitHub Packages never replaces a Maven version: a re-run records
exists(409) for those. A failed publish is retried once; ForgeGradle 7's Mavenizer once found its cachedmcp_configzip truncated.
Mojang family input
- Keys use
InputConstants.KEY_*from 1.20 on (26.3 no longer has LWJGL's GLFW on the compile classpath) andGLFW.GLFW_KEY_*below.
Validation
validate_jar.pyprints the platform classes of each jar. Every total is about 327 classes, because Java 8 targets add synthetic classes where newer targets use nestmates. The platform list is what matters.
Contributing conventions
-
Commits:
- Conventional-style subjects:
feat(platform): …,fix(port): …,ci(…),docs: …,chore(versions): …. - Explain the why in the body.
- Commit early and often.
- Conventional-style subjects:
-
Push:
maingets a bot commit fromversion-matrix.ymlwhenevertools/versions/**changes. Alwaysgit fetch origin main && git merge origin/mainbefore pushing, and re-run anything you dispatched from a rejected push. -
Style: match the surrounding code, keep comments sparse and useful. Run gofmt, rustfmt and clippy
-D warnings, which CI enforces. -
Changing the protocol:
- Update the spec.
- Update
generate.py, then regenerate and commit the vectors. - Update both backends and the Java client.
- Add or adjust conformance scenarios.
Both backends must pass the same suite.
-
Changing the client core: keep Java 8. Add a unit or headless e2e test (
client/coretests useFakeMinecraftand a real backend binary). -
Changing SVC compat: keep it an independent implementation and re-run
svc-interop.yml. Add a version toSvcProtocols.VERIFIEDonly after that probe confirms it. -
New Minecraft support: follow the porting loop. A version is only "supported" after CI passes, and
supported.mdis generated, never hand-written. -
Docs:
README.mdanddocs/*.mdmust match reality. Update them in the same change.
Current state and next work (keep this section updated)
- HUD editing (unreleased): the shared Java 8 client includes the local
speaker in the HUD during detected transmission. The microphone is hidden
during idle push-to-talk, grey for silence in PTT/open-mic modes, and green
while speaking. It defaults to 16 px at bottom right. General settings has
Edit HUD: independent dragging, vertical/horizontal speaker rows,
background toggle, 10–32 px microphone sizing, Done/Cancel/Reset. Layout is
saved in
hudLayoutinconfig/mcvoice.json, using normalized positions. This changes shared client code; the published 0.1.4 jars remain unchanged. - Passing: 62 Minecraft versions, 102 jars (
versions/build-status.json).- Every Forge release 1.8–1.12.2, 1.13.2 and 1.14.4–26.3.
- Fabric wherever Fabric API exists for 1.14.4–26.3.
- Legacy Fabric 1.8–1.8.9, 1.9.4, 1.10.2, 1.11.2, 1.12.2 (1.8.1–1.8.8 without Legacy Fabric API).
- Stable release: v0.1.4 (run 36308643678: 102/102 jars, 62/62 GitHub
Releases, 102/102 Maven packages, images
0.1.4/latest; all 134 jobs passed). Includes the 0.1.3-rc.1 features, optional cosmetic name-tag injections from issue #5, and modern Forge resource-pack metadata. Sourced337b49is inmain; runtime smoke passed six representative targets. Seedocs/release-0.1.4.md. Rust0.1.4is deployed on the public backend; readiness, public HTTPS and protocol capabilities were verified. - Previous candidate: v0.1.3-rc.1 (run 36277389029: 102/102 jars,
62/62 GitHub Releases, 102/102 Maven packages, both backend images). Adds
compact shaded speaking indicators and formatted HUD names across every
supported adapter, 20-entry group pages, and per-connection group budgets.
The owner tested the rendering changes in Fabric 26.1.2; the full matrix
was built and validated in CI. See
docs/release-0.1.3-rc.1.md. - Earlier stable release: v0.1.2 (run 36250351566: 102/102 jars, 62/62 GitHub
Releases, 102/102 Maven packages, images
0.1.2/latest) adds voice groups, the macOS microphone fix and vanilla-style screens. The public backend uses protocol 1.1 and advertisesgroups; see the deployment below for its current image. - Earlier release v0.1.0: every passing version has an
mc/branch and av0.1.0-mc<version>GitHub Release (62 releases). Full run 36112725820 (release-report.mdonv0.1.0), then 36115472419 for 1.8.1–1.8.8 Legacy Fabric, 1.13.2 and a retry of Forge 1.21.11 (release-report-run36115472419.md). Maven: every jar is in GitHub Packages; re-runs recordexists. Backend images0.1.0/latestpushed. - Public backend:
wss://mcvoice.ravoxx.dev/v1/control(Rust, GHCR image pinned byMCVOICE_VERSIONin/opt/mcvoice/.envon 5.83.145.152;/opt/mcvoice/update.shpulls and restarts only that container). Host nginx vhostmcvoice.ravoxx.dev→127.0.0.1:18455, certbotdns-cloudflarecertificate, DNS not proxied. UDP 24455 is opened in the host'svpn_hardeningnftables firewall via/etc/vpn-hardening/firewall-base.nft(input + forward). The host is a shared production server (mail, other sites): only ever add, validate (nginx -t,nft --check), never restart others. Since 0.1.1 the jars default to this backend (mcvoiceBackendUrlinclient/gradle.properties). Release0.1.4, revisiond337b49a68b8, deployed 2026-09-27 at 10:01 UTC after full release workflow36308643678passed. Local readiness, public HTTPS and protocol 1.1groups+group_pagingverified; Mojang auth and port bindings unchanged. The other 30 running containers were unchanged. A mode-600 environment backup was retained. Rollback image:0.1.3-rc.1. Measurements and limitations of that optimization are indocs/rust-routing-performance.md(allocation/presence-lock improvements; no general end-to-end latency improvement established). Initial deployment attempts timed out without changing production; the retry above succeeded. - SVC interop: verified against SVC 2.6.24 on Paper 1.18.2, 1.19.4, 1.20.1 and 1.21.4 (compatibility 20, AES-GCM with 12-byte IV). Older compatibility versions (19–16) are not verified.
- Client runtime smoke test: the owner approved it for 0.1.4. Run
36308348990passed all six targets: Fabric and Forge on 1.16.5, 1.21.1, and 26.1.2. The tested revision5948dc7has the same tree as mergedmainrevisiond337b49. This checks initialisation, world entry, clean exit and MCVoice runtime errors, not visual quality or real voice delivery. Initial runs exposed missing modern Forgepack.mcmetaand two harness issues: older Fabric API Maven jars are metadata-only (use the bundled GitHub release), and Forge's caught optional-mod lookup at TRACE is not a runtime error. The severity-aware log checker has regression tests and still fails MCVoice runtime stacks, error records and HUD failures. - Not implemented (reasons are in
versions/supported.md):- Forge 1.14.2/1.14.3 (MCP names, 1.14 class names: extend
mcp13); - Legacy Fabric 1.13.2 (no API; needs a Legacy Yarn 1.13 adapter).
- Forge 1.14.2/1.14.3 (MCP names, 1.14 class names: extend
- Voice groups: implemented in both backends (conformance 40/40) and the client (end-to-end tests against both backends). Version 0.1.3-rc.1 adds negotiated paging, scroll/search loading, stale-response protection and per-connection request limits. Legacy clients/backends retain their compatible list behavior.
- Ideas / next steps:
- expand runtime smoke coverage; add Legacy Fabric when mc-runtime-test supports it;
- group voice for SVC interop;
- receiving SVC voice from a second real client in CI;
- shared-state backend scaling (Redis).
