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>
This commit is contained in:
layue13andClaude Opus 5.5 committed 2026-10-22 14:34:01 +08:00
1 parent 7664c1cf60
commit 789648c82f
369 files changed
+188 -160

No files matched your search

+2 -2
View File
@@ -1,3 +1,3 @@
[submodule "vendor/luckperms"]
path = vendor/luckperms
[submodule "proxy/vendor/luckperms"]
path = proxy/vendor/luckperms
url = https://github.com/LuckPerms/LuckPerms.git
+15 -5
View File
@@ -8,7 +8,7 @@ MoonBridge is a session proxy for Minecraft 1.7.10 Forge (protocol 5): login/aut
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.
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.
```sh
./gradlew check # tests, installed-dist smoke tests, benchmark compile
@@ -17,7 +17,7 @@ The LuckPerms platform plugin compiles LuckPerms from the `vendor/luckperms` sub
./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
./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.
@@ -26,14 +26,24 @@ The slow tests are the session integration tests (`ProxySessionListenerTest`, `S
## 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 `vendor/luckperms` via the included build `luckperms-moonbridge/upstream-build`; only included with `-Pluckperms`.
- `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.
- `proxy-build-logic` — Kotlin convention plugins: `java-library`, `java8-library`, `application`, and `publishing-conventions` (shared package repository; set `moonbridgePublication { artifactId ... }` for a standard publication).
- `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
@@ -58,4 +68,4 @@ The slow tests are the session integration tests (`ProxySessionListenerTest`, `S
## Performance work
Benchmarks live in `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 `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.
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.
+4 -4
View File
@@ -19,14 +19,14 @@
```sh
./gradlew check :proxy-core:installDist # Windows: .\gradlew.bat check :proxy-core:installDist
cd proxy-core/build/install/moonbridge
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`。
需要全部部署物(服务器包、后端插件、客户端 mod)时运行 `./gradlew release`,说明见[发行物与安装位置](docs/release-layout.md)。LuckPerms 平台插件从 `proxy/vendor/luckperms` 子模块编译,默认不构建;需要时先 `git submodule update --init --recursive`,再加 `-Pluckperms`。
默认配置为 `OFFLINE`,仅监听 `127.0.0.1:25577`。公开部署使用 `ONLINE_BUNGEE` 并限制玩家直连后端;认证、监听、入口候选和后端地址见[运行配置](docs/operations.md)。发行包根目录附带 `docs/` 与按日期归档的 `smoke/results/`、`benchmarks/results/`。
默认配置为 `OFFLINE`,仅监听 `127.0.0.1:25577`。公开部署使用 `ONLINE_BUNGEE` 并限制玩家直连后端;认证、监听、入口候选和后端地址见[运行配置](docs/operations.md)。
## 后端插件开发
@@ -36,6 +36,6 @@ bin/moonbridge --config config/moonbridge.yml
## 验证状态
当前可引用的验收记录及其覆盖范围见[文档索引](docs/README.md#当前可引用的验证记录)和[验证方法](docs/testing.md)。各记录只证明其注明的提交和环境,不代表生产容量。已知 Uranium 候选服登录停滞见[记录](smoke/results/2026-09-26-uranium-login-stalls.md)及 [Uranium #585](https://git.nest.potatolab.uk:8443/TDLM/Uranium/issues/585)。
当前可引用的验收记录及其覆盖范围见[文档索引](docs/README.md#当前可引用的验证记录)和[验证方法](docs/testing.md)。各记录只证明其注明的提交和环境,不代表生产容量。已知 Uranium 候选服登录停滞见[记录](testing/smoke/results/2026-09-26-uranium-login-stalls.md)及 [Uranium #585](https://git.nest.potatolab.uk:8443/TDLM/Uranium/issues/585)。
生产权限存储统一使用 MariaDB 13.0.2;见 [MariaDB 权限数据库](docs/mariadb.md)。
File renamed without changes.
@@ -5,4 +5,4 @@ dependencyResolutionManagement {
}
}
rootProject.name = "proxy-build-logic"
rootProject.name = "build-logic"
+4 -4
View File
@@ -6,7 +6,7 @@
- 要运行 Proxy:先看[运行配置](operations.md),再按部署需求修改认证方式、入口候选和后端地址。
- 要开发插件:先看[插件开发](plugins.md);后端 Bukkit 插件另看[后端 Channel 接入](backend-channel-design.md#后端插件接入)。
- 要理解初次登录为什么拒绝或如何重试:看[初次登录路由](initial-routing.md)及其[专项验收记录](../smoke/results/2026-09-27-initial-routing.md)。
- 要理解初次登录为什么拒绝或如何重试:看[初次登录路由](initial-routing.md)及其[专项验收记录](../testing/smoke/results/2026-09-27-initial-routing.md)。
- 要核对证据覆盖面:看[验证方法](testing.md),并按各记录注明的日期、提交、环境和探针类型判断。
| 文档 | 内容 |
@@ -28,8 +28,8 @@
## 当前可引用的验证记录
- [初次路由专项验收](../smoke/results/2026-09-27-initial-routing.md):自动化路由覆盖与真实 Java 8 Uranium 后端协议探针;不是完整 Prism 模组包视觉验收。
- [MoonBridge 后端 API 验收](../smoke/results/2026-09-27-moonbridge-api.md):隔离 Uranium 服务端中的 Bukkit 宿主和 API 功能验收;无玩家。
- [2026-09-26 Prism 历史记录](../benchmarks/results/2026-09-26-prism-pack.md):真实客户端及配对测量,仅适用于该记录描述的代码版本和环境。
- [初次路由专项验收](../testing/smoke/results/2026-09-27-initial-routing.md):自动化路由覆盖与真实 Java 8 Uranium 后端协议探针;不是完整 Prism 模组包视觉验收。
- [MoonBridge 后端 API 验收](../testing/smoke/results/2026-09-27-moonbridge-api.md):隔离 Uranium 服务端中的 Bukkit 宿主和 API 功能验收;无玩家。
- [2026-09-26 Prism 历史记录](../testing/benchmarks/results/2026-09-26-prism-pack.md):真实客户端及配对测量,仅适用于该记录描述的代码版本和环境。
设计约束:核心管理连接和协议;插件负责玩法与访问策略。`ServerView` 是后端目录快照,发现方式是目录的提供者,不引入核心负载观测。准入与会话通知使用同一事件类型、监听器和注册表;不同事件在派发策略上处理结果。常规 PLAY 转发没有通用事件派发。
+20 -7
View File
@@ -4,12 +4,25 @@
## 模块
仓库按用途分目录,Gradle 项目名保持不变(任务路径与 Maven 坐标照旧):
| 目录 | Gradle 项目 | 内容 |
| --- | --- | --- |
| `proxy/core` | `proxy-core` | 代理运行时 |
| `proxy/plugin-api` | `proxy-plugin-api` | 代理插件 API |
| `proxy/luckperms`、`proxy/vendor/luckperms` | `luckperms-moonbridge` | LuckPerms 平台插件及其上游子模块(`-Pluckperms`) |
| `messaging` | `messaging` | 代理与后端共用的消息 API 与线协议 |
| `backend` | `backend` | Bukkit/Uranium 后端宿主 |
| `client-mod` | 独立构建 | 1.7.10 WebSocket 客户端 mod |
| `build-logic` | 包含构建 | Gradle 约定插件 |
| `testing/smoke`、`testing/benchmarks` | — | 烟测脚本、基准与按日期归档的记录;不进入发行物 |
- `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) 追踪。
入口可从 [`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) 追踪。
## 会话与连接生命周期
@@ -31,7 +44,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/session/play/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)。
## 后端目录与发现所有权
@@ -43,7 +56,7 @@ flowchart LR
## 转服、并发与错误边界
转服先在旧后端保持服务的同时连接并登录候选后端。普通后端在 Join Game 后可进入切换;Forge 候选后端要等待客户端 Forge 握手链路切换及 Join Game,再进行世界切换。交接期间客户端与旧后端帧分别进入有上限的缓冲区;切换前失败时尽可能恢复旧链路并回放暂存帧,成功时改用候选链路并清理旧链路。候选连接失败不会立即破坏旧链路;Forge 客户端切换包写出后若目标拒绝或超时,旧链路无法安全恢复,代理会关闭会话。`NETWORK_READY` 只表示协议握手和代理发出的世界切换已完成,不表示目标服插件或玩法世界已准备好。接口定义见 [`Players`](../proxy-plugin-api/src/main/java/dev/moonbridge/api/Players.java)、状态见 [`TransferStatus`](../proxy-plugin-api/src/main/java/dev/moonbridge/api/TransferStatus.java)。
转服先在旧后端保持服务的同时连接并登录候选后端。普通后端在 Join Game 后可进入切换;Forge 候选后端要等待客户端 Forge 握手链路切换及 Join Game,再进行世界切换。交接期间客户端与旧后端帧分别进入有上限的缓冲区;切换前失败时尽可能恢复旧链路并回放暂存帧,成功时改用候选链路并清理旧链路。候选连接失败不会立即破坏旧链路;Forge 客户端切换包写出后若目标拒绝或超时,旧链路无法安全恢复,代理会关闭会话。`NETWORK_READY` 只表示协议握手和代理发出的世界切换已完成,不表示目标服插件或玩法世界已准备好。接口定义见 [`Players`](../proxy/plugin-api/src/main/java/dev/moonbridge/api/Players.java)、状态见 [`TransferStatus`](../proxy/plugin-api/src/main/java/dev/moonbridge/api/TransferStatus.java)。
主要并发和失败界限:
@@ -80,7 +93,7 @@ flowchart LR
## 相关源码
- 插件 API:[`PluginContext`](../proxy-plugin-api/src/main/java/dev/moonbridge/api/PluginContext.java)、[`Servers`](../proxy-plugin-api/src/main/java/dev/moonbridge/api/Servers.java)、[`Players`](../proxy-plugin-api/src/main/java/dev/moonbridge/api/Players.java)
- 会话和转服:[`Session`](../proxy-core/src/main/java/dev/moonbridge/core/session/Session.java)、[`TransferCandidate`](../proxy-core/src/main/java/dev/moonbridge/core/session/transfer/TransferCandidate.java)、[`TransferFrameBuffer`](../proxy-core/src/main/java/dev/moonbridge/core/session/transfer/TransferFrameBuffer.java)
- 目录:[`BackendCatalog`](../proxy-core/src/main/java/dev/moonbridge/core/backend/BackendCatalog.java)、[`InMemoryBackendCatalog`](../proxy-core/src/main/java/dev/moonbridge/core/backend/InMemoryBackendCatalog.java)
- 后端控制通道:[`BackendControlService`](../proxy-core/src/main/java/dev/moonbridge/core/control/BackendControlService.java)
- 插件 API:[`PluginContext`](../proxy/plugin-api/src/main/java/dev/moonbridge/api/PluginContext.java)、[`Servers`](../proxy/plugin-api/src/main/java/dev/moonbridge/api/Servers.java)、[`Players`](../proxy/plugin-api/src/main/java/dev/moonbridge/api/Players.java)
- 会话和转服:[`Session`](../proxy/core/src/main/java/dev/moonbridge/core/session/Session.java)、[`TransferCandidate`](../proxy/core/src/main/java/dev/moonbridge/core/session/transfer/TransferCandidate.java)、[`TransferFrameBuffer`](../proxy/core/src/main/java/dev/moonbridge/core/session/transfer/TransferFrameBuffer.java)
- 目录:[`BackendCatalog`](../proxy/core/src/main/java/dev/moonbridge/core/backend/BackendCatalog.java)、[`InMemoryBackendCatalog`](../proxy/core/src/main/java/dev/moonbridge/core/backend/InMemoryBackendCatalog.java)
- 后端控制通道:[`BackendControlService`](../proxy/core/src/main/java/dev/moonbridge/core/control/BackendControlService.java)
+3 -3
View File
@@ -25,7 +25,7 @@ flowchart LR
## 后端插件接入
在 Bukkit/Uranium 后端安装一次 `backend` 模块生成的 `moonbridge-backend` JAR(发行包 `backend/` 目录),插件名为 `MoonBridgeBackend`。配置唯一的实例 ID、后端名称、代理控制地址、游戏地址与凭据。宿主异步建立连接,不在启动主线程等待网络。
在 Bukkit/Uranium 后端安装一次 `backend` 模块生成的 `moonbridge-backend` JAR(`gradlew release` 输出的 `backend/` 目录),插件名为 `MoonBridgeBackend`。配置唯一的实例 ID、后端名称、代理控制地址、游戏地址与凭据。宿主异步建立连接,不在启动主线程等待网络。
消费者 `plugin.yml` 声明:
@@ -100,7 +100,7 @@ Proxy 为每个插件管理独立的 scope,在停用时自动撤销。项目
## 配置与部署
代理在 `backendChannel.listen` 启动独立监听器。配置示例见 [moonbridge-channel.example.yml](../proxy-core/src/main/resources/config/moonbridge-channel.example.yml)。每个实例的 `secret` 至少 32 个 UTF-8 字节,`allowedHosts` 限制其广告游戏地址;`allowedNamespaces` 控制发送,`allowedReceiveNamespaces` 控制接收,未配置接收列表时继承发送列表,显式空列表表示禁止该方向的所有消息。跨后端路由同时检查源发送权限与目的接收权限。Channel registration v3 将代理启动 epoch 和 backend registration epoch 同时交给后端。代理用该 backend client secret 为发往该 backendName 的 legacy forwarding handshake 生成短时 HMAC 会话证明;证明绑定两端当前 epoch、backendName、UUID、connectionId、一次性 nonce 和到期时间。后端 `MoonBridgeBackend` 校验它并把会话绑定到实际 Bukkit Player 登录对象。两端身份或 secret 不匹配、registration 未完成或 epoch 已变化时,`BukkitSessionService.find(player)` 返回空值。证明不代表任何 AetherShard 操作。
代理在 `backendChannel.listen` 启动独立监听器。配置示例见 [moonbridge-channel.example.yml](../proxy/core/src/main/resources/config/moonbridge-channel.example.yml)。每个实例的 `secret` 至少 32 个 UTF-8 字节,`allowedHosts` 限制其广告游戏地址;`allowedNamespaces` 控制发送,`allowedReceiveNamespaces` 控制接收,未配置接收列表时继承发送列表,显式空列表表示禁止该方向的所有消息。跨后端路由同时检查源发送权限与目的接收权限。Channel registration v3 将代理启动 epoch 和 backend registration epoch 同时交给后端。代理用该 backend client secret 为发往该 backendName 的 legacy forwarding handshake 生成短时 HMAC 会话证明;证明绑定两端当前 epoch、backendName、UUID、connectionId、一次性 nonce 和到期时间。后端 `MoonBridgeBackend` 校验它并把会话绑定到实际 Bukkit Player 登录对象。两端身份或 secret 不匹配、registration 未完成或 epoch 已变化时,`BukkitSessionService.find(player)` 返回空值。证明不代表任何 AetherShard 操作。
在 `OFFLINE` 代理模式中,只有显式配置了该 backend 的 session-binding secret 时,代理才会在初始连接或转服时转发由代理派生的 offline UUID 和会话证明;这证明代理连接身份,不表示 Mojang 已认证。没有配置 secret 的 offline backend 仍接收原始握手。静态 backend 或尚未获得正数 registration epoch 的目标不会收到会话证明。
@@ -114,7 +114,7 @@ HMAC 挑战认证本身不加密链路;跨机器使用私有加密链路或可
自动化验证针对 Java 8 API/宿主字节码、宿主 JAR 打包边界、真实 TCP 的零玩家双向及跨后端请求、发布、多插件关闭、并发回复关联、超时与异常帧。构建和独立 TCP 测试不能代替真实 Bukkit/Uranium 装载、玩家整合包验收或生产容量测试。
本次构建、244 项回归和实际 Java 8 进程的结果见 [2026-09-27 验证记录](../smoke/results/2026-09-27-channel-messaging.md)。
本次构建、244 项回归和实际 Java 8 进程的结果见 [2026-09-27 验证记录](../testing/smoke/results/2026-09-27-channel-messaging.md)。
安装发行包的 `backend/` 提供共享宿主 JAR;它同时包含非 Bukkit 宿主所需的控制通道客户端、消息 API 和协议。运行 `:proxy-core:installDist` 后,可用实际 JDK 8 运行独立进程验收:
+1 -1
View File
@@ -84,4 +84,4 @@ initialRouting:
这些是验收要求而不是本文件对运行时通过状态的声明。自动化测试、隔离协议烟测、真实后端登录与完整客户端/Forge 登录属于不同证据层级;各自结果必须单独记录。
本次专项测试与真实 Uranium 协议登录结果见[验收记录](../smoke/results/2026-09-27-initial-routing.md)。
本次专项测试与真实 Uranium 协议登录结果见[验收记录](../testing/smoke/results/2026-09-27-initial-routing.md)。
+7 -7
View File
@@ -22,19 +22,19 @@ PDS、AetherShard 与 LuckPerms 可以使用同一台 MariaDB 服务,各自保
git submodule update --init --recursive
bash ./gradlew clean check :proxy-core:distZip --no-daemon --max-workers=2
mkdir -p build/permission-network-classes
javac -cp 'proxy-core/build/install/moonbridge/lib/*' -d build/permission-network-classes smoke/PermissionNetworkAcceptance.java
node smoke/permission-services.cjs start
javac -cp 'proxy/core/build/install/moonbridge/lib/*' -d build/permission-network-classes testing/smoke/PermissionNetworkAcceptance.java
node testing/smoke/permission-services.cjs start
```
运行下面两轮后,务必执行 stop(失败也需清理):
```sh
node smoke/permission-network.cjs sql outage
node smoke/permission-network.cjs redis outage
node smoke/permission-services.cjs query
node smoke/permission-services.cjs stop
node testing/smoke/permission-network.cjs sql outage
node testing/smoke/permission-network.cjs redis outage
node testing/smoke/permission-services.cjs query
node testing/smoke/permission-services.cjs stop
```
fixture 只创建带本轮所有权标签的本机容器,固定使用 `mariadb:13.0.2` 并检查实际服务器版本。SQL 模式使用 MariaDB 原生自动 SQL messenger;Redis 模式仍使用 MariaDB 保存权限。测试执行双实例授权/撤销/组继承、上下文、临时权限、插件重启持久化,以及 MariaDB/Redis 停机恢复。普通构建不会自动执行这些外部服务测试。
stop 删除本轮容器和数据卷、移除凭据文件,并对保留的测试配置密码脱敏。[当前验收记录](../smoke/results/2026-10-02-mariadb.md) 与早期 MySQL/H2/游戏客户端证据分别记录,避免将旧运行结果当成本次 MariaDB 联测。
stop 删除本轮容器和数据卷、移除凭据文件,并对保留的测试配置密码脱敏。[当前验收记录](../testing/smoke/results/2026-10-02-mariadb.md) 与早期 MySQL/H2/游戏客户端证据分别记录,避免将旧运行结果当成本次 MariaDB 联测。
+9 -9
View File
@@ -4,7 +4,7 @@
## 配置和启动
默认配置模板为 [`moonbridge.yml`](../proxy-core/src/main/resources/config/moonbridge.yml)。发行包将示例配置放在 `config/`;自有插件 JAR 可放在 `plugins.directory` 指向的目录。相对 `plugins.directory` 从所选配置文件所在目录解析;`plugins.enabled` 以插件实现类全名为键,空映射不启用外部插件。插件可用设置由各插件在启动时校验。
默认配置模板为 [`moonbridge.yml`](../proxy/core/src/main/resources/config/moonbridge.yml)。发行包将示例配置放在 `config/`;自有插件 JAR 可放在 `plugins.directory` 指向的目录。相对 `plugins.directory` 从所选配置文件所在目录解析;`plugins.enabled` 以插件实现类全名为键,空映射不启用外部插件。插件可用设置由各插件在启动时校验。
本地安装发行包并启动:
@@ -44,16 +44,16 @@ status:
- 可选的 `websocket` 段在 TCP `listen` 之外再开一个 WebSocket 玩家监听,供经 CDN 接入的客户端使用;两种入口同时可用,见 [WebSocket 接入](websocket.md)。OFFLINE 模式对 `websocket.listen` 同样要求字面量回环 IP,除非设置 `allowOfflinePublicAccess: true`。
- `maxConnections` 默认 4096,范围为 1–1,000,000;计数包括登录中的客户端,以及尚未完成 WebSocket 升级的连接。达到上限时新连接直接关闭。按部署资源和连接规模配置,不将其解释为后端容量。
- `transport` 选择玩家连接、后端连接和 DNS 使用的网络传输层,默认 `AUTO`:Linux 上用 epoll,macOS 上用 kqueue,其他平台(如 Windows)或原生库无法加载时(如 ARM/x86 以外的架构、musl 发行版)回退到 NIO,启动日志会输出实际选用的传输层。发行包附带 Linux x86_64/aarch64 的 epoll 与 macOS x86_64/aarch64 的 kqueue 原生库,启动脚本已加 `--enable-native-access=ALL-UNNAMED`。显式写 `NIO`、`EPOLL` 或 `KQUEUE` 会强制使用对应实现;指定的原生传输层不可用时启动失败,不会静默回退。后端控制通道使用独立的阻塞套接字,不受此项影响。
- 发行包默认关闭 Netty 的内存泄漏检测(`-Dio.netty.leakDetection.level=disabled`):它在每个手写转发/编码路径上都有逐帧开销,合成基准中 burst 场景每帧 CPU 中位数约低 26%(范围有重叠,见[记录](../benchmarks/results/2026-10-05-event-loop-queue-and-netty.md))。代替它的是测试期的 `LeakGate`:`proxy-core` 的每个测试类都以 PARANOID 级别运行,发现未释放的缓冲区即让该类失败,且有自测证明它能抓到故意的泄漏。引用计数的手写位置已集中到 `ByteBufs`、`FrameTransformHandler` 与 Netty 自带的编解码基类里;已评估的手写 release 位置都有测试守护,范围与方法见[验证说明](testing.md)。需要在某个部署上重新启用时,设置 `JAVA_OPTS=-Dio.netty.leakDetection.level=simple`(启动脚本里后出现的 `-D` 覆盖默认值)。
- 发行包默认关闭 Netty 的内存泄漏检测(`-Dio.netty.leakDetection.level=disabled`):它在每个手写转发/编码路径上都有逐帧开销,合成基准中 burst 场景每帧 CPU 中位数约低 26%(范围有重叠,见[记录](../testing/benchmarks/results/2026-10-05-event-loop-queue-and-netty.md))。代替它的是测试期的 `LeakGate`:`proxy-core` 的每个测试类都以 PARANOID 级别运行,发现未释放的缓冲区即让该类失败,且有自测证明它能抓到故意的泄漏。引用计数的手写位置已集中到 `ByteBufs`、`FrameTransformHandler` 与 Netty 自带的编解码基类里;已评估的手写 release 位置都有测试守护,范围与方法见[验证说明](testing.md)。需要在某个部署上重新启用时,设置 `JAVA_OPTS=-Dio.netty.leakDetection.level=simple`(启动脚本里后出现的 `-D` 覆盖默认值)。
- `plugins.eventTimeoutSeconds` 为统一事件链期限,默认 5 秒,可设为 1–30 秒。玩家准入事件按订阅注册顺序共享一个期限;无订阅者时跳过派发。准入队列容量为 128,最多 1024 个未决请求。队列饱和、超时、异常或空结果都会拒绝当前访问:连接准入阶段关闭 TCP 连接,玩家准入阶段返回通用错误文本;插件可通过显式拒绝结果提供登录断开原因。生命周期通知有独立的单线程 FIFO 队列,容量 128,最多一个事件正在派发;单事件链受同一事件期限约束。过载时尽力丢弃并聚合记录,不影响连接处理。
- 未认证帧上限为 4 KiB;后端 Login Success 后切换到协议 5 PLAY 帧上限。握手/身份验证与后端连接/登录各自有 15 秒期限;`initialRouting.timeoutSeconds` 默认 15 秒,可设为 1–120 秒,覆盖插件异步选服和所有候选服务器的 DNS/TCP 建连。`initialRouting.servers` 是无插件策略时显式配置的有序候选服务器名,最多 16 个;默认空列表会拒绝首次登录,不会从动态注册目录猜一个服务器。有初始选服插件时,该插件的选择完全接管路由,失败不会回退到配置列表。只有发送任何 Minecraft 握手/登录字节前的 DNS/TCP 失败可继续尝试候选;开始后端登录后失败会结束本次登录,不重放协议流量。成功登录后,客户端须在两分钟内完成首次 PLAY/Forge 握手。
- 登录拒绝、插件主动断开以及后端关闭/读失败的已提交前端写入最多排空等待 5 秒。转服切换阶段有 15 秒期限;Forge 客户端切换后目标握手最多等待 30 秒。各期限届满可能使当前登录失败或关闭当前会话。
## 可选后端发现
后端控制通道示例见 [`moonbridge-channel.example.yml`](../proxy-core/src/main/resources/config/moonbridge-channel.example.yml),使用独立监听端口接收实例注册、心跳和双向消息;使用方法与限制见[后端控制通道](backend-channel-design.md)。
后端控制通道示例见 [`moonbridge-channel.example.yml`](../proxy/core/src/main/resources/config/moonbridge-channel.example.yml),使用独立监听端口接收实例注册、心跳和双向消息;使用方法与限制见[后端控制通道](backend-channel-design.md)。
Docker 容器间的监听、服务名和地址配置见[配置与部署](backend-channel-design.md#配置与部署);`sh smoke/container-channel.sh` 可运行跨容器烟测。
Docker 容器间的监听、服务名和地址配置见[配置与部署](backend-channel-design.md#配置与部署);`sh testing/smoke/container-channel.sh` 可运行跨容器烟测。
控制通道默认关闭。启用后,后端实例通过独立 TCP 连接认证和注册;代理不提供 DNS 发现或 HTTP 注册。代理核心没有内置云实例发现或健康探测。
@@ -71,9 +71,9 @@ 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/src/test/`
- 登录、认证和协议:`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/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);已有成功样例不构成稳定性或生产验收。
仓库另有本地 Uranium 协议探针和 Prism Forge 客户端记录,见 [`testing/smoke/results`](../testing/smoke/results/2026-09-25-local-uranium.md)、[转服记录](../testing/smoke/results/2026-09-25-local-uranium-transfer.md) 和 [真实客户端记录](../testing/smoke/results/2026-09-25-prism-forge-client.md)。这些记录支持其明确写出的单机环境、软件版本和探针路径。已知也出现过真实 Forge 客户端转服候选后端登录停滞,见[调查记录](../testing/smoke/results/2026-09-26-uranium-login-stalls.md);已有成功样例不构成稳定性或生产验收。
+2 -2
View File
@@ -55,7 +55,7 @@ context.commands().register("island", "example.command.island", invocation -> {
## LuckPerms 平台插件
`luckperms-moonbridge` 使用固定版本的官方 LuckPerms 公共核心,并实现 MoonBridge 平台适配。上游源码在 Git 子模块 `vendor/luckperms`。编译它较慢且依赖很多,因此默认构建不包含该插件,需要显式开启:
`luckperms-moonbridge` 使用固定版本的官方 LuckPerms 公共核心,并实现 MoonBridge 平台适配。上游源码在 Git 子模块 `proxy/vendor/luckperms`。编译它较慢且依赖很多,因此默认构建不包含该插件,需要显式开启:
```sh
git submodule update --init --recursive
@@ -120,7 +120,7 @@ redis:
Redis 负责通知其他实例重载权限,权限数据仍保存在 MariaDB。Redis 中断期间不会保证其他实例即时看到修改;恢复连接后的新通知可触发刷新,也可以在需要时使用 `lp sync` 主动同步。数据库中断时,已加载玩家可继续使用现有缓存,新玩家加载失败会被拒绝登录;缓存不是数据库最新状态的保证。
此前 MySQL 8.4 与 Redis 7.4 的结果保留为 [历史验收记录](../smoke/results/2026-09-28-permissions.md)。当前 MariaDB 13.0.2 的验收入口和记录见 [MariaDB 验证](mariadb.md)。
此前 MySQL 8.4 与 Redis 7.4 的结果保留为 [历史验收记录](../testing/smoke/results/2026-09-28-permissions.md)。当前 MariaDB 13.0.2 的验收入口和记录见 [MariaDB 验证](mariadb.md)。
backend 可安装与其 Minecraft / Java 版本兼容的原生 Bukkit LuckPerms,使用相同的身份模式、共享数据库与 LuckPerms 支持的同步方式;MoonBridge 平台插件 JAR 只供代理使用。共享数据库不意味着所有场景的检查结果相同:代理知道后端名称,但不知道玩家在 Bukkit 世界中的位置或其他后端私有上下文。玩家首次登录的权限加载不依赖某个 backend 在线。
+1 -1
View File
@@ -1,6 +1,6 @@
# 玩家操作与部署完善设计
状态:本文保留首批玩家操作的设计基线。当前消息模型见[富文本与命令补全](messages-and-completion.md)。2026-09-26 Prism 记录是历史提交下的真实在线认证和客户端结果,不代表当前初次路由分支的完整模组包验收;当前路由证据见[专项验收](../smoke/results/2026-09-27-initial-routing.md)。
状态:本文保留首批玩家操作的设计基线。当前消息模型见[富文本与命令补全](messages-and-completion.md)。2026-09-26 Prism 记录是历史提交下的真实在线认证和客户端结果,不代表当前初次路由分支的完整模组包验收;当前路由证据见[专项验收](../testing/smoke/results/2026-09-27-initial-routing.md)。
## 目标与边界
+1 -1
View File
@@ -145,6 +145,6 @@ context.events().subscribe(PlayerDisconnectedEvent.class, event ->
统一事件模型让准入决策和会话通知共享同一类型、监听器、订阅表与派发实现,同时保留各类事件所需的运行策略。准入阶段执行有序且失败关闭的结果聚合;生命周期通知异步 FIFO 尽力交付并与会话处理隔离。没有 PLAY 包事件。目前没有事件 API 的专门基准,不能据此宣称性能收益或无开销。
源码入口:[`PluginContext`](../proxy-plugin-api/src/main/java/dev/moonbridge/api/PluginContext.java)、[`Events`](../proxy-plugin-api/src/main/java/dev/moonbridge/api/event/Events.java)、[`AccessDecision`](../proxy-plugin-api/src/main/java/dev/moonbridge/api/AccessDecision.java)。
源码入口:[`PluginContext`](../proxy/plugin-api/src/main/java/dev/moonbridge/api/PluginContext.java)、[`Events`](../proxy/plugin-api/src/main/java/dev/moonbridge/api/event/Events.java)、[`AccessDecision`](../proxy/plugin-api/src/main/java/dev/moonbridge/api/AccessDecision.java)。
富文本构造、MiniMessage 和异步命令补全示例见[消息与命令补全](messages-and-completion.md)。
+1 -1
View File
@@ -54,7 +54,7 @@ dependencies {
## Proxy 发行包
`:proxy-core:distZip` 生成 `proxy-core/build/distributions/moonbridge-<version>.zip`。CI 把这个 ZIP 作为 `moonbridge-development` 下载工件的一部分上传,保留 30 天;它不是 Maven 模块,也不自动成为 Git Release。解压后从发行包目录运行:
`:proxy-core:distZip` 生成 `proxy/core/build/distributions/moonbridge-<version>.zip`。CI 把这个 ZIP 作为 `moonbridge-development` 下载工件的一部分上传,保留 30 天;它不是 Maven 模块,也不自动成为 Git Release。解压后从发行包目录运行:
```powershell
.\bin\moonbridge.bat --validate-config .\config\moonbridge.yml
+22 -22
View File
@@ -7,26 +7,26 @@
.\gradlew.bat :proxy-core:installedDistSmokeTest
.\gradlew.bat -Pluckperms :proxy-core:permissionAcceptance
.\smoke\container-network.ps1
sh smoke/container-channel.sh
sh testing/smoke/container-channel.sh
```
定向测试覆盖控制通道认证与代次、目录租约、插件生命周期与超时、协议边界、relay 背压,以及合成 TCP 登录和切换。初次路由最新专项验收包括自动化覆盖和实际 Java 8 Uranium 后端协议探针,见[2026-09-27 路由验收](../smoke/results/2026-09-27-initial-routing.md);它不是完整模组包视觉验收。2026-09-26 Prism 记录保留为其提交和环境下的历史真实客户端/配对测量,见[历史 Prism 记录](../benchmarks/results/2026-09-26-prism-pack.md)。下面其他历史结果也只支持其记录注明的范围。
定向测试覆盖控制通道认证与代次、目录租约、插件生命周期与超时、协议边界、relay 背压,以及合成 TCP 登录和切换。初次路由最新专项验收包括自动化覆盖和实际 Java 8 Uranium 后端协议探针,见[2026-09-27 路由验收](../testing/smoke/results/2026-09-27-initial-routing.md);它不是完整模组包视觉验收。2026-09-26 Prism 记录保留为其提交和环境下的历史真实客户端/配对测量,见[历史 Prism 记录](../testing/benchmarks/results/2026-09-26-prism-pack.md)。下面其他历史结果也只支持其记录注明的范围。
`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 整合包实测。
`testing/smoke/container-network.ps1` 使用 Docker bridge 中的独立代理、后端和客户端容器,依次检查静态配置和控制通道注册、登录及 PLAY 转发;控制通道模式还验证注销后已连接会话继续转发。PowerShell 7 和 Docker Desktop 是运行前提。`testing/smoke/container-channel.sh` 在 Unix shell 下验证控制通道的同一跨容器路径。2026-09-25 的[容器网络烟测记录](../testing/smoke/results/2026-09-25-container-network.md)是旧实现的历史结果,不代表当前通道已在那次运行中验证。容器烟测不能代替跨物理主机或目标 Forge 整合包实测。
`.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)。
带 `-Pluckperms` 时 `check` 包含 `permissionAcceptance`(本地默认 `check` 不编译 LuckPerms,因此不含这一项;CI 始终开启):使用发行包中的实际 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)。真实客户端记录见下段和其单独的历史验收档案;它们不应与当前路由分支的完整模组包验收混为一谈。
如果本地已有 Uranium 1.7.10 可运行包及其编译好的 `MinecraftProtocolProbe`,可运行 `testing/smoke/local-uranium.ps1 -BundlePath <包目录> -ProbeClassesPath <探针类目录>`。脚本复制服务端到忽略目录,启动 Java 8 后端与当前安装包,再让探针经代理完成状态查询、FML 登录、Join Game 和持续 Keep Alive,结束时停止两个进程。本地一次结果与具体前提见 [最小 Uranium 联机记录](../testing/smoke/results/2026-09-25-local-uranium.md)。初次路由的当前 `ProxyMain` 协议探针结果另见[路由验收](../testing/smoke/results/2026-09-27-initial-routing.md)。真实客户端记录见下段和其单独的历史验收档案;它们不应与当前路由分支的完整模组包验收混为一谈。
`smoke/local-uranium-transfer.ps1 -BundlePath <包目录>` 复制并启动两台 Uranium,然后编译仓库中的协议探针,通过实际 `ProxySessionListener` 请求从旧服切换到新服。加上 `-InstalledPlugin` 则启动已安装的 `ProxyMain`:旧服由静态配置注册,临时插件用 `Servers.register` 注册目标服,在初始落点回调读取 `ServerView`,再通过 `Players.transfer` 发起转服。默认模式使用协议探针核对 FML 重置、重新握手、世界切换包和目标连接的 Keep Alive,见 [两台 Uranium 转服记录](../smoke/results/2026-09-25-local-uranium-transfer.md)。`-InstalledPlugin -ReturnToOld` 让协议探针验证旧服→新服→旧服的两次切换。`-InstalledPlugin -PrismClient` 改用本机 Prism 中的 `1.7.10` Forge 实例,要求客户端在目标服保持连接 10 秒;可同时使用 `-ReturnToOld` 验证真实客户端往返。两个真实客户端脚本都可用 `-PrismInstance <实例名>` 选择其他实例。若实例的文件夹名与启动名不同,另传 `-PrismInstanceFolder <文件夹名>`,以便准确定位并清理这次启动的客户端;`-PrismPath` 指向其他 Prism 安装位置。`smoke/local-uranium.ps1 -PrismClient` 可单独验证首次登录。这些实测仍不能替代目标整合包验收。
`testing/smoke/local-uranium-transfer.ps1 -BundlePath <包目录>` 复制并启动两台 Uranium,然后编译仓库中的协议探针,通过实际 `ProxySessionListener` 请求从旧服切换到新服。加上 `-InstalledPlugin` 则启动已安装的 `ProxyMain`:旧服由静态配置注册,临时插件用 `Servers.register` 注册目标服,在初始落点回调读取 `ServerView`,再通过 `Players.transfer` 发起转服。默认模式使用协议探针核对 FML 重置、重新握手、世界切换包和目标连接的 Keep Alive,见 [两台 Uranium 转服记录](../testing/smoke/results/2026-09-25-local-uranium-transfer.md)。`-InstalledPlugin -ReturnToOld` 让协议探针验证旧服→新服→旧服的两次切换。`-InstalledPlugin -PrismClient` 改用本机 Prism 中的 `1.7.10` Forge 实例,要求客户端在目标服保持连接 10 秒;可同时使用 `-ReturnToOld` 验证真实客户端往返。两个真实客户端脚本都可用 `-PrismInstance <实例名>` 选择其他实例。若实例的文件夹名与启动名不同,另传 `-PrismInstanceFolder <文件夹名>`,以便准确定位并清理这次启动的客户端;`-PrismPath` 指向其他 Prism 安装位置。`testing/smoke/local-uranium.ps1 -PrismClient` 可单独验证首次登录。这些实测仍不能替代目标整合包验收。
安装包转服烟测可加 `-DebugSession`,在本次复制的 Uranium 配置中开启登录阶段日志,并将代理会话的连接、登录写入和失败路径 DEBUG 日志写入所打印运行目录的 `proxy.stdout.log`。加 `-TraceBackend` 会在代理与两台 Uranium 之间放置本地 TCP 中继,并将每个方向的前 512 字节记录到运行目录的 `tap-old.log` 和 `tap-new.log`;这一诊断选项会改变连接时序。初次登录 EOF 的复现实验见 [运行记录](../smoke/results/2026-09-26-initial-login-repeat.md);增强日志和中继后捕获的 Uranium 登录停滞及源码假设见 [登录停滞记录](../smoke/results/2026-09-26-uranium-login-stalls.md)。
安装包转服烟测可加 `-DebugSession`,在本次复制的 Uranium 配置中开启登录阶段日志,并将代理会话的连接、登录写入和失败路径 DEBUG 日志写入所打印运行目录的 `proxy.stdout.log`。加 `-TraceBackend` 会在代理与两台 Uranium 之间放置本地 TCP 中继,并将每个方向的前 512 字节记录到运行目录的 `tap-old.log` 和 `tap-new.log`;这一诊断选项会改变连接时序。初次登录 EOF 的复现实验见 [运行记录](../testing/smoke/results/2026-09-26-initial-login-repeat.md);增强日志和中继后捕获的 Uranium 登录停滞及源码假设见 [登录停滞记录](../testing/smoke/results/2026-09-26-uranium-login-stalls.md)。
## 历史验证记录
带日期的逐轮验证结果放在 [`records/`](records/),与 `smoke/results/`、`benchmarks/results/` 一样只证明各自注明的提交和环境:
带日期的逐轮验证结果放在 [`records/`](records/),与 `testing/smoke/results/`、`testing/benchmarks/results/` 一样只证明各自注明的提交和环境:
- [2026-09-26 验证记录](records/2026-09-26-validation-log.md):玩家操作与服务器列表、统一事件模型、历史类型化事件与访问检查。
- [2026-09-27 富文本与命令补全验证](records/2026-09-27-rich-text-completion.md)。
@@ -39,7 +39,7 @@ sh smoke/container-channel.sh
## 合成 relay 基准
`benchmarks/run-relay.ps1` 比较同一 JVM、同一个本机回声后端上的直连、原始字节 relay、按帧 relay,以及安装了实际 `KeepAliveBridge` 的按帧 relay。每个连接先预热,再重复发送固定长度的合成 Minecraft 帧并读取同样长度的回声;连接数、每连接消息数、预热数、帧负载字节数、重复轮数和同时在途的消息数均可配置。`-Window 1` 是逐包等待回声;更大的窗口使用独立写线程持续发送,读线程按顺序核对回声,并用信号量限制在途消息数。基准在轮次间交替执行三种 relay,并在首尾测直连基线。输出往返吞吐、按单向传输字节计算的 MiB/s、往返延迟 p50/p95/p99、JVM GC 次数/耗时和堆已用量变化。
`testing/benchmarks/run-relay.ps1` 比较同一 JVM、同一个本机回声后端上的直连、原始字节 relay、按帧 relay,以及安装了实际 `KeepAliveBridge` 的按帧 relay。每个连接先预热,再重复发送固定长度的合成 Minecraft 帧并读取同样长度的回声;连接数、每连接消息数、预热数、帧负载字节数、重复轮数和同时在途的消息数均可配置。`-Window 1` 是逐包等待回声;更大的窗口使用独立写线程持续发送,读线程按顺序核对回声,并用信号量限制在途消息数。基准在轮次间交替执行三种 relay,并在首尾测直连基线。输出往返吞吐、按单向传输字节计算的 MiB/s、往返延迟 p50/p95/p99、JVM GC 次数/耗时和堆已用量变化。
```powershell
.\benchmarks\run-relay.ps1 -Connections 8 -Messages 2000 -Warmup 200 -Payload 4096
@@ -48,8 +48,8 @@ sh smoke/container-channel.sh
该基准只覆盖本机 TCP 回声和三种 relay 数据路径。不同窗口使用不同的客户端发送方式,应在相同窗口内比较。负载是合成帧,不含登录、Forge 握手、模组流量、转服时的实体 ID 改写或真实客户端/后端行为;结果不代表 1.7.10 整合包等价性能,也不设 CI 性能门槛。堆变化是阶段前后的粗略观测,不是分配速率;请在目标机器、JDK 和连接规模上多轮运行并记录环境,避免把单次结果当成容量承诺。
本地一次测量的环境、参数与原始输出见 `benchmarks/results/2026-09-25-local-relay.md`。
加入实际 `KeepAliveBridge` 后的配对测量见 `benchmarks/results/2026-09-25-keepalive-bridge.md`;两份结果使用的 relay 事件循环安排不同,不能直接视为前后性能对比。
本地一次测量的环境、参数与原始输出见 `testing/benchmarks/results/2026-09-25-local-relay.md`。
加入实际 `KeepAliveBridge` 后的配对测量见 `testing/benchmarks/results/2026-09-25-keepalive-bridge.md`;两份结果使用的 relay 事件循环安排不同,不能直接视为前后性能对比。
## 真实 TCP 慢接收端背压回归
@@ -63,34 +63,34 @@ sh smoke/container-channel.sh
## 合成会话基准
`benchmarks/run-proxy-session.ps1` 在同一 JVM 中比较直接连接模拟后端,以及经过实际 `ProxySessionListener` 登录、落点选择和会话转发后连接同一后端。客户端先完成离线登录,读取 Join Game 和 Position and Look,再预热;计时仅包含固定 PLAY 帧的往返。`-Window` 设置每个客户端允许的在途请求数,默认 `1`(stop-and-wait);大于 `1` 时客户端最多连续发送 Window 个帧,再按 TCP 顺序读取回声并逐条校验,每收到一条便补发一条。预热和计时阶段、直连和代理都使用相同的发送算法。测得的单条延迟从该请求写出前计时到其回声读完;延迟样本和在途时间戳都使用有界数组,受 `Connections`、`Messages` 和 Window 上限约束。出现错误回声或阶段超时时基准以失败退出。
`testing/benchmarks/run-proxy-session.ps1` 在同一 JVM 中比较直接连接模拟后端,以及经过实际 `ProxySessionListener` 登录、落点选择和会话转发后连接同一后端。客户端先完成离线登录,读取 Join Game 和 Position and Look,再预热;计时仅包含固定 PLAY 帧的往返。`-Window` 设置每个客户端允许的在途请求数,默认 `1`(stop-and-wait);大于 `1` 时客户端最多连续发送 Window 个帧,再按 TCP 顺序读取回声并逐条校验,每收到一条便补发一条。预热和计时阶段、直连和代理都使用相同的发送算法。测得的单条延迟从该请求写出前计时到其回声读完;延迟样本和在途时间戳都使用有界数组,受 `Connections`、`Messages` 和 Window 上限约束。出现错误回声或阶段超时时基准以失败退出。
```powershell
.\benchmarks\run-proxy-session.ps1 -Connections 4 -Messages 1000 -Warmup 100 -Payload 1024 -Repeats 2 -Window 1
.\benchmarks\run-proxy-session.ps1 -Connections 4 -Messages 1000 -Warmup 100 -Payload 1024 -Repeats 2 -Window 1 -Mode post-transfer
```
2026-09-25 的多轮、同条件本机测量及原始输出见 [生命周期修复后的会话基准记录](../benchmarks/results/2026-09-25-session-after-lifecycle.md)。更早的测量见 [原会话基准记录](../benchmarks/results/2026-09-25-current-session-benchmark.md)。这些记录对应各自注明的源码版本,不是当前事件 API 的性能结果,也不代表后端负载观测。
2026-09-25 的多轮、同条件本机测量及原始输出见 [生命周期修复后的会话基准记录](../testing/benchmarks/results/2026-09-25-session-after-lifecycle.md)。更早的测量见 [原会话基准记录](../testing/benchmarks/results/2026-09-25-current-session-benchmark.md)。这些记录对应各自注明的源码版本,不是当前事件 API 的性能结果,也不代表后端负载观测。
`-Mode post-transfer` 让代理客户端先从 `bench` 转到 `replacement`,直连客户端直接进入同一个 `replacement` 模拟后端;转服与预热均在计时外。两后端使用不同玩家实体 ID,因此代理在转服后仍运行实体 ID 映射路径。[转服前后会话基准记录](../benchmarks/results/2026-09-25-post-transfer-session.md) 包含窗口 1 和 16 的同条件多轮测量及原始输出。结果没有显示值得据此修改普通转发路径的稳定差异。
`-Mode post-transfer` 让代理客户端先从 `bench` 转到 `replacement`,直连客户端直接进入同一个 `replacement` 模拟后端;转服与预热均在计时外。两后端使用不同玩家实体 ID,因此代理在转服后仍运行实体 ID 映射路径。[转服前后会话基准记录](../testing/benchmarks/results/2026-09-25-post-transfer-session.md) 包含窗口 1 和 16 的同条件多轮测量及原始输出。结果没有显示值得据此修改普通转发路径的稳定差异。
加入 Forge 握手阶段旧世界数据包过滤后,在该历史版本上重跑的 [转服后基准](../benchmarks/results/2026-09-25-forge-transfer-gate-followup.md) 与先前结果的范围重叠;合成流量没有显示稳定的大幅退化,目标整合包仍需单独测量。
加入 Forge 握手阶段旧世界数据包过滤后,在该历史版本上重跑的 [转服后基准](../testing/benchmarks/results/2026-09-25-forge-transfer-gate-followup.md) 与先前结果的范围重叠;合成流量没有显示稳定的大幅退化,目标整合包仍需单独测量。
同一提交在独立 Linux 容器中的 [转服后会话基准](../benchmarks/results/2026-09-25-linux-container-post-transfer.md) 也完成了窗口 1 和 16 的各五轮对照与逐包校验。该结果仅验证另一运行环境中的合成路径;目标整合包和跨主机网络仍需实测。
同一提交在独立 Linux 容器中的 [转服后会话基准](../testing/benchmarks/results/2026-09-25-linux-container-post-transfer.md) 也完成了窗口 1 和 16 的各五轮对照与逐包校验。该结果仅验证另一运行环境中的合成路径;目标整合包和跨主机网络仍需实测。
2026-09-25 会话路径的 [JFR 采样记录](../benchmarks/results/2026-09-25-session-jfr-screening.md) 只提供了分配线索,代理 I/O 线程的 CPU 执行样本不足以定位热点;因此没有据此改动普通转发路径。
2026-09-25 会话路径的 [JFR 采样记录](../testing/benchmarks/results/2026-09-25-session-jfr-screening.md) 只提供了分配线索,代理 I/O 线程的 CPU 执行样本不足以定位热点;因此没有据此改动普通转发路径。
[玩家命令拦截初筛](../benchmarks/results/2026-09-26-command-interceptor.md) 比较同一合成 PLAY 回声流量下启用与关闭命令拦截器的结果;三轮吞吐和延迟范围重叠。该实验没有发送命令,也不能替代目标整合包的容量验收。
[玩家命令拦截初筛](../testing/benchmarks/results/2026-09-26-command-interceptor.md) 比较同一合成 PLAY 回声流量下启用与关闭命令拦截器的结果;三轮吞吐和延迟范围重叠。该实验没有发送命令,也不能替代目标整合包的容量验收。
将 `-Window` 改为 `16` 可测每连接最多 16 个在途往返的情形。
`-Burst N`(直接运行 Java 时为 `--burst N`)让每次往返由客户端一次写出 N 个帧、模拟后端一次 flush 回声,接近一个服务器 tick 内的多个小 PLAY 包;此时 Window 按批计数。输出另含代理 I/O 线程每帧 CPU 时间,以及在 Linux 上读取 `/proc/net/snmp` 得到的全机 TCP 发送段数/帧(含 ACK,其他平台显示 `n/a`)。[2026-10-05 批量 flush 记录](../benchmarks/results/2026-10-05-relay-batched-flush.md)用它比较逐帧 flush 与按读取批次 flush。`-Transport auto|nio|epoll|kqueue`(`--transport`)选择代理使用的网络传输层,默认 `auto`;[2026-10-05 传输层记录](../benchmarks/results/2026-10-05-network-transport.md)比较了 epoll 与 NIO。
`-Burst N`(直接运行 Java 时为 `--burst N`)让每次往返由客户端一次写出 N 个帧、模拟后端一次 flush 回声,接近一个服务器 tick 内的多个小 PLAY 包;此时 Window 按批计数。输出另含代理 I/O 线程每帧 CPU 时间,以及在 Linux 上读取 `/proc/net/snmp` 得到的全机 TCP 发送段数/帧(含 ACK,其他平台显示 `n/a`)。[2026-10-05 批量 flush 记录](../testing/benchmarks/results/2026-10-05-relay-batched-flush.md)用它比较逐帧 flush 与按读取批次 flush。`-Transport auto|nio|epoll|kqueue`(`--transport`)选择代理使用的网络传输层,默认 `auto`;[2026-10-05 传输层记录](../testing/benchmarks/results/2026-10-05-network-transport.md)比较了 epoll 与 NIO。
原 stop-and-wait 小样本见 `benchmarks/results/2026-09-25-proxy-session-benchmark-smoke.md`;Window 参数的小样本和 Window=1/16 重复测量见 `benchmarks/results/2026-09-25-proxy-session-window.md`。这些是环回网络上的合成帧对照,不能代表 Forge 整合包、真实后端或跨主机部署的性能。
原 stop-and-wait 小样本见 `testing/benchmarks/results/2026-09-25-proxy-session-benchmark-smoke.md`;Window 参数的小样本和 Window=1/16 重复测量见 `testing/benchmarks/results/2026-09-25-proxy-session-window.md`。这些是环回网络上的合成帧对照,不能代表 Forge 整合包、真实后端或跨主机部署的性能。
## 后端宿主真实服务端验收
`smoke/backend-uranium.ps1` 将已经接受 EULA 的 Uranium 发行包中的服务端 JAR、运行库和 EULA 复制到全新隔离目录,启动两个 Java 8 后端及实际 Java 25 Proxy。它安装 `MoonBridgeBackend`、两个独立的 Bukkit 业务插件和一个外部 Proxy 验收插件,通过正式插件 API 检查注册与消息服务;不会复制或修改原有世界、模组、插件及凭据。Bukkit 业务插件以发行包 `backend/` 中的 JAR 和服务器 Bukkit API 编译(compileOnly,运行时依赖 `MoonBridgeBackend`),并在实际插件加载器下验证服务类身份一致。
`testing/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
@@ -100,4 +100,4 @@ sh smoke/container-channel.sh
脚本默认使用本机 Zulu Java 8 和 Java 25,可通过 `-Java8Home`、`-Java25Home` 指定其他路径。完整成功输出 `BACKEND_URANIUM_PASS`;任一阶段失败会返回非零退出码并保留日志。后台进程有启动、请求、停机期限,退出时只清理本次创建的进程。
验收覆盖 Proxy→后端、后端→Proxy、后端→后端请求,UUID/replyTo、多个独立订阅者接收同一广播、Bukkit 主线程与零在线玩家、业务插件停用隔离、Proxy 重启后的自动重新注册、正常停服快速注销、异常退出后的租约清理和后端重启。具体构件哈希和运行结果见[2026-09-27 后端宿主记录](../smoke/results/2026-09-27-backend-host.md)。
验收覆盖 Proxy→后端、后端→Proxy、后端→后端请求,UUID/replyTo、多个独立订阅者接收同一广播、Bukkit 主线程与零在线玩家、业务插件停用隔离、Proxy 重启后的自动重新注册、正常停服快速注销、异常退出后的租约清理和后端重启。具体构件哈希和运行结果见[2026-09-27 后端宿主记录](../testing/smoke/results/2026-09-27-backend-host.md)。
@@ -86,12 +86,12 @@ val installedDistVersionSmokeTest = tasks.register<Exec>("installedDistVersionSm
}
}
// The benchmarks in benchmarks/ declare proxy-core packages to reach package-private classes and are
// The benchmarks in testing/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") {
group = "verification"
description = "Compiles the benchmarks in benchmarks/ against proxy-core."
source(rootProject.fileTree("benchmarks") { include("*.java") })
description = "Compiles the benchmarks in testing/benchmarks/ against proxy-core."
source(rootProject.fileTree("testing/benchmarks") { include("*.java") })
classpath = sourceSets.main.get().runtimeClasspath
destinationDirectory.set(layout.buildDirectory.dir("classes/benchmarks"))
options.release.set(25)
@@ -101,7 +101,7 @@ val compileBenchmarks = tasks.register<JavaCompile>("compileBenchmarks") {
val luckPerms = rootProject.findProject(":luckperms-moonbridge")
if (luckPerms != null) {
val compilePermissionAcceptance = tasks.register<JavaCompile>("compilePermissionAcceptance") {
source(rootProject.file("smoke/PermissionAcceptance.java"))
source(rootProject.file("testing/smoke/PermissionAcceptance.java"))
classpath = sourceSets.main.get().runtimeClasspath
destinationDirectory.set(layout.buildDirectory.dir("classes/permissionAcceptance"))
options.release.set(25)
Loaded 100 of 369 files, more files were not shown because too many files have changed in this diff. Show more