Files
MoonBridge/CLAUDE.md
T
layue13andClaude Opus 5.5 789648c82f refactor: 仓库根目录按用途分组
- proxy/:core、plugin-api、luckperms 及其上游子模块 vendor/luckperms
- messaging/、backend/ 保持在根目录(messaging 是代理与后端共用的 API)
- build-logic/(原 proxy-build-logic)
- testing/:smoke/ 与 benchmarks/,不进入发行物

Gradle 项目名不变(settings.gradle.kts 映射目录),任务路径与 Maven 坐标照旧。
脚本的仓库根定位、文档与记录中的链接同步更新。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 14:34:01 +08:00

9.6 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

MoonBridge is a session proxy for Minecraft 1.7.10 Forge (protocol 5): login/auth, frame-level forwarding, transfers between backends, plus a plugin API, a backend control channel and channel messaging. User-facing docs are Chinese and live in docs/ (start at docs/README.md); docs/architecture.md is the authoritative boundary description.

Build and test

JDK 25 is required (Gradle toolchain; the daemon JVM is pinned to 25). Use ./gradlew (.\gradlew.bat on Windows).

The LuckPerms platform plugin compiles LuckPerms from the proxy/vendor/luckperms submodule, which is slow and download-heavy, so it is opt-in: -Pluckperms includes luckperms-moonbridge, its tests, permissionAcceptance and the plugin in the distribution (run git submodule update --init --recursive first). CI always passes it; run it locally when touching permissions, plugin loading or the distribution layout.

./gradlew check                                       # tests, installed-dist smoke tests, benchmark compile
./gradlew -Pluckperms check                           # what CI runs, ~7 min: adds the LuckPerms plugin and permissionAcceptance
./gradlew release                                     # build/release: server zip, backend jar, client mod (-PskipClientMod to omit it)
./gradlew :proxy-core:test                            # proxy-core tests only
./gradlew :proxy-core:test --tests '*RawRelayTest'     # one class
./gradlew :proxy-core:test --tests '*RawRelayTest.m'   # one method
./gradlew :proxy-core:installDist                     # runnable distribution in proxy/core/build/install/moonbridge

The slow tests are the session integration tests (ProxySessionListenerTest, SessionTransferTest, InitialRoutingTest, SourceReleaseTransferTest); iterate with --tests and run check once before pushing.

.github/workflows/verify.yml runs -Pluckperms check and builds client-mod/ on every pull request; .github/workflows/ci.yml runs -Pluckperms check release, uploads build/release/ and publishes the Maven modules on pushes to main. Run -Pluckperms check locally before pushing anyway when the change can affect it: a red PR costs a round trip.

Modules

Directories group the modules by side; Gradle project names stay flat, so task paths and Maven artifact IDs are the same as the names below. settings.gradle.kts maps each name to its directory.

proxy/core          proxy-core            proxy/plugin-api   proxy-plugin-api
proxy/luckperms     luckperms-moonbridge  proxy/vendor/luckperms (submodule)
messaging           messaging             backend            backend
client-mod/         separate build        build-logic/       convention plugins (included build)
testing/smoke/      smoke scripts and dated records; testing/benchmarks/ likewise. Never packaged.
  • proxy-core — the proxy runtime (dev.moonbridge.app = startup/config, dev.moonbridge.core.* = everything else). The only module that uses Netty.
  • proxy-plugin-api — the stable plugin boundary. Plugins are discovered with META-INF/services/dev.moonbridge.api.Plugin and loaded in separate class loaders by PluginHost.
  • messaging — message envelope, channel API and wire codec shared by the proxy and backends.
  • backend — the Bukkit/Uranium host plugin MoonBridgeBackend (its jar bundles messaging), its service API (dev.moonbridge.bukkit) and the control channel client (dev.moonbridge.backendchannel). Business plugins compile against this jar compileOnly.
  • messaging and backend use moonbridge.java8-library-conventions (--release 8, for Java 8 backends such as Uranium) and must not pull in newer APIs.
  • luckperms-moonbridge — native LuckPerms platform plugin, built from proxy/vendor/luckperms via the included build proxy/luckperms/upstream-build; only included with -Pluckperms.
  • client-mod/ — the 1.7.10 Forge WebSocket client mod. A separate Gradle build (GTNH template, RetroFuturaGradle, Mixins) with its own wrapper: cd client-mod && ./gradlew build. Its net package is free of Minecraft classes and uses Minecraft's bundled Netty 4.0.10.
  • build-logic — Kotlin convention plugins: java-library, java8-library, application, and publishing-conventions (shared package repository; set moonbridgePublication { artifactId ... } for a standard publication).

