Read this first. 这一页给正在帮用户写 cheart 脚本的 AI 读。 完整 API 签名看左边侧栏的其他页;这里只放 你必须知道但容易错 的东西。
环境
- 语言:BeanShell 3.0(不是 Java 8 也不是 Groovy,有自己的一套规则)
- 宿主:Minecraft 1.20.1 Forge 47.3+,Java 17 toolchain
- 脚本位置:用户用
.scripts folder打开(路径不要硬编码) - 热重载:
.scripts reload [<name>],重载会保留 enabled / 键位 / 属性值 - 加载顺序:
_开头 → 含libs→ 其他;先加载的脚本可以bridge.addPublicMethod把工具发布给后面的
BeanShell 陷阱(AI 最常写错的)
1. 颜色 / 大十六进制字面量是 long,不是 int
BSH 对0xFF4CAF50 这种无符号值 > Integer.MAX_VALUE 的十六进制自动判成 long。API 的 color 参数全部是 long,直接传就行:
((long)intVal) & 0xFFFFFFFFL == hexLong。
2. 事件值不要用 (Number) / (int) 强转
BSH 对 Object → 抽象类 cast / Object → primitive cast 支持脆弱:
ScriptEvent 上的 getInt/getNumber/getBool/getString 都在 Java 侧做 instanceof Number 转型,BSH 不参与。
3. 未类型化变量在 if/else 里定义会成块作用域
4. 不能 implements Runnable
BSH 在本客户端不能实现 Java 接口。所有”回调”都用方法名字符串:
注解
@Module、@Command、@EventTarget、@OnLoad / @OnUnload / @OnEnable / @OnDisable 直接写就行。注解字段支持 name="..."、category="..."、defaultEnabled=true、key=71(GLFW keycode)等。
时机硬规则
错了会炸 / 行为不一致,没有补救:| API | 必须在这个事件里 | 原因 |
|---|---|---|
me.setRotation(..., moveFix=true) | player_update | RotationBus 在这个相位处理,moveFix 只对齐下一帧 motion 包 |
me.placeBlock(...) | player_update | 旋转 + 放置包要同 tick 发出,不然服务端看到旋转没改就 useItemOn |
render.* 绘制 | render_2d 或 render_screen | 否则 GuiGraphics 未绑定,静默 no-op |
render3d.* 绘制 | render_3d (post 阶段) | pre 阶段世界几何还没渲染完 |
render3d.clearDepth() | render_3d post,画 3D 文字前一次 | pre 阶段调会清掉游戏主渲染的深度 |
HttpClient.* | me.async(...) 包一下 | 同步阻塞,tick 里调会冻游戏 |
异步 / 线程安全
- 脚本事件一律在主线程派发(BeanShell interpreter 单线程执行)
me.async("method")→ 后台线程跑 method;方法里不能碰 MC 状态(碰了会和主线程竞争崩)- 后台拿数据,切回主线程用事件或标志位:
- 绝不要在
tick/render_2d里HttpClient.get、Thread.sleep、文件 IO、大循环(60fps × 1ms delay = 卡顿) bridge的 shared map 用ConcurrentHashMap,跨脚本读写安全
事件取消 / 写回
很多事件可以 cancel 或改参数:配置持久化(自动)
脚本 module 的这些状态自动存到 cheart 配置、下次启动恢复、.scripts reload 也保留:
enabled状态- 键位绑定(
key字段) - 所有注册的属性值(slider / boolean / mode / color / list / …)
int counter = 0;)—— reload 后回到文件里的初值。需要跨 reload 持久化的状态请塞进属性(vm.registerText 或 vm.registerSlider 隐藏掉)或者写到 bridge shared map。
常见安全问题
崩溃风险
- 在非主线程碰 MC:async 回调里直接调
me.getPlayer()/render.*/packet.send→ 并发崩 - 无限循环:BSH 没有超时保护,
while (true)冻死 - 深度递归:BSH 帧栈小,
invokeMethod递归 > 几百层炸栈 - 空指针:所有 API
get*方法在目标不存在时返回 null / 0 / false,但脚本自己链式调时要 null check:
服务端检测(防封号)
- 超 reach 交互:
useItemOn距离 > 5 格会被服务端拒绝或记录me.placeBlock前先检查pos.distanceTo(player.getPosition()) < 4.5
- 同 tick 多次发位置包:Blink / TimerHack 类脚本切勿一 tick 发 > 1 个 motion
- HTTP 请求自己服务器:
HttpClient.*请求会暴露 IP(服务器 log 看得到)——不要请求外网泄露玩家数据
自身脚本崩溃不会影响其他脚本
每个事件 handler 都有 try/catch 兜底,单脚本抛异常会被ScriptLog.error 打出来但不传染。但启动时 @OnLoad 抛异常会标记脚本 FAILED,后续事件不会派发给它。
不要做的事
- ❌ 改其他脚本的属性 / 状态(除非通过
bridge)——reload 时序不可控 - ❌
me.unsafe().invoke(...)调有副作用的 Forge 内部 API——可能崩服务端或 mod 兼容 - ❌ 把密钥 / token 硬编码进脚本——脚本文件用户肉眼可见
- ❌
bridge.clear()——影响所有脚本的共享数据,慎用 - ❌
@OnLoad里发包 / 调me.getPlayer()——此时玩家可能还没进服务器 - ❌ 写文件到 cheart 目录——用
me.unsafe()配合java.io.File写到<gameDir>/config/yourscript/
顶层对象速查(看详细签名跳详情页)
me— 玩家 / 世界 / 动作 / raytrace / 异步moduleManager— 查询 / 开关所有模块inventory— 背包 / 容器 / slot spoofpacket— 47 个 serverbound 包工厂 + 发送notify— 右上角通知bridge— 跨脚本共享 / 调用render— 2D 绘制 + world→screen 投影render3d— 3D 世界绘制
包装类速查
Entity及其子类Player/LocalPlayer/LivingEntity/ItemEntityWorld— 实体 / 方块 / 记分板 / tile entityBlock/ItemStack/Hit/Vec3Image/HttpClient/Regex
可工作的模板
1. 带属性的模块 + tick 事件
2. 2D ESP(render_2d 里画框)
3. 3D 世界文字
4. Scaffold(注意 player_update + moveFix)
5. 跨脚本工具库
6. 异步 HTTP + 主线程消费
7. 自定义 clickgui(registerScreen)
调试流程
脚本没按预期工作,按顺序检查:.scripts list— 状态✗ failed说明加载时崩了,.scripts info <name>看错误.scripts events— 事件订阅数为 0 说明@EventTarget没生效(可能写错事件名)- 在可疑方法第一行加
me.log("reached")看是否进入 - 运行时错误通过
ScriptLog输出到控制台(不是聊天),去看logs/latest.log ClassCastException多半是(int)/(Number)硬转,改用event.getInt类型方法InterpreterError: cannot assign <number> to type int= 十六进制太大,用 long 接
最后提醒
- 代码里少写类型声明,BSH 里
double x = 1.5和x = 1.5都能跑但后者在某些块作用域会出问题——看第 3 条陷阱 - API 调用失败都是软返回,不抛异常;脚本作者需要自己判空
- 每次改完脚本文件后
.scripts reload <name>就生效,不用重启游戏 - 写之前建议先看目标 API 详情页,上面有更完整的签名 / 参数说明