Merge pull request #4 from layue13/refactor/build-and-modules

重构构建与模块:后端 5 合 2、LuckPerms 可选构建、统一 release
This commit is contained in:
layue13 authored and GitHub committed 2026-10-21 03:26:15 -07:00
commit 480999a448
99 files changed
+374 -516

No files matched your search

+4 -8
View File
@@ -29,18 +29,14 @@ jobs:
- name: Set development version
run: echo "ORG_GRADLE_PROJECT_releaseVersion=0.1.0-dev.${GITHUB_RUN_ID}.${GITHUB_RUN_ATTEMPT:-1}.${GITHUB_SHA:0:12}" >> "$GITHUB_ENV"
- name: Check and assemble distribution
run: bash ./gradlew --no-daemon check :proxy-core:distZip
- name: Check and assemble the release
run: bash ./gradlew --no-daemon -Pluckperms check release
- name: Upload build outputs
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882
with:
name: moonbridge-development
path: |
**/build/libs/*.jar
!luckperms-moonbridge/build/libs/*-engine.jar
!vendor/**/build/libs/*.jar
proxy-core/build/distributions/*.zip
path: build/release/
if-no-files-found: error
retention-days: 30
@@ -75,4 +71,4 @@ jobs:
fi
- name: Publish all Maven modules
run: bash ./gradlew --no-daemon publish
run: bash ./gradlew --no-daemon -Pluckperms publish
+19 -4
View File
@@ -19,7 +19,22 @@ jobs:
with:
distribution: zulu
java-version: '25'
- name: Check
run: |
chmod +x ./gradlew
./gradlew --no-daemon check
- name: Check (including the LuckPerms plugin)
run: ./gradlew --no-daemon -Pluckperms check
client-mod:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
with:
persist-credentials: false
- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6
with:
distribution: zulu
java-version: '25'
- name: Build and test the 1.7.10 client mod
working-directory: client-mod
run: ./gradlew --no-daemon build
+18 -11
View File
@@ -6,33 +6,40 @@ MoonBridge is a session proxy for Minecraft 1.7.10 Forge (protocol 5): login/aut
## Build and test
JDK 25 is required (Gradle toolchain; the daemon JVM is pinned to 25). `gradlew` is not executable in a fresh Linux checkout, so use `bash ./gradlew` (`.\gradlew.bat` on Windows). The LuckPerms plugin builds from a submodule: run `git submodule update --init --recursive` first.
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 `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.
```sh
bash ./gradlew check # everything CI runs, ~7 min: tests, installed-dist smoke tests, permissionAcceptance
bash ./gradlew :proxy-core:test # proxy-core tests only
bash ./gradlew :proxy-core:test --tests '*RawRelayTest' # one class
bash ./gradlew :proxy-core:test --tests '*RawRelayTest.methodName' # one method
bash ./gradlew :proxy-core:installDist # runnable distribution in proxy-core/build/install/moonbridge
./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 `check` on every pull request; `.github/workflows/ci.yml` runs `check :proxy-core:distZip` and publishes the Maven modules on pushes to `main`. Run `check` locally before pushing anyway: it is ~7 minutes and a red PR costs a round trip.
`.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
- `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-api`, `messaging-protocol`, `backend-channel-client`, `backend-bukkit-api`, `backend-bukkit` — backend side. These compile with `--release 8` (Java 8 backends such as Uranium) and must not pull in newer APIs.
- `luckperms-moonbridge` — native LuckPerms platform plugin, built from `vendor/luckperms` via the included build `luckperms-moonbridge/upstream-build`. `proxy-core:check` depends on it (`permissionAcceptance`).
- `proxy-build-logic` — Kotlin convention plugins shared by all modules.
- `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 `vendor/luckperms` via the included build `luckperms-moonbridge/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.
- `proxy-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 per-frame PLAY handlers and relay start-up buffering; `core.session.channel` has 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`.
**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`).
+4 -3
View File
@@ -18,18 +18,19 @@
需要 JDK 25。发行包不包含 Minecraft 后端;要完成玩家登录,需要一个 Minecraft 1.7.10 后端,且 `initialRouting.servers` 中至少一个名称能解析到它(随附配置为 `lobby` → `127.0.0.1:25565`)。
```sh
git submodule update --init --recursive
bash ./gradlew check :proxy-core:installDist # Windows: .\gradlew.bat check :proxy-core:installDist
./gradlew check :proxy-core:installDist # Windows: .\gradlew.bat check :proxy-core:installDist
cd proxy-core/build/install/moonbridge
bin/moonbridge --validate-config config/moonbridge.yml # Windows: bin\moonbridge.bat
bin/moonbridge --config config/moonbridge.yml
```
需要全部部署物(服务器包、后端插件、客户端 mod)时运行 `./gradlew release`,说明见[发行物与安装位置](docs/release-layout.md)。LuckPerms 平台插件从 `vendor/luckperms` 子模块编译,默认不构建;需要时先 `git submodule update --init --recursive`,再加 `-Pluckperms`。
默认配置为 `OFFLINE`,仅监听 `127.0.0.1:25577`。公开部署使用 `ONLINE_BUNGEE` 并限制玩家直连后端;认证、监听、入口候选和后端地址见[运行配置](docs/operations.md)。发行包根目录附带 `docs/` 与按日期归档的 `smoke/results/`、`benchmarks/results/`。
## 后端插件开发
服务器安装 `moonbridge-backend-bukkit` JAR(插件名 `MoonBridgeBackend`)。业务插件对 `backend-bukkit-api` 使用 `compileOnly` / `provided`,并通过 `BukkitMessagingService` 获取共享消息服务;无需依赖宿主实现或创建网络连接。源码当前的 `0.1.0-SNAPSHOT` 仅是本地开发构件版本;发布依赖应使用发布工作流提供的不可变版本。API 与完整示例见[后端插件接入](docs/backend-channel-design.md#后端插件接入)。
服务器安装 `moonbridge-backend` JAR(插件名 `MoonBridgeBackend`)。业务插件对同一个构件 `uk.potatolab.moonbridge:backend` 使用 `compileOnly` / `provided`,并通过 `BukkitMessagingService` 获取共享消息服务;无需创建网络连接。源码当前的 `0.1.0-SNAPSHOT` 仅是本地开发构件版本;发布依赖应使用发布工作流提供的不可变版本。API 与完整示例见[后端插件接入](docs/backend-channel-design.md#后端插件接入)。
从 StrataProxy 升级见[迁移说明](docs/operations.md#从-strataproxy-迁移);历史验收记录保留当时的名称与构件路径。
-77
View File
@@ -1,77 +0,0 @@
import java.io.DataInputStream
import java.util.jar.JarFile
plugins {
`java-library`
`maven-publish`
}
java {
toolchain.languageVersion.set(JavaLanguageVersion.of(25))
withSourcesJar()
withJavadocJar()
}
tasks.withType<JavaCompile>().configureEach {
options.release.set(8)
options.encoding = "UTF-8"
}
dependencies {
api(project(":messaging-api"))
// Consumers compile with their server API; the running server supplies Bukkit.
compileOnly("org.spigotmc:spigot-api:1.8-R0.1-SNAPSHOT") { isTransitive = false }
}
val verifyApiJar = tasks.register("verifyApiJar") {
group = "verification"
dependsOn(tasks.jar)
val apiJar = tasks.jar.flatMap { it.archiveFile }
inputs.file(apiJar)
doLast {
JarFile(apiJar.get().asFile).use { archive ->
val entries = archive.entries().asSequence().toList()
val classes = entries.filter { it.name.endsWith(".class") }
check(classes.map { it.name }.toSet() == setOf(
"dev/moonbridge/bukkit/BackendPlayerSession.class",
"dev/moonbridge/bukkit/BukkitMessagingService.class",
"dev/moonbridge/bukkit/BukkitSessionService.class")) {
"Bukkit API artifact must contain only the public service contract"
}
check(entries.none { it.name == "plugin.yml" }) { "API is not a deployable plugin" }
classes.forEach { entry ->
DataInputStream(archive.getInputStream(entry)).use { input ->
input.readInt()
input.readUnsignedShort()
check(input.readUnsignedShort() == 52) { "Bukkit API must target Java 8" }
}
}
}
}
}
tasks.named("check") { dependsOn(verifyApiJar) }
publishing {
publications {
create<MavenPublication>("bukkitApi") {
from(components["java"])
artifactId = "backend-bukkit-api"
pom {
name.set("MoonBridge Bukkit API")
description.set("Java 8 service contract for plugins using the shared MoonBridge backend host.")
}
}
}
repositories {
maven {
name = "giteaPackages"
url = uri(providers.environmentVariable("MAVEN_URL")
.orElse("https://git.nest.potatolab.uk:8443/api/packages/layue13/maven").get())
credentials {
username = providers.environmentVariable("MAVEN_USER").orElse("NONE").get()
password = providers.environmentVariable("MAVEN_PASSWORD").orElse("NONE").get()
}
}
}
}
-102
View File
@@ -1,102 +0,0 @@
import java.io.DataInputStream
import java.util.jar.JarFile
plugins {
`java-library`
`maven-publish`
}
java {
toolchain.languageVersion.set(JavaLanguageVersion.of(25))
withSourcesJar()
withJavadocJar()
}
tasks.withType<JavaCompile>().configureEach {
options.release.set(8)
options.encoding = "UTF-8"
}
dependencies {
api(project(":backend-bukkit-api"))
implementation(project(":backend-channel-client"))
compileOnly("org.spigotmc:spigot-api:1.8-R0.1-SNAPSHOT") { isTransitive = false }
testImplementation("org.spigotmc:spigot-api:1.8-R0.1-SNAPSHOT") { isTransitive = false }
// Plugin's legacy API signature references Ebean; needed by the test proxy only.
testRuntimeOnly("org.avaje:ebean:2.8.1") { isTransitive = false }
testImplementation(platform("org.junit:junit-bom:5.14.1"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.withType<Test>().configureEach { useJUnitPlatform() }
// One provider owns the API classes, codec and transport. Consumers use compileOnly
// and plugin.yml depend, so Bukkit resolves the same service class identity.
tasks.jar {
archiveBaseName.set("moonbridge-backend-bukkit")
dependsOn(configurations.runtimeClasspath)
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
from({ configurations.runtimeClasspath.get().filter { it.extension == "jar" }.map { zipTree(it) } })
exclude("META-INF/*.SF", "META-INF/*.RSA", "META-INF/*.DSA")
}
val verifyHostJar = tasks.register("verifyHostJar") {
group = "verification"
dependsOn(tasks.jar)
val hostJar = tasks.jar.flatMap { it.archiveFile }
inputs.file(hostJar)
doLast {
JarFile(hostJar.get().asFile).use { archive ->
val entries = archive.entries().asSequence().toList()
val names = entries.map { it.name }
listOf("plugin.yml", "config.yml",
"dev/moonbridge/bukkit/BukkitMessagingService.class",
"dev/moonbridge/bukkit/BukkitSessionService.class",
"dev/moonbridge/bukkit/BackendPlayerSession.class",
"dev/moonbridge/messaging/Message.class",
"dev/moonbridge/messaging/internal/LocalMessaging.class",
"dev/moonbridge/messaging/protocol/MessageCodec.class",
"dev/moonbridge/messaging/session/ForwardedSessionProof.class",
"dev/moonbridge/messaging/session/ForwardedSessionProof\$Claims.class",
"dev/moonbridge/backendchannel/BackendChannelClient.class",
"dev/moonbridge/backendchannel/Wire\$RegisteredIdentity.class").forEach {
check(names.count { name -> name == it } == 1) { "Host jar must contain exactly one $it" }
}
check(names.none { it.startsWith("org/bukkit/") }) { "Bukkit API must remain server-provided" }
entries.filter { it.name.endsWith(".class") }.forEach { entry ->
DataInputStream(archive.getInputStream(entry)).use { input ->
input.readInt()
input.readUnsignedShort()
check(input.readUnsignedShort() <= 52) { "Host class is newer than Java 8: ${entry.name}" }
}
}
}
}
}
tasks.named("check") { dependsOn(verifyHostJar) }
publishing {
publications {
create<MavenPublication>("bukkitHost") {
from(components["java"])
artifactId = "backend-bukkit"
pom {
name.set("MoonBridge Backend Host")
description.set("Java 8 shared channel messaging service for Bukkit and Uranium plugins.")
}
}
}
repositories {
maven {
name = "giteaPackages"
url = uri(providers.environmentVariable("MAVEN_URL")
.orElse("https://git.nest.potatolab.uk:8443/api/packages/layue13/maven").get())
credentials {
username = providers.environmentVariable("MAVEN_USER").orElse("NONE").get()
password = providers.environmentVariable("MAVEN_PASSWORD").orElse("NONE").get()
}
}
}
}
-51
View File
@@ -1,51 +0,0 @@
plugins {
`java-library`
`maven-publish`
}
java {
withSourcesJar()
withJavadocJar()
toolchain.languageVersion.set(JavaLanguageVersion.of(25))
}
tasks.withType<JavaCompile>().configureEach {
options.release.set(8)
options.encoding = "UTF-8"
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
dependencies {
api(project(":messaging-api"))
api(project(":messaging-protocol"))
testImplementation(platform("org.junit:junit-bom:5.14.1"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
publishing {
publications {
create<MavenPublication>("backendChannelClient") {
from(components["java"])
artifactId = "backend-channel-client"
pom {
name.set("MoonBridge Backend Channel Client")
description.set("Java 8-compatible backend control channel SDK.")
}
}
}
repositories {
maven {
name = "giteaPackages"
url = uri(providers.environmentVariable("MAVEN_URL")
.orElse("https://git.nest.potatolab.uk:8443/api/packages/layue13/maven").get())
credentials {
username = providers.environmentVariable("MAVEN_USER").orElse("NONE").get()
password = providers.environmentVariable("MAVEN_PASSWORD").orElse("NONE").get()
}
}
}
}
+71
View File
@@ -0,0 +1,71 @@
import java.io.DataInputStream
import java.util.jar.JarFile
plugins {
id("moonbridge.java8-library-conventions")
id("moonbridge.publishing-conventions")
}
val spigotApi = "org.spigotmc:spigot-api:1.8-R0.1-SNAPSHOT"
dependencies {
api(project(":messaging"))
// The running server supplies Bukkit; business plugins compile against their own server API.
compileOnly(spigotApi) { isTransitive = false }
testImplementation(spigotApi) { isTransitive = false }
// Plugin's legacy API signature references Ebean; needed by the test proxy only.
testRuntimeOnly("org.avaje:ebean:2.8.1") { isTransitive = false }
}
moonbridgePublication {
artifactId.set("backend")
displayName.set("MoonBridge Backend")
description.set("Java 8 backend side of MoonBridge: the shared Bukkit host plugin (MoonBridgeBackend), its "
+ "service API and the control channel client for other hosts.")
}
// The jar is the deployable Bukkit plugin and the one copy of the messaging classes on a server.
// Business plugins use it compileOnly plus plugin.yml depend, so Bukkit resolves one service class identity.
tasks.jar {
archiveBaseName.set("moonbridge-backend")
dependsOn(configurations.runtimeClasspath)
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
from({ configurations.runtimeClasspath.get().filter { it.extension == "jar" }.map { zipTree(it) } })
exclude("META-INF/*.SF", "META-INF/*.RSA", "META-INF/*.DSA")
}
val verifyBackendJar = tasks.register("verifyBackendJar") {
group = "verification"
dependsOn(tasks.jar)
val backendJar = tasks.jar.flatMap { it.archiveFile }
inputs.file(backendJar)
doLast {
JarFile(backendJar.get().asFile).use { archive ->
val entries = archive.entries().asSequence().toList()
val names = entries.map { it.name }
listOf("plugin.yml", "config.yml",
"dev/moonbridge/bukkit/BukkitMessagingService.class",
"dev/moonbridge/bukkit/BukkitSessionService.class",
"dev/moonbridge/bukkit/BackendPlayerSession.class",
"dev/moonbridge/messaging/Message.class",
"dev/moonbridge/messaging/internal/LocalMessaging.class",
"dev/moonbridge/messaging/protocol/MessageCodec.class",
"dev/moonbridge/messaging/session/ForwardedSessionProof.class",
"dev/moonbridge/messaging/session/ForwardedSessionProof\$Claims.class",
"dev/moonbridge/backendchannel/BackendChannelClient.class",
"dev/moonbridge/backendchannel/Wire\$RegisteredIdentity.class").forEach {
check(names.count { name -> name == it } == 1) { "Backend jar must contain exactly one $it" }
}
check(names.none { it.startsWith("org/bukkit/") }) { "Bukkit API must remain server-provided" }
entries.filter { it.name.endsWith(".class") }.forEach { entry ->
DataInputStream(archive.getInputStream(entry)).use { input ->
input.readInt()
input.readUnsignedShort()
check(input.readUnsignedShort() <= 52) { "Backend class is newer than Java 8: ${entry.name}" }
}
}
}
}
}
tasks.named("check") { dependsOn(verifyBackendJar) }
+2 -2
View File
@@ -49,8 +49,8 @@ public final class ProxySessionBenchmark {
int commandInterceptor = intArg(args, "command-interceptor", 1);
int burst = intArg(args, "burst", 1);
String mode = modeArg(args);
var transport = dev.moonbridge.core.net.NetworkTransport.select(
dev.moonbridge.core.net.NetworkTransport.Preference.valueOf(
var transport = dev.moonbridge.core.session.channel.NetworkTransport.select(
dev.moonbridge.core.session.channel.NetworkTransport.Preference.valueOf(
stringArg(args, "transport", "auto").toUpperCase(java.util.Locale.ROOT)));
boolean afterTransfer = mode.equals("post-transfer");
if (connections < 1 || connections > 32 || messages < 1 || messages > 100_000
+1 -1
View File
@@ -5,7 +5,7 @@ import dev.moonbridge.core.session.play.KeepAliveBridge;
import dev.moonbridge.core.protocol.MinecraftFrameDecoder;
import dev.moonbridge.core.protocol.ProtocolProfile;
import dev.moonbridge.core.protocol.ProtocolVarInt;
import dev.moonbridge.core.relay.RawRelay;
import dev.moonbridge.core.session.play.RawRelay;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
+37 -5
View File
@@ -10,15 +10,47 @@ allprojects {
version = releaseVersion
}
val luckPerms = findProject(":luckperms-moonbridge")
val published = listOfNotNull(":proxy-plugin-api", ":messaging", ":backend", luckPerms?.path)
tasks.register("check") {
group = "verification"
dependsOn(":proxy-plugin-api:check", ":messaging-api:check", ":messaging-protocol:check",
":backend-channel-client:check", ":backend-bukkit-api:check", ":backend-bukkit:check", ":proxy-core:check",
":luckperms-moonbridge:check")
description = "Runs every module's checks (add -Pluckperms to include the LuckPerms plugin)."
dependsOn(subprojects.map { "${it.path}:check" })
}
tasks.register("publish") {
group = "publishing"
dependsOn(":proxy-plugin-api:publish", ":messaging-api:publish", ":messaging-protocol:publish",
":backend-channel-client:publish", ":backend-bukkit-api:publish", ":backend-bukkit:publish", ":luckperms-moonbridge:publish")
description = "Publishes the Maven modules (CI passes -Pluckperms so the LuckPerms plugin is included)."
dependsOn(published.map { "$it:publish" })
}
// client-mod/ is a separate Gradle build (GTNH toolchain for Minecraft 1.7.10); release runs it through its
// own wrapper. Pass -PskipClientMod to leave it out, e.g. where the Minecraft toolchain cannot be downloaded.
val skipClientMod = providers.gradleProperty("skipClientMod").map { it != "false" }.getOrElse(false)
val windows = System.getProperty("os.name").lowercase().contains("windows")
val buildClientMod = tasks.register<Exec>("buildClientMod") {
group = "build"
description = "Builds the 1.7.10 Forge WebSocket client mod in client-mod/."
workingDir = file("client-mod")
// clean: the jar name carries the git version, so older jars would otherwise be collected too.
val gradle = if (windows) listOf("cmd", "/c", "gradlew.bat") else listOf("bash", "./gradlew")
commandLine(gradle + listOf("clean", "build"))
}
tasks.register<Sync>("release") {
group = "distribution"
description = "Collects everything a deployment needs into build/release (see docs/release-layout.md)."
into(layout.buildDirectory.dir("release"))
from(file("docs/release-layout.md")) { rename { "README.md" } }
from(project(":proxy-core").tasks.named("distZip")) { into("server") }
from(project(":backend").tasks.named("jar")) { into("backend") }
if (!skipClientMod) {
dependsOn(buildClientMod)
from(fileTree("client-mod/build/libs") {
include("moonbridgews-*.jar")
exclude("*-dev.jar", "*-sources.jar")
}) { into("client") }
}
}
+1
View File
@@ -23,6 +23,7 @@
| [后端 Channel 消息服务](backend-channel-design.md) | 共享宿主、跨后端路由、消息身份与交付语义 |
| [Channel 消息设计契约](channel-messaging-contract.md) | 从参与者、寻址、响应与失败语义推导 API 和验收条件 |
| [验证方法](testing.md) | 自动化测试、烟测、真实客户端与合成性能测量 |
| [发行物与安装位置](release-layout.md) | `gradlew release` 产出的服务器包、后端插件、客户端 mod 各装在哪里 |
| [发布与依赖版本](publishing.md) | 本地 SNAPSHOT、CI 不可变版本、Maven 构件和发行包的区别 |
## 当前可引用的验证记录
+6 -4
View File
@@ -6,6 +6,7 @@
- `proxy-plugin-api` 定义插件外部接口:生命周期、玩家/服务器只读视图、后端注册、类型化事件、初始选服、命令和转服。核心实现位于 `proxy-core`;可选发现器位于 `plugins/`。
- `ProxyMain` 读取严格 YAML 配置,先注册静态后端,再加载配置启用的插件,连接插件选服回调,最后启动 Netty 监听器。插件以 `META-INF/services/dev.moonbridge.api.Plugin` 声明,通过独立类加载器加载。
- 后端一侧只有两个 Java 8 模块:`messaging`(消息 API 与线协议,代理与后端共用)和 `backend`(Bukkit/Uranium 共享宿主 `MoonBridgeBackend`、其服务 API 与控制通道客户端,打成一个 JAR)。`luckperms-moonbridge` 是可选的原生权限插件(`-Pluckperms` 构建);`client-mod/` 是独立构建的 1.7.10 客户端 WebSocket mod。
- 核心支持 Minecraft 1.7.10(协议 5)登录和转发。`OFFLINE` 信任客户端提交的名字;`ONLINE_BUNGEE` 在代理执行加密与会话验证,并将身份转发给受信任的旧版 Bungee 后端。
入口可从 [`ProxyMain`](../proxy-core/src/main/java/dev/moonbridge/app/ProxyMain.java)、[`ProxySessionListener`](../proxy-core/src/main/java/dev/moonbridge/core/session/ProxySessionListener.java) 和 [`PluginHost`](../proxy-core/src/main/java/dev/moonbridge/core/plugin/PluginHost.java) 追踪。
@@ -30,7 +31,7 @@ flowchart LR
F --> G[PLAY / Forge 握手与转发]
```
普通转发沿用协议 5 帧边界,不重新编码普通帧;同一次 socket 读取解码出的帧逐个写入对端,在读取完成时合并为一次 flush,读取之外转发的帧由每批一次的事件循环任务补发 flush。前后端通道绑定同一 Netty EventLoop,会话控制状态在该循环串行变更。Keep Alive ID 由代理转换并跟踪未完成请求;必要的玩家实体 ID 数据包在转服后改写。通道不可写时通过暂停对端读取施加背压。实现见 [`Session`](../proxy-core/src/main/java/dev/moonbridge/core/session/Session.java)、[`RawRelay`](../proxy-core/src/main/java/dev/moonbridge/core/relay/RawRelay.java) 与 [`KeepAliveBridge`](../proxy-core/src/main/java/dev/moonbridge/core/session/play/KeepAliveBridge.java)。
普通转发沿用协议 5 帧边界,不重新编码普通帧;同一次 socket 读取解码出的帧逐个写入对端,在读取完成时合并为一次 flush,读取之外转发的帧由每批一次的事件循环任务补发 flush。前后端通道绑定同一 Netty EventLoop,会话控制状态在该循环串行变更。Keep Alive ID 由代理转换并跟踪未完成请求;必要的玩家实体 ID 数据包在转服后改写。通道不可写时通过暂停对端读取施加背压。实现见 [`Session`](../proxy-core/src/main/java/dev/moonbridge/core/session/Session.java)、[`RawRelay`](../proxy-core/src/main/java/dev/moonbridge/core/session/play/RawRelay.java) 与 [`KeepAliveBridge`](../proxy-core/src/main/java/dev/moonbridge/core/session/play/KeepAliveBridge.java)。
## 后端目录与发现所有权
@@ -69,10 +70,11 @@ flowchart LR
|---|---|
| `core.session` | `ProxySessionListener`(监听与会话索引);`Session`(每个玩家连接一个,Netty handler 与生命周期)及其委托:`FrontendLogin`(握手、状态、加密、Mojang 验证)、`InitialRouter`(放置决策与首个后端拨号)、`ClientControl`(代发聊天与有序断开)、`BackendHandshakes` |
| `core.session.transfer` | `TransferCoordinator` 转服状态机,经 `TransferHost` 接口读取会话状态,自己持有全部转服状态 |
| `core.session.play` | PLAY 阶段逐帧 handler(Keep Alive、命令拦截、Tab 补全)与中继启动缓冲 |
| `core.session.channel` | 后端拨号、pipeline 查找辅助与玩家地址(`clientAddress`) |
| `core.session.play` | PLAY 阶段数据通路:`RawRelay` 帧转发热路径、逐帧 handler(Keep Alive、命令拦截、Tab 补全)与中继启动缓冲 |
| `core.session.channel` | `NetworkTransport`(epoll/kqueue/NIO 选择)、后端拨号、pipeline 查找辅助与玩家地址(`clientAddress`) |
| `core.websocket` | WebSocket 前端:升级、可信代理真实 IP(`ClientAddressPolicy`)、二进制消息与字节流互转;不依赖 `session` |
| `core.relay` | `RawRelay` 帧转发热路径 |
| `core.auth` | 在线认证与加密、Mojang 会话验证、Bungee 旧版身份转发(`BungeeLegacyForwarding`) |
| `core.event` | 会话调用插件运行时的端口:`EventDispatcher`、转服准备、`PermissionService` |
| `core.plugin` | `PluginHost` 生命周期,分出 `EventRouter`、`CommandService`、`PlacementService`、`PluginContextImpl` |
| `core.control` | 后端控制通道:`BackendControlService` 接入,`LeaseRegistry`、`InboundRouter`、`BackendExchange`、`OutboundWriter` |
+5 -5
View File
@@ -25,7 +25,7 @@ flowchart LR
## 后端插件接入
在 Bukkit/Uranium 后端安装一次 `backend-bukkit` 生成的 `moonbridge-backend-bukkit` JAR,插件名为 `MoonBridgeBackend`。配置唯一的实例 ID、后端名称、代理控制地址、游戏地址与凭据。宿主异步建立连接,不在启动主线程等待网络。
在 Bukkit/Uranium 后端安装一次 `backend` 模块生成的 `moonbridge-backend` JAR(发行包 `backend/` 目录),插件名为 `MoonBridgeBackend`。配置唯一的实例 ID、后端名称、代理控制地址、游戏地址与凭据。宿主异步建立连接,不在启动主线程等待网络。
消费者 `plugin.yml` 声明:
@@ -33,7 +33,7 @@ flowchart LR
depend: [MoonBridgeBackend]
```
消费者依赖独立的 `uk.potatolab.moonbridge:backend-bukkit-api`,它传递依赖平台无关的 `messaging-api`。源码默认 `0.1.0-SNAPSHOT` 是本地开发版本;使用已发布构件时,必须将示例中的 `<published-version>` 替换为 CI 成功发布的准确不可变版本。`backend-bukkit` 是部署到服务器的宿主实现,`backend-channel-client` 是其他平台实现宿主时使用的传输 SDK,普通业务插件不依赖它们。发布版本及构件列表见[发布与依赖版本](publishing.md)。
消费者对 `uk.potatolab.moonbridge:backend` 使用 `compileOnly`,它就是服务器上安装的那个 JAR,传递依赖平台无关的 `messaging`。业务插件只应使用 `dev.moonbridge.bukkit` 中的 `BukkitMessagingService`、`BukkitSessionService`、`BackendPlayerSession` 和 `dev.moonbridge.messaging` 的公共 API;`dev.moonbridge.backendchannel` 是宿主内部的控制通道客户端,只有在其他平台自行实现宿主时才直接使用。源码默认 `0.1.0-SNAPSHOT` 是本地开发版本;使用已发布构件时,必须将示例中的 `<published-version>` 替换为 CI 成功发布的准确不可变版本。发布版本及构件列表见[发布与依赖版本](publishing.md)。
Gradle 示例(Bukkit API 仍由业务插件按目标服务器版本自行声明):
@@ -42,11 +42,11 @@ repositories {
maven("https://git.nest.potatolab.uk:8443/api/packages/layue13/maven")
}
dependencies {
compileOnly("uk.potatolab.moonbridge:backend-bukkit-api:<published-version>")
compileOnly("uk.potatolab.moonbridge:backend:<published-version>")
}
```
Maven 使用同一坐标并设置 `<scope>provided</scope>`。本地开发可改用 `0.1.0-SNAPSHOT`,但不要把它当作已发布版本。发行包的 `backend-api/` 也包含编译所需的 API JAR,适合隔离构建环境直接编译。本例中的 Bukkit API 仍由业务插件按目标服务器版本自行声明为 `compileOnly` / `provided`。不要把 API、SDK 或宿主实现重新打包进业务插件 JAR,也不要把 API JAR 单独放进服务器 `plugins/`;运行时由 `MoonBridgeBackend` 提供同一份 API 类和服务实现。
Maven 使用同一坐标并设置 `<scope>provided</scope>`。本地开发可改用 `0.1.0-SNAPSHOT`,但不要把它当作已发布版本。隔离构建环境也可以直接对发行包 `backend/` 中的 JAR 编译。本例中的 Bukkit API 仍由业务插件按目标服务器版本自行声明为 `compileOnly` / `provided`。不要把这个 JAR 重新打包进业务插件;运行时由服务器 `plugins/` 中的 `MoonBridgeBackend` 提供唯一一份 API 类和服务实现。
```java
import dev.moonbridge.bukkit.BukkitMessagingService;
@@ -116,7 +116,7 @@ HMAC 挑战认证本身不加密链路;跨机器使用私有加密链路或可
本次构建、244 项回归和实际 Java 8 进程的结果见 [2026-09-27 验证记录](../smoke/results/2026-09-27-channel-messaging.md)。
安装发行包的 `backend-host/` 提供可直接安装的共享宿主 JAR;`backend-client/` 提供非 Bukkit 宿主所需的 SDK、消息 API 和协议三个 JAR。运行 `:proxy-core:installDist` 后,可用实际 JDK 8 运行独立进程验收:
安装发行包的 `backend/` 提供共享宿主 JAR;它同时包含非 Bukkit 宿主所需的控制通道客户端、消息 API 和协议。运行 `:proxy-core:installDist` 后,可用实际 JDK 8 运行独立进程验收:
```powershell
.\smoke\channel-java8.ps1 -Java8Home 'C:\path\to\jdk8' -Java25Home 'C:\path\to\jdk25'
+2 -2
View File
@@ -22,7 +22,7 @@
MoonBridge 是破坏性改名,旧包名/API、启动程序和配置文件没有兼容别名。升级时将 `strataproxy` 启动程序改为 `moonbridge`、将 `strataproxy.yml` 迁移为 `moonbridge.yml`,并检查 `dev.strataproxy` Java 导入改为 `dev.moonbridge`。旧配置中的 `plugins.initialPlacementTimeoutSeconds` 改为 `initialRouting.timeoutSeconds`;首次登录入口应显式写入 `initialRouting.servers`,不要依赖注册顺序。
后端宿主插件从 `StrataProxyBackend` 改为 `MoonBridgeBackend`。更新业务插件 `plugin.yml` 的 `depend`、安装新的宿主 JAR,并将旧插件配置迁移到新宿主插件的数据目录。消费者需重新编译;已删除的 `backendChannels()` API 应迁移到 `messaging()`,Bukkit 业务插件改用 `backend-bukkit-api` 提供的 `BukkitMessagingService`。迁移后的依赖和宿主接法见[后端 Channel 接入](backend-channel-design.md#后端插件接入)。
后端宿主插件从 `StrataProxyBackend` 改为 `MoonBridgeBackend`。更新业务插件 `plugin.yml` 的 `depend`、安装新的宿主 JAR,并将旧插件配置迁移到新宿主插件的数据目录。消费者需重新编译;已删除的 `backendChannels()` API 应迁移到 `messaging()`,Bukkit 业务插件改用 `backend` 构件提供的 `BukkitMessagingService`。迁移后的依赖和宿主接法见[后端 Channel 接入](backend-channel-design.md#后端插件接入)。
## 服务器列表
@@ -74,6 +74,6 @@ Docker 容器间的监听、服务名和地址配置见[配置与部署](backend
- 登录、认证和协议:`proxy-core/src/test/java/dev/moonbridge/core/auth/` 与 `proxy-core/src/test/java/dev/moonbridge/core/protocol/`
- 会话、断开、转服与缓冲:`proxy-core/src/test/java/dev/moonbridge/core/session/`,重点包括 `SessionTransferTest`、`SessionTransferDispatchTest`、`transfer/TransferFrameBufferTest`
- 插件生命周期、访问检查和 API:`proxy-core/src/test/java/dev/moonbridge/core/plugin/PluginHostTest.java`、`proxy-core/src/test/java/dev/moonbridge/core/session/ProxySessionListenerTest.java`、`proxy-core/src/test/java/dev/moonbridge/app/ProxyConfigurationTest.java` 与 `proxy-plugin-api/src/test/`
- 控制通道:`proxy-core/src/test/java/dev/moonbridge/core/control/` 与 `backend-channel-client/src/test/`
- 控制通道:`proxy-core/src/test/java/dev/moonbridge/core/control/` 与 `backend/src/test/`
仓库另有本地 Uranium 协议探针和 Prism Forge 客户端记录,见 [`smoke/results`](../smoke/results/2026-09-25-local-uranium.md)、[转服记录](../smoke/results/2026-09-25-local-uranium-transfer.md) 和 [真实客户端记录](../smoke/results/2026-09-25-prism-forge-client.md)。这些记录支持其明确写出的单机环境、软件版本和探针路径。已知也出现过真实 Forge 客户端转服候选后端登录停滞,见[调查记录](../smoke/results/2026-09-26-uranium-login-stalls.md);已有成功样例不构成稳定性或生产验收。
+3 -2
View File
@@ -55,13 +55,14 @@ context.commands().register("island", "example.command.island", invocation -> {
## LuckPerms 平台插件
`luckperms-moonbridge` 使用固定版本的官方 LuckPerms 公共核心,并实现 MoonBridge 平台适配。上游源码在 Git 子模块 `vendor/luckperms`,构建前运行:
`luckperms-moonbridge` 使用固定版本的官方 LuckPerms 公共核心,并实现 MoonBridge 平台适配。上游源码在 Git 子模块 `vendor/luckperms`。编译它较慢且依赖很多,因此默认构建不包含该插件,需要显式开启:
```sh
git submodule update --init --recursive
./gradlew -Pluckperms check release # 构建插件、运行 permissionAcceptance,并放进发行包
```
发行包在 `plugins/` 提供插件 JAR。修改 `config/moonbridge.yml`,替换默认的 `enabled: {}`:
带 `-Pluckperms` 构建的发行包(CI 和正式发布均如此)在 `plugins/` 提供插件 JAR。修改 `config/moonbridge.yml`,替换默认的 `enabled: {}`:
```yaml
plugins:
+10 -11
View File
@@ -6,9 +6,9 @@ MoonBridge 当前区分本地开发版本、CI 发布的不可变开发构件和
`.github/workflows/ci.yml` 在推送 `main` 或手动触发时运行。工作流以 CI run ID、attempt 和提交的前 12 位 SHA 生成版本 `0.1.0-dev.<run-id>.<attempt>.<commit>`,然后按顺序运行:
1. 使用 JDK 25 运行 `gradlew check :proxy-core:distZip`。
2. 将 JAR 和发行 ZIP 上传为名为 `moonbridge-development` 的 CI 工件,保留 30 天。
3. 执行 `gradlew publish`,发布七个 Maven 模块。
1. 使用 JDK 25 运行 `gradlew -Pluckperms check release`。
2. 将 `build/release/`(服务器包、后端插件、客户端 mod,见[发行物与安装位置](release-layout.md))上传为名为 `moonbridge-development` 的 CI 工件,保留 30 天。
3. 执行 `gradlew -Pluckperms publish`,发布四个 Maven 模块。
Maven 发布需要 CI secrets `MAVEN_USER` 和 `MAVEN_PASSWORD`。验证构建和发行 ZIP 的步骤先运行;凭据缺失会使发布步骤失败。CI 成功状态是发布结果的依据;CI 工件下载和 Maven 包仓库是不同交付物。工作流不创建 Git Release,也不表示已有稳定版本。
@@ -21,14 +21,13 @@ Maven 坐标统一使用 group `uk.potatolab.moonbridge` 和 CI 所报版本:
| Artifact ID | 用途 |
| --- | --- |
| `proxy-plugin-api` | Proxy 插件 API |
| `messaging-api` | 跨节点消息公共 API |
| `messaging-protocol` | 消息传输协议 |
| `backend-channel-client` | 非 Bukkit 后端宿主使用的传输 SDK |
| `backend-bukkit-api` | Bukkit 业务插件编译时使用的服务 API |
| `backend-bukkit` | 部署在 Bukkit/Uranium 服务端的共享宿主实现 |
| `messaging` | 跨节点消息公共 API 与线协议(Java 8) |
| `backend` | 后端共享宿主 `MoonBridgeBackend`(Bukkit/Uranium 插件,Java 8),含其服务 API 与控制通道客户端;业务插件对它 `compileOnly` |
| `luckperms-moonbridge` | Proxy 原生 LuckPerms 插件;不是业务插件的编译 API |
七个模块均随同一次 CI 发布使用相同版本。业务插件通常将 `backend-bukkit-api` 和目标服务器的 Bukkit API 作为 `compileOnly` / Maven `provided` 依赖;服务器提供 Bukkit API,`MoonBridgeBackend` 宿主提供运行时消息 API 和服务。普通业务插件不应依赖宿主/传输 SDK 实现,也不应把任何 API JAR 打包进插件。非 Bukkit 宿主才需要 `backend-channel-client` 及消息 API/协议;见[后端接入说明](backend-channel-design.md)。
四个模块均随同一次 CI 发布使用相同版本。业务插件将 `backend` 和目标服务器的 Bukkit API 作为 `compileOnly` / Maven `provided` 依赖;服务器提供 Bukkit API,`MoonBridgeBackend` 宿主提供运行时消息 API 和服务。不要把 `backend` 打包进插件。非 Bukkit 宿主也使用 `backend` 中的控制通道客户端;见[后端接入说明](backend-channel-design.md)。
旧坐标 `messaging-api`、`messaging-protocol`、`backend-channel-client`、`backend-bukkit-api`、`backend-bukkit` 已停止发布:前两者合并为 `messaging`,后三者合并为 `backend`,包名与类名不变,只需替换依赖坐标并重新编译。
包仓库地址为 `https://git.nest.potatolab.uk:8443/api/packages/layue13/maven`。消费者应从成功 CI 记录中复制实际版本;以下 `<published-version>` 只是需替换的说明占位符:
@@ -38,14 +37,14 @@ repositories {
}
dependencies {
compileOnly("uk.potatolab.moonbridge:backend-bukkit-api:<published-version>")
compileOnly("uk.potatolab.moonbridge:backend:<published-version>")
}
```
```xml
<dependency>
<groupId>uk.potatolab.moonbridge</groupId>
<artifactId>backend-bukkit-api</artifactId>
<artifactId>backend</artifactId>
<version>&lt;published-version&gt;</version>
<scope>provided</scope>
</dependency>
+16
View File
@@ -0,0 +1,16 @@
# 发行物与安装位置
`bash ./gradlew release`(Windows:`.\gradlew.bat release`)把部署需要的东西收集到 `build/release/`:
| 路径 | 是什么 | 装在哪里 |
| --- | --- | --- |
| `server/moonbridge-<版本>.zip` | 代理发行包:`bin/` 启动脚本、`lib/`、`config/` 示例配置、`docs/`;带 `-Pluckperms` 构建时 `plugins/` 里含 LuckPerms 平台插件 | 代理服务器,解压后用 JDK 25 运行 `bin/moonbridge --config config/moonbridge.yml` |
| `backend/moonbridge-backend-<版本>.jar` | 后端共享宿主 `MoonBridgeBackend`(Bukkit/Uranium 插件,Java 8),内含消息 API 与控制通道客户端 | 每台后端服务器的 `plugins/`;后端业务插件对它 `compileOnly` 并在 `plugin.yml` 里 `depend: [MoonBridgeBackend]` |
| `client/moonbridgews-<版本>.jar` | 1.7.10 Forge WebSocket 客户端 mod | 玩家客户端 `mods/`,另需 UniMixins;只在使用 `ws://`/`wss://` 地址时需要 |
可选开关:
- `-Pluckperms`:同时编译 LuckPerms 平台插件(需要 `git submodule update --init --recursive`),并运行 `permissionAcceptance`。CI 和正式发布都带这个开关。
- `-PskipClientMod`:不构建客户端 mod(它需要下载 Minecraft 1.7.10 工具链)。
Maven 坐标(group `uk.potatolab.moonbridge`):`proxy-plugin-api`(代理插件 API)、`messaging`(消息 API 与协议)、`backend`(后端宿主及其 API)、`luckperms-moonbridge`(LuckPerms 平台插件)。版本与仓库见[发布与依赖版本](publishing.md)。
+4 -4
View File
@@ -5,7 +5,7 @@
```powershell
.\gradlew.bat check
.\gradlew.bat :proxy-core:installedDistSmokeTest
.\gradlew.bat :proxy-core:permissionAcceptance
.\gradlew.bat -Pluckperms :proxy-core:permissionAcceptance
.\smoke\container-network.ps1
sh smoke/container-channel.sh
```
@@ -14,9 +14,9 @@ sh smoke/container-channel.sh
`smoke/container-network.ps1` 使用 Docker bridge 中的独立代理、后端和客户端容器,依次检查静态配置和控制通道注册、登录及 PLAY 转发;控制通道模式还验证注销后已连接会话继续转发。PowerShell 7 和 Docker Desktop 是运行前提。`smoke/container-channel.sh` 在 Unix shell 下验证控制通道的同一跨容器路径。2026-09-25 的[容器网络烟测记录](../smoke/results/2026-09-25-container-network.md)是旧实现的历史结果,不代表当前通道已在那次运行中验证。容器烟测不能代替跨物理主机或目标 Forge 整合包实测。
`.github/workflows/verify.yml` 在每个 Pull Request 上运行 `gradlew check`。`.github/workflows/ci.yml` 在 `main` 推送或手动触发时先执行 `gradlew check :proxy-core:distZip`,再上传 CI 工件并发布 Maven 模块。发布步骤要求 `MAVEN_USER` 与 `MAVEN_PASSWORD` secrets;本地 `check` 不能证明远端 Maven 发布成功。版本规则、七个坐标和发行 ZIP 见[发布与依赖版本](publishing.md)。
`.github/workflows/verify.yml` 在每个 Pull Request 上运行 `gradlew -Pluckperms check`,并单独构建、测试 `client-mod/`。`.github/workflows/ci.yml` 在 `main` 推送或手动触发时先执行 `gradlew -Pluckperms check release`,再上传 `build/release/` 并发布 Maven 模块。发布步骤要求 `MAVEN_USER` 与 `MAVEN_PASSWORD` secrets;本地 `check` 不能证明远端 Maven 发布成功。版本规则、Maven 坐标和发行物 见[发布与依赖版本](publishing.md)。
`check` 包含 `permissionAcceptance`:使用发行包中的实际 LuckPerms JAR、隔离插件类加载器和临时 H2 数据库,验证原生命令、授权/拒绝/未设置、组继承、临时节点到期、后端上下文、连接代次隔离及关闭后重新打开数据库。输出位于 `proxy-core/build/permission-acceptance/run-*/`,成功标记为 `PERMISSION_ACCEPTANCE_PASS`。它使用 API 玩家探针,不连接真实 Minecraft 客户端,也不证明外部 SQL/Redis 联调成功;真实会话的权限准备和释放由 TCP 生命周期测试另行覆盖。
带 `-Pluckperms` 时 `check` 包含 `permissionAcceptance`(本地默认 `check` 不编译 LuckPerms,因此不含这一项;CI 始终开启):使用发行包中的实际 LuckPerms JAR、隔离插件类加载器和临时 H2 数据库,验证原生命令、授权/拒绝/未设置、组继承、临时节点到期、后端上下文、连接代次隔离及关闭后重新打开数据库。输出位于 `proxy-core/build/permission-acceptance/run-*/`,成功标记为 `PERMISSION_ACCEPTANCE_PASS`。它使用 API 玩家探针,不连接真实 Minecraft 客户端,也不证明外部 SQL/Redis 联调成功;真实会话的权限准备和释放由 TCP 生命周期测试另行覆盖。
如果本地已有 Uranium 1.7.10 可运行包及其编译好的 `MinecraftProtocolProbe`,可运行 `smoke/local-uranium.ps1 -BundlePath <包目录> -ProbeClassesPath <探针类目录>`。脚本复制服务端到忽略目录,启动 Java 8 后端与当前安装包,再让探针经代理完成状态查询、FML 登录、Join Game 和持续 Keep Alive,结束时停止两个进程。本地一次结果与具体前提见 [最小 Uranium 联机记录](../smoke/results/2026-09-25-local-uranium.md)。初次路由的当前 `ProxyMain` 协议探针结果另见[路由验收](../smoke/results/2026-09-27-initial-routing.md)。真实客户端记录见下段和其单独的历史验收档案;它们不应与当前路由分支的完整模组包验收混为一谈。
@@ -90,7 +90,7 @@ sh smoke/container-channel.sh
## 后端宿主真实服务端验收
`smoke/backend-uranium.ps1` 将已经接受 EULA 的 Uranium 发行包中的服务端 JAR、运行库和 EULA 复制到全新隔离目录,启动两个 Java 8 后端及实际 Java 25 Proxy。它安装 `MoonBridgeBackend`、两个独立的 Bukkit 业务插件和一个外部 Proxy 验收插件,通过正式插件 API 检查注册与消息服务;不会复制或修改原有世界、模组、插件及凭据。Bukkit 业务插件只使用发行包 `backend-api/` 中的 API JAR 和服务器 Bukkit API 编译,编译路径不包含宿主及传输 SDK,并在实际插件加载器下验证服务类身份一致。
`smoke/backend-uranium.ps1` 将已经接受 EULA 的 Uranium 发行包中的服务端 JAR、运行库和 EULA 复制到全新隔离目录,启动两个 Java 8 后端及实际 Java 25 Proxy。它安装 `MoonBridgeBackend`、两个独立的 Bukkit 业务插件和一个外部 Proxy 验收插件,通过正式插件 API 检查注册与消息服务;不会复制或修改原有世界、模组、插件及凭据。Bukkit 业务插件以发行包 `backend/` 中的 JAR 和服务器 Bukkit API 编译(compileOnly,运行时依赖 `MoonBridgeBackend`),并在实际插件加载器下验证服务类身份一致。
```powershell
.\gradlew.bat check :proxy-core:installDist
Vendored Regular → Executable
View File
File mode changed.
+1 -12
View File
@@ -4,7 +4,7 @@ import org.gradle.jvm.toolchain.JavaLanguageVersion
plugins {
id("moonbridge.java-library-conventions")
id("com.gradleup.shadow") version "9.6.1"
`maven-publish`
id("moonbridge.publishing-conventions")
}
val hostBridgeCompileClasspath = configurations.create("hostBridgeCompileClasspath") {
@@ -146,15 +146,4 @@ publishing {
}
}
}
repositories {
maven {
name = "giteaPackages"
url = uri(providers.environmentVariable("MAVEN_URL")
.orElse("https://git.nest.potatolab.uk:8443/api/packages/layue13/maven").get())
credentials {
username = providers.environmentVariable("MAVEN_USER").orElse("NONE").get()
password = providers.environmentVariable("MAVEN_PASSWORD").orElse("NONE").get()
}
}
}
}
-49
View File
@@ -1,49 +0,0 @@
plugins {
`java-library`
`maven-publish`
}
java {
withSourcesJar()
withJavadocJar()
toolchain.languageVersion.set(JavaLanguageVersion.of(25))
}
tasks.withType<JavaCompile>().configureEach {
options.release.set(8)
options.encoding = "UTF-8"
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
dependencies {
testImplementation(platform("org.junit:junit-bom:5.14.1"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
publishing {
publications {
create<MavenPublication>("messagingApi") {
from(components["java"])
artifactId = "messaging-api"
pom {
name.set("MoonBridge Messaging API")
description.set("Java 8-compatible shared message envelope and channel API.")
}
}
}
repositories {
maven {
name = "giteaPackages"
url = uri(providers.environmentVariable("MAVEN_URL")
.orElse("https://git.nest.potatolab.uk:8443/api/packages/layue13/maven").get())
credentials {
username = providers.environmentVariable("MAVEN_USER").orElse("NONE").get()
password = providers.environmentVariable("MAVEN_PASSWORD").orElse("NONE").get()
}
}
}
}
-50
View File
@@ -1,50 +0,0 @@
plugins {
`java-library`
`maven-publish`
}
java {
withSourcesJar()
withJavadocJar()
toolchain.languageVersion.set(JavaLanguageVersion.of(25))
}
tasks.withType<JavaCompile>().configureEach {
options.release.set(8)
options.encoding = "UTF-8"
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
dependencies {
api(project(":messaging-api"))
testImplementation(platform("org.junit:junit-bom:5.14.1"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
publishing {
publications {
create<MavenPublication>("messagingProtocol") {
from(components["java"])
artifactId = "messaging-protocol"
pom {
name.set("MoonBridge Messaging Protocol")
description.set("Java 8-compatible message wire codec shared by proxy and backend clients.")
}
}
}
repositories {
maven {
name = "giteaPackages"
url = uri(providers.environmentVariable("MAVEN_URL")
.orElse("https://git.nest.potatolab.uk:8443/api/packages/layue13/maven").get())
credentials {
username = providers.environmentVariable("MAVEN_USER").orElse("NONE").get()
password = providers.environmentVariable("MAVEN_PASSWORD").orElse("NONE").get()
}
}
}
}
+10
View File
@@ -0,0 +1,10 @@
plugins {
id("moonbridge.java8-library-conventions")
id("moonbridge.publishing-conventions")
}
moonbridgePublication {
artifactId.set("messaging")
displayName.set("MoonBridge Messaging")
description.set("Java 8-compatible message envelope, channel API and wire codec shared by the proxy and backends.")
}
+8
View File
@@ -8,6 +8,14 @@ gradlePlugin {
id = "moonbridge.java-library-conventions"
implementationClass = "MoonBridgeJavaLibraryConventionsPlugin"
}
register("java8LibraryConventions") {
id = "moonbridge.java8-library-conventions"
implementationClass = "MoonBridgeJava8LibraryConventionsPlugin"
}
register("publishingConventions") {
id = "moonbridge.publishing-conventions"
implementationClass = "MoonBridgePublishingConventionsPlugin"
}
register("applicationConventions") {
id = "moonbridge.application-conventions"
implementationClass = "MoonBridgeApplicationConventionsPlugin"
@@ -0,0 +1,17 @@
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.kotlin.dsl.withType
/** Libraries loaded by Java 8 backends (Uranium and similar): compiled with --release 8. */
class MoonBridgeJava8LibraryConventionsPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.plugins.apply(MoonBridgeJavaLibraryConventionsPlugin::class.java)
project.tasks.withType<JavaCompile>().configureEach {
options.release.set(8)
options.encoding = "UTF-8"
// JDK 25 warns that --release 8 is obsolete; targeting Java 8 is the point of this convention.
options.compilerArgs.add("-Xlint:-options")
}
}
}
@@ -0,0 +1,56 @@
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.provider.Property
import org.gradle.api.publish.PublishingExtension
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.publish.maven.plugins.MavenPublishPlugin
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.create
import org.gradle.kotlin.dsl.findByType
/** Name and description of a module's standard Maven publication (its jar, sources and javadoc). */
abstract class MoonBridgePublicationExtension {
abstract val artifactId: Property<String>
abstract val displayName: Property<String>
abstract val description: Property<String>
}
/**
* Adds the shared package repository. A module that sets `moonbridgePublication.artifactId` also gets the
* standard publication; a module with an unusual artifact (the LuckPerms plugin) declares its own instead.
*/
class MoonBridgePublishingConventionsPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.plugins.apply(MavenPublishPlugin::class.java)
val publication = project.extensions.create<MoonBridgePublicationExtension>("moonbridgePublication")
project.extensions.configure<PublishingExtension> {
repositories.maven {
name = "giteaPackages"
url = project.uri(project.providers.environmentVariable("MAVEN_URL")
.orElse("https://git.nest.potatolab.uk:8443/api/packages/layue13/maven").get())
credentials {
username = project.providers.environmentVariable("MAVEN_USER").orElse("NONE").get()
password = project.providers.environmentVariable("MAVEN_PASSWORD").orElse("NONE").get()
}
}
}
project.afterEvaluate {
if (!publication.artifactId.isPresent) return@afterEvaluate
project.extensions.findByType<JavaPluginExtension>()?.apply {
withSourcesJar()
withJavadocJar()
}
project.extensions.configure<PublishingExtension> {
publications.create<MavenPublication>("maven") {
from(project.components.getByName("java"))
artifactId = publication.artifactId.get()
pom {
name.set(publication.displayName)
description.set(publication.description)
}
}
}
}
}
}
+29 -35
View File
@@ -7,7 +7,7 @@ plugins {
val runtimeJavaHome = System.getProperty("java.home")
dependencies {
implementation(project(":messaging-protocol"))
implementation(project(":messaging"))
// The plugin contract is the only stable external boundary. Everything else
// in this project remains an implementation package of the proxy core.
api(project(":proxy-plugin-api"))
@@ -32,7 +32,7 @@ dependencies {
implementation(libs.findLibrary("adventure-gson").get())
implementation(libs.findLibrary("adventure-plain").get())
runtimeOnly(libs.findLibrary("logback-classic").get())
testImplementation(project(":backend-channel-client"))
testImplementation(project(":backend"))
testImplementation(libs.findLibrary("logback-classic").get())
testImplementation(libs.findLibrary("netty-pkitesting").get())
}
@@ -86,13 +86,6 @@ val installedDistVersionSmokeTest = tasks.register<Exec>("installedDistVersionSm
}
}
val compilePermissionAcceptance = tasks.register<JavaCompile>("compilePermissionAcceptance") {
source(rootProject.file("smoke/PermissionAcceptance.java"))
classpath = sourceSets.main.get().runtimeClasspath
destinationDirectory.set(layout.buildDirectory.dir("classes/permissionAcceptance"))
options.release.set(25)
}
// The benchmarks in benchmarks/ declare proxy-core packages to reach package-private classes and are
// normally compiled by their PowerShell runners. Compiling them here keeps them building with main.
val compileBenchmarks = tasks.register<JavaCompile>("compileBenchmarks") {
@@ -104,21 +97,33 @@ val compileBenchmarks = tasks.register<JavaCompile>("compileBenchmarks") {
options.release.set(25)
}
val permissionAcceptance = tasks.register<JavaExec>("permissionAcceptance") {
group = "verification"
description = "Loads the packaged LuckPerms plugin and verifies native permissions, commands and H2 persistence."
dependsOn(tasks.named("installDist"), compilePermissionAcceptance)
classpath = sourceSets.main.get().runtimeClasspath + files(compilePermissionAcceptance.flatMap { it.destinationDirectory })
mainClass.set("PermissionAcceptance")
args(layout.buildDirectory.dir("install/moonbridge").get().asFile.absolutePath,
layout.buildDirectory.dir("permission-acceptance").get().asFile.absolutePath)
// permissionAcceptance needs the LuckPerms plugin, which is only built with -Pluckperms.
val luckPerms = rootProject.findProject(":luckperms-moonbridge")
if (luckPerms != null) {
val compilePermissionAcceptance = tasks.register<JavaCompile>("compilePermissionAcceptance") {
source(rootProject.file("smoke/PermissionAcceptance.java"))
classpath = sourceSets.main.get().runtimeClasspath
destinationDirectory.set(layout.buildDirectory.dir("classes/permissionAcceptance"))
options.release.set(25)
}
val permissionAcceptance = tasks.register<JavaExec>("permissionAcceptance") {
group = "verification"
description = "Loads the packaged LuckPerms plugin and verifies native permissions, commands and H2 persistence."
dependsOn(tasks.named("installDist"), compilePermissionAcceptance)
classpath = sourceSets.main.get().runtimeClasspath + files(compilePermissionAcceptance.flatMap { it.destinationDirectory })
mainClass.set("PermissionAcceptance")
args(layout.buildDirectory.dir("install/moonbridge").get().asFile.absolutePath,
layout.buildDirectory.dir("permission-acceptance").get().asFile.absolutePath)
}
tasks.named("check") { dependsOn(permissionAcceptance) }
}
tasks.named("check") {
dependsOn(installedDistSmokeTest)
dependsOn(installedDistHelpSmokeTest)
dependsOn(installedDistVersionSmokeTest)
dependsOn(permissionAcceptance)
dependsOn(compileBenchmarks)
}
@@ -147,8 +152,10 @@ distributions {
main {
contents {
from(rootProject.file("README.md"))
from(project(":luckperms-moonbridge").tasks.named("pluginJar")) {
into("plugins")
if (luckPerms != null) {
from(luckPerms.tasks.named("pluginJar")) {
into("plugins")
}
}
from(rootProject.file("docs")) {
into("docs")
@@ -162,21 +169,8 @@ distributions {
from("src/main/resources/config") {
into("config")
}
from(project(":backend-channel-client").tasks.named("jar")) {
into("backend-client")
}
from(project(":messaging-api").tasks.named("jar")) {
into("backend-client")
}
from(project(":messaging-protocol").tasks.named("jar")) {
into("backend-client")
}
from(project(":backend-bukkit").tasks.named("jar")) {
into("backend-host")
}
from(listOf(project(":backend-bukkit-api").tasks.named("jar"),
project(":messaging-api").tasks.named("jar"))) {
into("backend-api")
from(project(":backend").tasks.named("jar")) {
into("backend")
}
}
}
@@ -1,6 +1,6 @@
package dev.moonbridge.app;
import dev.moonbridge.core.net.NetworkTransport;
import dev.moonbridge.core.session.channel.NetworkTransport;
import dev.moonbridge.core.backend.InMemoryBackendCatalog;
import dev.moonbridge.core.control.BackendControlService;
import dev.moonbridge.core.auth.MojangSessionVerifier;
@@ -1,11 +1,9 @@
package dev.moonbridge.core.forwarding;
package dev.moonbridge.core.auth;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import dev.moonbridge.core.auth.ProfileProperty;
import dev.moonbridge.core.auth.VerifiedProfile;
import dev.moonbridge.core.protocol.MinecraftHandshake;
import dev.moonbridge.core.protocol.ProtocolProfile;
import io.netty.buffer.ByteBuf;
@@ -1,4 +1,4 @@
package dev.moonbridge.core.permission;
package dev.moonbridge.core.event;
import dev.moonbridge.api.PlayerIdentity;
import dev.moonbridge.api.PlayerView;
@@ -13,7 +13,7 @@ import dev.moonbridge.api.Players;
import dev.moonbridge.api.Plugin;
import dev.moonbridge.api.permission.PermissionResult;
import net.kyori.adventure.text.Component;
import dev.moonbridge.core.permission.PermissionService;
import dev.moonbridge.core.event.PermissionService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -3,7 +3,7 @@ package dev.moonbridge.core.plugin;
import dev.moonbridge.messaging.internal.LocalMessaging;
import dev.moonbridge.api.Players;
import dev.moonbridge.core.backend.BackendCatalog;
import dev.moonbridge.core.permission.PermissionService;
import dev.moonbridge.core.event.PermissionService;
import java.util.UUID;
@@ -14,7 +14,7 @@ import dev.moonbridge.core.backend.BackendCatalog;
import dev.moonbridge.core.backend.BackendOwner;
import dev.moonbridge.core.control.BackendChannelTransport;
import dev.moonbridge.core.event.EventDispatcher;
import dev.moonbridge.core.permission.PermissionService;
import dev.moonbridge.core.event.PermissionService;
import dev.moonbridge.core.event.TransferPreparation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -3,7 +3,7 @@ package dev.moonbridge.core.session;
import dev.moonbridge.core.session.channel.SessionChannels;
import dev.moonbridge.api.PlayerIdentity;
import dev.moonbridge.core.auth.VerifiedProfile;
import dev.moonbridge.core.forwarding.BungeeLegacyForwarding;
import dev.moonbridge.core.auth.BungeeLegacyForwarding;
import dev.moonbridge.core.protocol.MinecraftHandshake;
import dev.moonbridge.core.protocol.ProtocolProfile;
import dev.moonbridge.messaging.session.ForwardedSessionProof;
@@ -5,7 +5,7 @@ import dev.moonbridge.api.MessageResult;
import dev.moonbridge.core.protocol.Minecraft1710PlayPackets;
import dev.moonbridge.core.protocol.MinecraftLoginDisconnect;
import dev.moonbridge.core.protocol.MinecraftText;
import dev.moonbridge.core.relay.RawRelay;
import dev.moonbridge.core.session.play.RawRelay;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import net.kyori.adventure.text.Component;
@@ -10,7 +10,7 @@ import dev.moonbridge.api.event.ConnectionAdmissionEvent;
import dev.moonbridge.api.event.PlayerDisconnectedEvent;
import dev.moonbridge.api.event.ServerConnectedEvent;
import dev.moonbridge.core.event.EventDispatcher;
import dev.moonbridge.core.permission.PermissionService;
import dev.moonbridge.core.event.PermissionService;
import dev.moonbridge.api.PlayerIdentity;
import dev.moonbridge.api.PlayerView;
import dev.moonbridge.api.MessageResult;
@@ -21,7 +21,7 @@ import dev.moonbridge.api.TransferStatus;
import dev.moonbridge.core.auth.MinecraftEncryptionRequest;
import dev.moonbridge.core.auth.SessionVerifier;
import dev.moonbridge.core.backend.BackendCatalog;
import dev.moonbridge.core.net.NetworkTransport;
import dev.moonbridge.core.session.channel.NetworkTransport;
import dev.moonbridge.core.protocol.ServerListStatus;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
@@ -25,10 +25,10 @@ import dev.moonbridge.core.protocol.LoginStart;
import dev.moonbridge.core.protocol.MinecraftLoginSuccess;
import dev.moonbridge.core.protocol.ProtocolProfile;
import dev.moonbridge.core.protocol.ProtocolVarInt;
import dev.moonbridge.core.relay.RawRelay;
import dev.moonbridge.core.session.play.RawRelay;
import dev.moonbridge.core.backend.BackendCatalog;
import dev.moonbridge.core.event.TransferPreparation;
import dev.moonbridge.core.net.NetworkTransport;
import dev.moonbridge.core.session.channel.NetworkTransport;
import io.netty.resolver.AddressResolverGroup;
import java.util.function.BiConsumer;
import io.netty.buffer.ByteBuf;
@@ -1,4 +1,4 @@
package dev.moonbridge.core.net;
package dev.moonbridge.core.session.channel;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.IoEventLoop;
@@ -1,6 +1,5 @@
package dev.moonbridge.core.session.channel;
import dev.moonbridge.core.net.NetworkTransport;
import dev.moonbridge.core.protocol.MinecraftFrameDecoder;
import dev.moonbridge.core.protocol.ProtocolProfile;
import io.netty.bootstrap.Bootstrap;
@@ -1,6 +1,5 @@
package dev.moonbridge.core.session.play;
import dev.moonbridge.core.relay.RawRelay;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
@@ -1,4 +1,4 @@
package dev.moonbridge.core.relay;
package dev.moonbridge.core.session.play;
import dev.moonbridge.core.protocol.ProtocolProfile;
import io.netty.buffer.ByteBuf;
@@ -1,7 +1,6 @@
package dev.moonbridge.core.session.play;
import dev.moonbridge.core.protocol.MinecraftTabCompletion;
import dev.moonbridge.core.relay.RawRelay;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
@@ -4,7 +4,7 @@ import dev.moonbridge.api.TransferResult;
import dev.moonbridge.api.event.TransferContext;
import dev.moonbridge.core.backend.BackendView;
import dev.moonbridge.core.event.TransferPreparation;
import dev.moonbridge.core.relay.RawRelay;
import dev.moonbridge.core.session.play.RawRelay;
import io.netty.channel.Channel;
import java.util.concurrent.CompletableFuture;
@@ -12,7 +12,7 @@ import dev.moonbridge.core.protocol.ByteBufs;
import dev.moonbridge.core.protocol.LoginStart;
import dev.moonbridge.core.protocol.ProtocolProfile;
import dev.moonbridge.core.protocol.Minecraft1710PlayPackets;
import dev.moonbridge.core.relay.RawRelay;
import dev.moonbridge.core.session.play.RawRelay;
import dev.moonbridge.core.session.channel.SessionChannels;
import dev.moonbridge.core.session.play.KeepAliveBridge;
import dev.moonbridge.core.session.play.PlayObservation;
@@ -7,7 +7,7 @@ import dev.moonbridge.core.protocol.Minecraft1710EntityIds;
import dev.moonbridge.core.protocol.Minecraft1710PlayPackets;
import dev.moonbridge.core.protocol.ProtocolProfile;
import dev.moonbridge.core.protocol.ProtocolVarInt;
import dev.moonbridge.core.relay.RawRelay;
import dev.moonbridge.core.session.play.RawRelay;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
@@ -5,8 +5,8 @@ import dev.moonbridge.api.PlayerView;
import dev.moonbridge.core.backend.BackendCatalog;
import dev.moonbridge.core.backend.BackendView;
import dev.moonbridge.core.event.TransferPreparation;
import dev.moonbridge.core.net.NetworkTransport;
import dev.moonbridge.core.relay.RawRelay;
import dev.moonbridge.core.session.channel.NetworkTransport;
import dev.moonbridge.core.session.play.RawRelay;
import dev.moonbridge.core.session.play.KeepAliveBridge;
import dev.moonbridge.core.session.play.PlayObservation;
import io.netty.buffer.ByteBuf;
@@ -1,8 +1,6 @@
package dev.moonbridge.core.forwarding;
package dev.moonbridge.core.auth;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.moonbridge.core.auth.ProfileProperty;
import dev.moonbridge.core.auth.VerifiedProfile;
import dev.moonbridge.core.protocol.MinecraftHandshake;
import dev.moonbridge.core.protocol.ProtocolProfile;
import io.netty.buffer.ByteBuf;
@@ -1,4 +1,4 @@
package dev.moonbridge.core.permission;
package dev.moonbridge.core.event;
import dev.moonbridge.api.PlayerIdentity;
import dev.moonbridge.api.PlayerView;
@@ -14,7 +14,7 @@ import dev.moonbridge.core.backend.BackendOwner;
import dev.moonbridge.core.backend.BackendRegistration;
import dev.moonbridge.core.backend.InMemoryBackendCatalog;
import dev.moonbridge.core.event.EventDispatcher;
import dev.moonbridge.core.permission.PermissionService;
import dev.moonbridge.core.event.PermissionService;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
@@ -2384,7 +2384,7 @@ final class ProxySessionListenerTest {
backendDone.get(5, TimeUnit.SECONDS);
var relayField = Session.class.getDeclaredField("relay");
relayField.setAccessible(true);
var relay = (dev.moonbridge.core.relay.RawRelay.Link) relayField.get(session);
var relay = (dev.moonbridge.core.session.play.RawRelay.Link) relayField.get(session);
assertTrue(relay.hasPendingClientboundWrites());
backendChannel.eventLoop().submit(() -> backendChannel.pipeline()
.fireExceptionCaught(new IOException("backend read failed"))).get(5, TimeUnit.SECONDS);
@@ -1,4 +1,4 @@
package dev.moonbridge.core.net;
package dev.moonbridge.core.session.channel;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
@@ -1,6 +1,5 @@
package dev.moonbridge.core.session.play;
import dev.moonbridge.core.relay.RawRelay;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
@@ -1,4 +1,4 @@
package dev.moonbridge.core.relay;
package dev.moonbridge.core.session.play;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
@@ -1,4 +1,4 @@
package dev.moonbridge.core.relay;
package dev.moonbridge.core.session.play;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
@@ -4,7 +4,7 @@ import dev.moonbridge.core.session.play.PlayObservation;
import dev.moonbridge.core.protocol.Minecraft1710PlayPackets;
import dev.moonbridge.core.protocol.ProtocolVarInt;
import dev.moonbridge.core.relay.RawRelay;
import dev.moonbridge.core.session.play.RawRelay;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
+6 -30
View File
@@ -1,41 +1,17 @@
plugins {
id("moonbridge.java-library-conventions")
`maven-publish`
id("moonbridge.publishing-conventions")
}
dependencies {
api(project(":messaging-api"))
api(project(":messaging"))
api(libs.findLibrary("slf4j-api").get())
api(libs.findLibrary("adventure-api").get())
api(libs.findLibrary("adventure-minimessage").get())
}
java {
withSourcesJar()
withJavadocJar()
}
publishing {
publications {
create<MavenPublication>("pluginApi") {
from(components["java"])
artifactId = "proxy-plugin-api"
pom {
name.set("MoonBridge Plugin API")
description.set("Stable API for MoonBridge proxy-side plugins.")
}
}
}
repositories {
maven {
name = "giteaPackages"
url = uri(providers.environmentVariable("MAVEN_URL")
.orElse("https://git.nest.potatolab.uk:8443/api/packages/layue13/maven")
.get())
credentials {
username = providers.environmentVariable("MAVEN_USER").orElse("NONE").get()
password = providers.environmentVariable("MAVEN_PASSWORD").orElse("NONE").get()
}
}
}
moonbridgePublication {
artifactId.set("proxy-plugin-api")
displayName.set("MoonBridge Plugin API")
description.set("Stable API for MoonBridge proxy-side plugins.")
}
+12 -8
View File
@@ -27,15 +27,19 @@ dependencyResolutionManagement {
rootProject.name = "moonbridge"
includeBuild("luckperms-moonbridge/upstream-build")
include(
"proxy-core",
"proxy-plugin-api",
"backend-channel-client",
"messaging-api",
"messaging-protocol",
"backend-bukkit",
"backend-bukkit-api",
"luckperms-moonbridge",
"messaging",
"backend",
)
// The LuckPerms platform plugin compiles LuckPerms itself from the vendor/luckperms submodule, which is
// slow and pulls a large dependency tree, so it is opt-in: pass -Pluckperms (CI and release builds do).
if (providers.gradleProperty("luckperms").map { it != "false" }.getOrElse(false)) {
check(file("vendor/luckperms/build.gradle").isFile) {
"-Pluckperms needs the LuckPerms sources: run git submodule update --init --recursive"
}
includeBuild("luckperms-moonbridge/upstream-build")
include("luckperms-moonbridge")
}
+4 -4
View File
@@ -16,8 +16,8 @@ if ($serverJars.Count -ne 1) { throw 'Expected exactly one Uranium server JAR.'
$serverJar = $serverJars[0]
$serverHash = (Get-FileHash -LiteralPath $serverJar.FullName -Algorithm SHA256).Hash
$dist = Join-Path $repo 'proxy-core/build/install/moonbridge'
$hostJars = @(Get-ChildItem (Join-Path $dist 'backend-host') -Filter 'moonbridge-backend-bukkit-*.jar')
if ($hostJars.Count -ne 1) { throw 'Build the renamed host and installDist first.' }
$hostJars = @(Get-ChildItem (Join-Path $dist 'backend') -Filter 'moonbridge-backend-*.jar')
if ($hostJars.Count -ne 1) { throw 'Build the backend jar and installDist first.' }
$run = Join-Path $repo ('build/backend-uranium-' + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Force $run | Out-Null
Write-Output "ACCEPTANCE_DIRECTORY=$run"
@@ -43,8 +43,8 @@ $probeClasses = Join-Path $run 'probe-classes'
$proxyClasses = Join-Path $run 'proxy-classes'
New-Item -ItemType Directory -Force $controlDir, $proxyPlugins, $probeClasses, $proxyClasses | Out-Null
$proxyClasspath = Join-Path $dist 'lib/*'
# Business plugins compile against public API artifacts only, never the host/transport.
$backendApiClasspath = Join-Path $dist 'backend-api/*'
# Business plugins compile against the backend jar (compileOnly) and depend on MoonBridgeBackend at runtime.
$backendApiClasspath = Join-Path $dist 'backend/*'
& (Join-Path $Java8Home 'bin/javac.exe') -cp "$backendApiClasspath;$BukkitApiJar" -d $probeClasses `
(Join-Path $PSScriptRoot 'BackendAcceptancePlugin.java') (Join-Path $PSScriptRoot 'BackendObserverPlugin.java')
if ($LASTEXITCODE -ne 0) { throw 'Bukkit probe compilation failed.' }
+1 -1
View File
@@ -7,7 +7,7 @@ $repo = Split-Path -Parent $PSScriptRoot
$fixtureClasses = Join-Path $repo 'build/channel-smoke/router'
$probeClasses = Join-Path $repo 'build/channel-smoke/probe'
$proxyClasspath = Join-Path $repo 'proxy-core/build/install/moonbridge/lib/*'
$backendClasspath = Join-Path $repo 'proxy-core/build/install/moonbridge/backend-client/*'
$backendClasspath = Join-Path $repo 'proxy-core/build/install/moonbridge/backend/*'
if (-not (Test-Path (Join-Path $repo 'proxy-core/build/install/moonbridge/lib'))) {
throw 'Run gradlew :proxy-core:installDist first.'
}