Architecture that spans files

Session threading. ProxySessionListener owns the listener, event loops and the UUID→session index. Each Session (one per player connection) confines its mutable state to the frontend event loop, and its backend channels are created on that same loop (SessionChannels.backendBootstrap enforces it). Login verification, backend DNS and plugin callbacks run off that loop and re-enter it by posting a task; results are checked against the current connection identity so a stale async result cannot act on a reconnected player. Plugin work goes through bounded pools that reject when full.

Session layout. core.session holds Session (the Netty handler and lifecycle owner) plus the pieces it delegates to: FrontendLogin (handshake, status, encryption, Mojang verification), InitialRouter (placement decision and first backend dial), BackendHandshakes. core.session.transfer is TransferCoordinator, which reaches the session only through the TransferHost port (Session supplies it as an adapter) and owns all transfer state. core.session.play has the PLAY data path (RawRelay, the per-frame handlers, relay start-up buffering); core.session.channel has NetworkTransport, backend dialing, pipeline lookups and clientAddress (the player IP: a trusted forwarded address or the socket peer; use it instead of remoteAddress()). core.websocket is the optional WebSocket frontend; after the upgrade the pipeline behind WebSocketFrameAdapter is the same as for TCP. Dependencies point one way: session -> transfer -> play/channel.

Data path. After login, RawRelay forwards already-framed PLAY frames between the two channels without copying. Per-frame concerns are separate inbound handlers (KeepAliveBridge, PlayerCommandInterceptor, TabCompletionBridge, TransferFrameHandler) in front of the relay. Backpressure works by pausing reads on the source when the peer is not writable; the relay flushes once per read batch (channelReadComplete).

Transfers. Session.beginTransfer dials and logs into the candidate backend while the old link keeps serving, buffers frames from both sides (TransferFrameBuffer, TransitionBuffer), swaps relays, and rewrites the player's entity IDs afterwards (Minecraft1710EntityIds). Forge clients also need an FML reset/handshake before the world switch; TransferCandidate and PlayObservation track that state. Failure before the client-visible switch restores the old link; after it, the session is closed.

Transport. NetworkTransport picks epoll / kqueue / NIO and supplies the channel types for the listener, backend dials and the DNS resolver together. Create event loops only through it: a bare MultiThreadIoEventLoopGroup silently uses a LinkedBlockingQueue instead of the MPSC queue, which showed up as a measurable per-frame cost.

Buffer ownership. Production runs with Netty leak detection disabled (the start script passes -Dio.netty.leakDetection.level=disabled), so refcounting must be right by construction. Do not write new try/finally release() blocks. Use ByteBufs.fill/use for allocate-write-release-on-failure, extend FrameTransformHandler for per-frame inbound handlers (its transform borrows the frame and returns what to forward), and use Netty's MessageToMessageEncoder/Decoder for codecs. Tests run every proxy-core test class under PARANOID leak detection: LeakGate (auto-registered via junit-platform.properties) fails a class that leaks a buffer. It cannot see Unpooled.wrappedBuffer(byte[]) buffers, so assert refCnt() directly for those.

Backends and plugins. Backends come from static config, plugin registration (Servers.register, owner-scoped handles with generations) or the optional control channel (BackendControlService: separate authenticated blocking sockets on virtual threads, leases that outlive short disconnects). Initial routing is fail-closed: with no placement plugin it uses only initialRouting.servers. Permissions are a provider interface (PermissionService) with per-connection subjects that are prepared before login admission and released on any exit.

Testing notes

  • EmbeddedChannel.close() destroys the pipeline synchronously, so "message arrives after close" races only reproduce on a real NIO loop (see RawRelaySocketTest).
  • A suite-wide check that every hand-written release() is guarded was done by replacing each with a no-op and running the covering tests; keep that bar for new refcount code (the rationale and results are in docs/testing.md).

Performance work

Benchmarks live in testing/benchmarks/ (ProxySessionBenchmark, PowerShell runners; on Linux compile it against proxy/core/build/install/moonbridge/lib and run java --enable-native-access=ALL-UNNAMED -cp ... dev.moonbridge.core.session.ProxySessionBenchmark --burst 32 --window 4 --transport epoll). The existing records show the house method: set the decision rule first, rotate variant order across JVM runs, report median and range, keep raw output next to a dated testing/benchmarks/results/*.md that states what it does not cover, and prefer profile samples (JFR) over overlapping throughput ranges. Single-host loopback numbers are never capacity claims.