传送目标流程
一条路牌从「被刻写」到「被传送」的完整数据链路,以及链路上每个环节的实际约束。所有坐标都源自客户端,服务端不做距离/权限校验。
阶段一:刻写(创建传送目标)
玩家手持魔法路牌右键未刻写的路牌(客户端 onBlockActivated)
→ openSignPostLabelGui(x, y, z, player.posX, floor(posY-1), player.posZ)
→ 玩家输入铭文(上限 15 字符)并点「完成」
→ CreateJumpTargetMessage(signX/Y/Z, jumpX/Y/Z, label) [判别号 0, C→S]
→ CreateJumpTargetMessageHandler
→ SignPostsMod.addJumpTarget(signX, signY, signZ, jumpX, jumpY, jumpZ, label, player)
SignPostsMod.addJumpTarget 做了什么(服务端):
| 步骤 | 代码 | 说明 |
|---|---|---|
| 1 | player.worldObj.getTileEntity(signX, signY, signZ) |
取路牌 TileEntity |
| 2 | if (tileEntity instanceof SignPostTileEntity) |
不是路牌则整段跳过(不报错、不扣耐久) |
| 3 | int markerId = addGlobalMarker(signX, signZ, label, player.worldObj) |
在 Antique Atlas 建全局标记(只取 X/Z)并 MarkersPacket 广播 |
| 4 | signPost.setAsJumpLocation(jumpX, jumpY, jumpZ, markerId) |
写入 4 个 NBT 字段(经 getBottom() 落到下半格)并 markBlockForUpdate |
| 5 | MarkerToTileMap.get(worldObj).setTileForMarker(signX, signY, signZ, markerId) |
记录 markerId → {x,y,z} 映射(存档数据) |
| 6 | currentItemStack.damageItem(1, player) |
扣 魔法路牌标记 1 点耐久,耗尽则销毁 |
重复刻写不会先删除旧标记:
onBlockActivated只在markerId == 0时才打开界面,所以正常玩家路径下不会产生孤儿标记。
阶段二:传送
玩家手持 Atlas 右键已刻写的路牌(客户端 onBlockActivated)
→ openAtlasGui(atlasStack, "signposts")
→ SignPostsAtlasGui:悬停 signPost 类型标记 → 染灰、toJump = marker
→ 玩家点击该标记
→ JumpMessage(markerId, "signposts") [判别号 1, C→S]
→ JumpMessageHandler
→ SignPostsMod.playerJump(markerId, player, "signposts")
SignPostsMod.playerJump 的执行顺序(服务端):
| 步骤 | 代码 | 说明 |
|---|---|---|
| 1 | paymentHandlers.get(paymentHandlerKey) |
按客户端给的键查处理器(可能为 null,源码无检查) |
| 2 | MarkerToTileMap.get(worldObj).getTileForMarker(markerId) |
反查 {x,y,z},为 null 则整个方法静默返回 |
| 3 | 先用 coords[0]+0.5 / coords[1]+1 / coords[2]+0.5 作为默认跳转点 |
方块中心上方 1 格 |
| 4 | if (tileEntity instanceof SignPostTileEntity) 则改用 tile.getJumpX/Y/Z() |
覆盖默认值,读取刻写时存下的玩家原坐标 |
| 5 | if (jumpX != 0 && jumpY != 0 && jumpZ != 0) |
三个轴都必须非 0 |
| 6 | player.capabilities.isCreativeMode || paymentHandler.pay(player, (int) jumpX, (int) jumpY, (int) jumpZ) |
创造模式免付费 |
| 7 | player.setPositionAndUpdate(jumpX, jumpY, jumpZ) |
传送(浮点坐标直接 setPositionAndUpdate) |
关键约束与已知失效条件
| 约束 | 来源 | 后果 |
|---|---|---|
目标必须是 SignPostTileEntity |
addJumpTarget / playerJump |
坐标不对时静默无操作 |
markerId 必须在 MarkerToTileMap 中 |
playerJump 第 2 步 |
查不到则静默无操作 |
| 跳转目标三个轴都不能为 0 | playerJump 第 5 步 |
刻写在坐标含 0 的轴上的路牌(主世界原点 X=0 或 Z=0)永久无法传送 |
| 无权限检查 | 全仓库 grep 无 permission/opLevel/ICommand |
任何玩家都能传送,费用一致 |
| 无距离限制 | playerJump 无距离判断 |
可跨整个世界瞬移 |
| 无维度校验 | playerJump 不比对 player.dimension |
目标坐标若与当前维度不符,由服务端坐标系统自行处理(通常落入地形或虚空) |
| 客户端坐标直接采信 | CreateJumpTargetMessageHandler 无校验 |
理论上可写入任意坐标 |
拆除时
路牌 的 breakBlock 在拆下半格时调用 SignPostsMod.removeJumpTarget(x, y, z, world):
int markerId = signPost.getMarkerId();
if (markerId != 0) {
removeGlobalMarker(markerId, world); // AtlasAPI.markers.deleteGlobalMarker
MarkerToTileMap.get(world).removeMarker(markerId); // 清理映射
}
拆上半格只连带拆下半格、不调用 removeJumpTarget(由下半格的 breakBlock 负责清理)。markerId == 0(从未刻写)时不做任何清理。
延迟加载侧
传送目的地不做区块加载请求——player.setPositionAndUpdate 直接把玩家放进目标坐标,区块由服务端按需加载。因此跳转到未加载的远距离区域时可能短暂看到未渲染地形。
相关条目
- 路牌 - 存储
jumpX/jumpY/jumpZ/markerId - Antique Atlas 集成 - 标记与
MarkerToTileMap - 饥饿值支付 - 第 6 步的扣费
- 网络消息 - 两条消息
- 铭文输入界面 - 阶段一入口