• 欢迎加入MineBBS QQ讨论群:点击查看所有的官方讨论群
  • 我们将于近期对服务器进行迁移,服务可能中断至多2日。请各位安排好自己的访问计划,造成不便敬请谅解!
  • MineBBS入站考试已经上线!想要成为【正式会员】解锁更多功能吗?快来参与吧!【点我去看】
资源图标

原生 CrossBind -- 跨协议适配 v0.1.0

完成注册/登录后下载
版权类型
原创
版权链接
https://github.com/Maskviva/CrossBind
语言支持
中文(简体)
前置组件
ll-rust-loader 26.20.4+ ( https://www.minebbs.com/resources/levilamina-rust-loader-levilamina-rust.17046/ )
适配版本(基岩)
  1. 最新版本
[MD] # crossbind 跨版本协议适配 —— 让协议版本和服务端不一致的基岩版客户端也能进服。用 Rust 写的 LeviLamina mod。 思路和 ViaVersion 一样:在字节层拦下每个包,按版本差异逐字段改写,然后放行。服务端 本身完全不知道客户端是别的版本。 ## 覆盖范围 **能互通的版本**(任意两两组合,靠链式串联): | 协议 | Minecraft | |-----:|-----------| | 2168 | 1.26.40 | | 1001 | 1.26.30 | | 975 | 1.26.20 | | 944 | 1.26.10 | | 924 | 1.26.0 | | 898 | 1.21.130 | | 860 | 1.21.124 | | 859 | 1.21.120 | 相邻版本各有一个双向翻译步骤,不相邻的走链式串联 —— 比如 v859 客户端连 v2168 服务端 会经过七跳。 **认得但翻译不了的版本**:844 / 827 / 819 / 818 / 800 / 786 / 776 / 766 / 729。这些只在 `versions.rs` 里登记了版本名,用来把拒绝理由说清楚,**没有**翻译逻辑。 为什么不做?往下扩每加一个版本,都需要那一档边界上确切的线格式差异。我手上没有 844 及更早版本的权威依据,凭印象编出来的 handler 只会把包改坏 —— 那比不做更糟,因为报错 会指向完全无关的地方。 ## 已知缺口 翻译是尽力而为,不是完备的。目前明确知道不完整的地方: - **v944 ↔ v975 的物品栏**。v975 换了物品堆的编码(定长 i16 id、没有空气短路、网络 id 多了个变体标签)。`PlayerEquipment` / `AddPlayer` / `AddItemActor` 里的物品已经转了, 但 `InventoryContent` / `InventorySlot` / `CraftingData` / 物品交互那一族**没有** —— 这些包里物品前面的字段在两个版本之间也动过,我没有可靠依据,硬猜会把每一次格子更新 都改坏。所以这对版本组合下容器里的物品会显示错乱。 - **v860 ↔ v898 的 `AvailableCommands` / `CommandOutput`**。类型(`commands.rs`)齐了并 且有测试,但包级 handler 还没接上,所以这对组合下命令补全会不正常。 - **v1001 ↔ v2168(1.26.30 ↔ 1.26.40)覆盖面明显低于其它档**。1.26.40 把大量序列化 搬到了 Cereal 反射上,gophertunnel 侧一次动了 53 个文件,是本项目跨过的最大一道坎。 登录/出生/移动这条链路是通的,但下面这些包**只丢不转**,因为它们的子结构是独立变的, 半对的猜测会在离出错点很远的地方炸:`PlayerSkin`(重做后的皮肤块经由另一层外壳传递, 那层外壳还没有实际抓包核对过)、`MapData`、`SetScore`、`SetScoreboardIdentity`、 `ClientboundUpdateSoundData`、`StructureBlockUpdate`、`SubChunk`(条目里四个字段 全变成了 optional)、`PlayerLocation`(唯一 id 和 type 换了位置、type 从定长 i32 变成 varint;gophertunnel 自己的 v2168 marshal 还把 type 写了两遍,它的模型要么在 描述一个真的重复字段、要么就是错的,两种猜法都不值得赌)。 `CraftingData`(52) **已实现**,见 `steps/crafting_data_v2168.rs`。一个配方表拆成了八个 定型数组,而 `recipeType` 判别值**不是连续的**:`recipe.go` 的 `iota` 里有两个空位 (炉子、炉子数据留下的),真实取值是 0, 1, 4, 5, 6, 7, 8, 9。另外配方里的物品描述符 从数字变成了名字,靠 `ItemRegistry`(162) 缓存的注册表回填;查不到名字的成分只丢那一条 配方,其余照发。 `PlayerList`(63) **已实现**,见 `steps/player_list_v2168.rs`。action 判别符从包头搬进了 每条 entry(v2168 两种情况都还在,所以往上转不需要拆包);`PlayerColour` 只是字节序 从小端变成大端;`EntityUniqueID` 的 `ActorUniqueID` 是纯改名,底下还是 `Varint64`。 真正的工作量在皮肤块的十来处字段改动。唯一一处推断是 `PersonaPiece.PieceType` 从名字变成了枚举号,gophertunnel 只给了枚举没给名字表;表里认不出的名字一律落到 `PieceTypeUnknown`,不猜相邻值——这一项只影响 persona 皮肤外观,不影响条目本身。 `ItemStackRequest`(147) / `ItemStackResponse`(148) **已实现**,见 `steps/item_stack_v2168.rs`。动作的 type id 其实没有重编号 —— 变的是它现在按 Cereal 变体表的下标发送,而那张表略去了客户端从不发送的两个容器动作,所以第 7 项往上整体 少 2,是严格可逆的两行公式。真正容易漏的是 `StackRequestSlotInfo.StackNetworkID` 从 varint 收窄成定长 i32:它嵌在九种动作里,错一处等于毁掉几乎所有请求。 `CraftRecipeAuto`(Shift 点击合成)带配料描述符,整条请求丢弃。 `CraftResultsDeprecated` 曾经**发空结果列表** —— 理由是"这个动作已弃用、内容冗余"。 服务端无条件读取第一个结果,于是空指针解引用、**整个服务端崩溃**。"有损但无害" 变成了比原 bug 严重得多的故障。现在它借助服务端自己发的物品注册表 (`ItemRegistry`,id=162)把名字翻回数字 id;注册表里查不到的名字一律拒绝整条请求, 绝不编造 id。写出的结果条数永远等于读入的条数,有回归测试盯着。 `CraftingData`(52) 仍然丢弃,这是合成不了的直接原因 —— 它的配方输入输出走同一套 描述符,需要把整张注册表映射铺到 `recipe.go` 那一族上,还没做。 另外 1.26.40 有三处"看着像稳定尾巴、其实变了"的地方,都已经处理: `StartGame` 末尾的 `ServerJoinInformation`(**整个结构体外面还套着一层 optional**, 内部四个字段又各自包进了 optional)、`CreativeContent`(分组 `Category` 从 **定长 i32 小端**收窄成 byte,且里面的物品描述符取消了空气短路),以及 v1001 的 游戏规则整数是**无符号 varint 而不是 zigzag**(读的字节数一样,所以不会错位, 只会让客户端拿到一个没人发过的值)。出生流程里前两者都必发,漏掉任何一个都表现为 "登录成功、进服前掉线"。 - 目标版本没有的包会被**丢弃**而不是硬转。丢包损失一个功能,硬转损失整条连接。 ## 依赖 需要 levilamina-rust-loader 26.20.4 以上的版本。 `crossbind-mod/Cargo.toml` 里的 `levilamina` 现在指向本地路径,按你的实际布局改,你也可以指向 [github仓库](https://github.com/Maskviva/levilamina-rust-loader) 或者使用 [crates.io上的levilamina包](https://crates.io/crates/levilamina)。 ## 构建 ```bash cargo test --workspace # 先跑测试,见下 cargo build --release -p crossbind-mod ``` **产物只有一个 DLL**:`target/release/crossbind.dll`。连同 `manifest.json` 一起丢进 `plugins/crossbind/` 就行。 workspace 里虽然有三个 crate,但只有 `crossbind-mod` 声明了 `crate-type = ["cdylib"]`; 另外两个是默认的 `rlib`,编译期就静态链进 DLL 里了。`target/release/` 下面你会看到 `libbedrock_codec.rlib` / `libbedrock_protocol.rlib`,那是中间产物不是插件;MSVC 还会 额外产出 `crossbind.dll.lib` / `.exp` / `.pdb`,同样不用管。 ## 配置 不用配。服务端协议版本在 `on_enable` 里通过 `ctx.server().protocol_version()` 读出来 (底层是 `SharedConstants::NetworkProtocolVersion()`,走 loader 已有的 `server_info_str` 槽位,不需要新 ABI)。 如果服务端本身跑在不能翻译的版本上(比如 1.26.30 / 协议 1001),mod 会打印一条警告然后 **不安装拦截器**。这是有意的:base 步骤会改写 `Login` 里的协议号来骗过 BDS 的版本检查, 如果后面没有翻译链兜着,等于把玩家放进来再喂他一堆看不懂的包 —— 那比让他看到一个正常的 "版本不匹配"界面糟得多。 ## 诊断:抓那种「不报错但也活不下来」的包 翻译**失败**是会自己叫的:`run_steps` 会推一条 notice,日志里长这样: ``` v2168->v1001: failed to translate clientbound StartGame: unexpected end of packet: wanted 8, 7 left ``` 真正难查的是另一半 —— 每个 handler 都返回 `Ok`,字节却仍然不是对面那个版本能读的形状。 这时候一行日志都没有,只看得到玩家连上、一秒后消失。症状和「服务端主动踢人」完全一样, 从四行日志倒推等于猜。 所以加了一个按需打开的逐包 trace,走的是现成的 notice 通道,loader 那边不用改: ```bash CROSSBIND_TRACE=1 ./bedrock_server # 可选:改上限,默认 20 万行 CROSSBIND_TRACE=1 CROSSBIND_TRACE_LIMIT=8000 ./bedrock_server ``` 输出: ``` trace 0 clientbound id=11 StartGame [rewrite 4211 B -> 4198 B] trace 1 clientbound id=162 Packet#162 [forward 88214 B] trace 2 clientbound id=52 CraftingData [DROP] trace 3 serverbound id=144 PlayerAuthInput [rewrite 74 B -> 71 B] ``` 上限是必要的:出生时那一波是几千个包,不封顶的话最有价值的尾巴会被埋掉。 `Disconnect` 不受上限约束,永远打印,并且附带正文的可打印字符 —— 它回答的是 trace 其余部分回答不了的那个问题: - **出现了 clientbound `Disconnect`** → 是**服务端**踢的人,正文里通常带原因。 那么问题出在**上行**:我们把客户端的某个包翻译成了 BDS 读不懂的东西。 这一对版本上最可疑的是 `PlayerAuthInput`(客户端每 tick 都在发)。 - **完全没有 `Disconnect`** → 是**客户端**自己走的,说明它读崩了某个下行包。 那就看 trace 最后几行 —— 客户端掉线前收到的最后一个 id 就是要查的那个。 两种情况指向流水线的两半,先分清楚再动手,比继续按症状猜包快得多。 ## 测试 ```bash cargo test --workspace ``` 纯逻辑测试,不需要跑服务端 —— `bedrock-codec` 和 `bedrock-protocol` 都不依赖 loader, 这是刻意的。覆盖的是最容易出错也最难在线上发现的部分: - **往返性**:`up` 之后 `down` 必须是恒等变换,否则包每过一跳链就漂一格 - **NBT 边界**:截断的输入必须报错而不是 panic;标签必须停在正确的位置 - **物品编码**:空气在两种编码之间往返必须还是空气(v944 一个字节,v975 六个字节) - **负 Y 坐标**:v924 ↔ v944 的 BlockPos 编码变化只在 Y < 0 时才体现出来 - **链式路由**:任意两个支持的版本之间都要能找到路径,且是最短路径 - **失败模式**:坏包必须原样放行,不能吐出改了一半的字节 - **多跳链**:`start_game_survives_the_v975_to_v2168_chain` 走的是真实拓扑 (服务端 975、客户端 2168,中间经 1001 两跳),而不是单步往返。单步往返对 `StartGame` 尾部是盲区 —— 旧的 v975 fixture 在 `ServerAuthoritativeSound` 之后就结束了,`passthrough_all` 对空输入是恒等的,所以整条尾巴从来没被测到过。 新 fixture 带了填满的 `ServerJoinInformation` 和四个尾部 ID。 - **"全缺席也要对"**:一个 optional 三个成员都不设时,两个版本编码完全一致, 只测那种情况的 handler 看起来永远是对的。所以 optional 一律测两次:全填满、全缺席, 且全缺席那次要断言 handler 没有多吃后面的字段 ## 结构 ``` crates/ bedrock-codec/ reader / writer / 字段类型体系(无依赖,可单独测) bedrock-protocol/ 版本表、包 id、翻译步骤、链式路由(只依赖 codec) crossbind-mod/ cdylib,把上面两个接到 loader 的抓包钩子上 devdocs/ 每个源文件一份设计说明(见下) ``` `bedrock-protocol` 不依赖 loader 是有意为之:翻译逻辑是这里最值得测也最容易写错的部分, 不该需要一个跑着的服务端才能验证。 ## 致谢 本项目借鉴了 [EndstoneMC/endweave](https://github.com/EndstoneMC/endweave):早期的低版本 翻译步骤(v859 ~ v1001 这几档)是照着它的 Python 实现移植过来的,整体架构则沿用 ViaVersion 的思路。1.26.4x(协议 2168)这一档是本项目自己做的,上游没有覆盖。 协议号 ↔ 版本号对照表最初参考了 GlacieTeam/ProtocolLib 的公开发布说明,但 924 以下那部分 和 gophertunnel 自己的提交历史对不上(例如把 898 标成了不存在的 "1.25.60")。现在整张表 已经改成直接从 [gophertunnel](https://github.com/Sandertv/gophertunnel) 的 `minecraft/protocol/info.go` 提交历史逐条核对过的版本——那条线是 1.18 → 1.19 → 1.20 → 1.21.x(一路到 1.21.130)→ 1.26.0,中间没有 1.25.x。线格式本身的权威依据同样是 gophertunnel 的提交历史,1.26.30 ↔ 1.26.40 这一档的每处字段改动都对着 `b36ddad~1` 和 `HEAD` 逐行核对过。 [/MD]
  • 喜欢
反馈: LCH
作者
Maskviva
下载
15
查看
171
首次发布
最后更新

评分

5.00 星 1 次评分

Maskviva 的其他资源

最新评价

五星好评,神插件
后退
顶部 底部