> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cheart.getvapu.today/llms.txt
> Use this file to discover all available pages before exploring further.

# LLM Primer

> 给 AI 辅助开发 / 代码补全用的速查

> **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**，直接传就行：

```java theme={null}
render.rect(x, y, w, h, 0xFF4CAF50);                  // ✓ 直接传
render.rect(x, y, w, h, new Color(255, 100, 0).getRGB()); // ✓ int → long widen 自动
```

不要写：

```java theme={null}
int c = 0xFF4CAF50;               // ✗ 溢出，BSH 抛 "cannot assign 4283215696 to int"
int c = (int) 0xFF4CAF50;          // ✗ BSH 的 (int) 超界不做截断，直接抛
```

比较两个颜色 bit 模式时走位掩码：`((long)intVal) & 0xFFFFFFFFL == hexLong`。

### 2. 事件值不要用 `(Number)` / `(int)` 强转

BSH 对 Object → 抽象类 cast / Object → primitive cast 支持脆弱：

```java theme={null}
int b = (int) event.get("button");        // ✗ 可能 ClassCastException
Number n = (Number) event.get("x");       // ✗ 同上

int b = event.getInt("button");            // ✓
double x = event.getNumber("x");            // ✓ (返回 double，也能拿 Integer 字段)
```

`ScriptEvent` 上的 `getInt/getNumber/getBool/getString` 都在 Java 侧做 `instanceof Number` 转型，BSH 不参与。

### 3. 未类型化变量在 `if/else` 里定义会成块作用域

```java theme={null}
if (cond) {
    x = 1;           // 块内新变量，外面看不到
} else {
    x = 2;
}
me.log(x);           // ✗ Undefined argument: x
```

修：在 if 前先初始化一次：

```java theme={null}
x = 0;
if (cond) { x = 1; } else { x = 2; }
me.log(x);           // ✓
```

### 4. 不能 `implements Runnable`

BSH 在本客户端不能实现 Java 接口。所有"回调"都用**方法名字符串**：

```java theme={null}
vm.registerButton("k", "Run", "onClick");         // "onClick" 是脚本里的方法名
void onClick() { me.log("clicked"); }

property.onChange("onValueChange");
me.async("doInBackground");
bridge.addPublicMethod("ns", "exposedFunc");
packet.sendSequenced("buildPacket");
```

### 注解

`@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 状态**（碰了会和主线程竞争崩）
* 后台拿数据，切回主线程用事件或标志位：

```java theme={null}
String cached = "";        // 主线程读
boolean ready = false;

void fetch() {
    r = HttpClient.get("...");
    cached = r.body;         // 后台写
    ready = true;
}

@EventTarget(events="tick")
void tick(e) {
    if (ready) {             // 主线程消费
        me.log(cached);
        ready = false;
    }
}
```

* 绝不要在 `tick` / `render_2d` 里 `HttpClient.get`、`Thread.sleep`、文件 IO、大循环（60fps × 1ms delay = 卡顿）
* `bridge` 的 shared map 用 `ConcurrentHashMap`，跨脚本读写安全

## 事件取消 / 写回

很多事件可以 cancel 或改参数：

```java theme={null}
@EventTarget(events="chat")
void onChat(e) {
    if (e.getString("message").startsWith(".")) return;  // cheart 命令透过
    e.put("message", "[cheart] " + e.getString("message")); // 改消息
    // e.cancel();  // 或者完全吞掉
}

@EventTarget(events="send_packet")
void onSend(e) {
    if ("ServerboundSwingPacket".equals(e.getString("type"))) {
        e.cancel();  // 不发
    }
}
```

字段详见 [events-reference](/api/events-reference)。

## 配置持久化（自动）

脚本 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：
  ```java theme={null}
  me.getPlayer().getPosition().x    // ✗ 断线时 getPlayer() null
  p = me.getPlayer(); if (p == null) return; pos = p.getPosition();  // ✓
  ```

### 服务端检测（防封号）

* **超 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**，后续事件不会派发给它。

### 不要做的事

1. ❌ 改其他脚本的属性 / 状态（除非通过 `bridge`）——reload 时序不可控
2. ❌ `me.unsafe().invoke(...)` 调有副作用的 Forge 内部 API——可能崩服务端或 mod 兼容
3. ❌ 把密钥 / token 硬编码进脚本——脚本文件用户肉眼可见
4. ❌ `bridge.clear()`——影响**所有脚本**的共享数据，慎用
5. ❌ `@OnLoad` 里发包 / 调 `me.getPlayer()`——此时玩家可能还没进服务器
6. ❌ 写文件到 cheart 目录——用 `me.unsafe()` 配合 `java.io.File` 写到 `<gameDir>/config/yourscript/`

## 顶层对象速查（看详细签名跳详情页）

* [`me`](/api/context) — 玩家 / 世界 / 动作 / raytrace / 异步
* [`moduleManager`](/api/module-manager) — 查询 / 开关所有模块
* [`inventory`](/api/inventory) — 背包 / 容器 / slot spoof
* [`packet`](/api/packet) — 47 个 serverbound 包工厂 + 发送
* [`notify`](/api/notify) — 右上角通知
* [`bridge`](/api/bridge) — 跨脚本共享 / 调用
* [`render`](/api/render) — 2D 绘制 + world→screen 投影
* [`render3d`](/api/render3d) — 3D 世界绘制

## 包装类速查

* [`Entity`](/api/entity) 及其子类 `Player` / `LocalPlayer` / `LivingEntity` / `ItemEntity`
* [`World`](/api/world) — 实体 / 方块 / 记分板 / tile entity
* [`Block`](/api/block) / [`ItemStack`](/api/itemstack) / [`Hit`](/api/hit) / [`Vec3`](/api/vec3)
* [`Image`](/api/image) / [`HttpClient`](/api/http) / [`Regex`](/api/regex)

## 可工作的模板

### 1. 带属性的模块 + tick 事件

```java theme={null}
@Module(name="Example", category="PLAYER", defaultEnabled=false)
void meta(vm) {
    vm.registerSliderDouble("speed", "Speed", 0.0, 5.0, 2.0, 0.1);
    vm.registerBoolean("notify_toggle", "Notify on Toggle", true);
}

@OnEnable  void onEnable()  {
    if (me.getBool("notify_toggle")) notify.info("Example", "on");
}
@OnDisable void onDisable() {
    if (me.getBool("notify_toggle")) notify.alert("Example", "off");
}

@EventTarget(events="tick")
void tick(event) {
    p = me.getPlayer();
    if (p == null) return;
    speed = me.getNumber("speed");
    // ...
}
```

### 2. 2D ESP（render\_2d 里画框）

```java theme={null}
@Module(name="Box ESP", category="VISUAL")
void meta(vm) { vm.registerColor("color", "Color", 0xFF00FF00); }

@EventTarget(events="render_2d")
void onRender(e) {
    self = me.getPlayer();
    if (self == null) return;
    long c = moduleManager.self().getProperty("color").getRGB();
    for (ent : me.getWorld().getPlayerEntities()) {
        if (ent.getId() == self.getId()) continue;
        r = render.worldBoxToScreenEntity(ent);
        if (r == null) continue;
        render.outline(r[0], r[1], r[2]-r[0], r[3]-r[1], 1.0, c);
    }
}
```

### 3. 3D 世界文字

```java theme={null}
@EventTarget(events="render_3d")
void onRender3D(e) {
    if (!render3d.isPost()) return;      // 只在 post 画
    render3d.clearDepth();               // 文字前清深度
    for (ent : me.getWorld().getItemEntities()) {
        p = render3d.interpolatedPos(ent);
        render3d.text("Item", p.x, p.y + 0.8, p.z, 1.5, true);
    }
}
```

### 4. Scaffold（注意 player\_update + moveFix）

```java theme={null}
@EventTarget(events="player_update")
void onPU(e) {
    p = me.getPlayer();
    if (p == null) return;
    // find (x, y, z, face)...
    float[] rot = me.getRotation(centerX, centerY, centerZ);
    me.setRotation(rot[0], rot[1], 180f, true);    // moveFix = true
    me.placeBlock(x, y, z, face, "MAIN_HAND");
}
```

### 5. 跨脚本工具库

```java theme={null}
// 文件名：_math_libs.bsh（下划线前缀，先加载）
@OnLoad void reg() {
    bridge.addPublicMethod("math", "distance");
    bridge.addPublicMethod("math", "lerp");
}
double distance(x1, y1, x2, y2) {
    return Math.sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1));
}
double lerp(a, b, t) { return a + (b - a) * t; }

// 其他脚本：
d = bridge.invokePublicMethod("math", "distance", 0.0, 0.0, 3.0, 4.0);
```

### 6. 异步 HTTP + 主线程消费

```java theme={null}
volatile String pending = null;

@Command(name="weather")
void exec(args) { me.async("fetch"); }

void fetch() {
    r = HttpClient.get("https://wttr.in/?format=3");
    if (r.isOk()) pending = r.body.trim();   // 后台写
}

@EventTarget(events="tick")
void tick(e) {
    if (pending != null) {                    // 主线程消费
        me.chat(pending);
        pending = null;
    }
}
```

### 7. 自定义 clickgui（registerScreen）

```java theme={null}
@Command(name="myui")
void exec(args) { me.openScreen("my_ui"); }

@OnLoad void onLoad() { me.registerScreen("my_ui"); }

@EventTarget(events="render_screen")
void onR(e) {
    render.rect(100, 100, 200, 100, 0xE0101418);
    render.textCentered("Hello", 200, 140, 0xFFFFFFFF);
}

@EventTarget(events="mouse_clicked")
void onClick(e) {
    double mx = e.getNumber("mouse_x"), my = e.getNumber("mouse_y");
    if (mx > 100 && mx < 300 && my > 100 && my < 200) {
        me.log("clicked");
    }
}

@EventTarget(events="close")
void onClose(e) { me.log("closed"); }
```

## 调试流程

脚本没按预期工作，按顺序检查：

1. `.scripts list` — 状态 `✗ failed` 说明加载时崩了，`.scripts info <name>` 看错误
2. `.scripts events` — 事件订阅数为 0 说明 `@EventTarget` 没生效（可能写错事件名）
3. 在可疑方法第一行加 `me.log("reached")` 看是否进入
4. 运行时错误通过 `ScriptLog` 输出到控制台（**不是**聊天），去看 `logs/latest.log`
5. `ClassCastException` 多半是 `(int)` / `(Number)` 硬转，改用 `event.getInt` 类型方法
6. `InterpreterError: cannot assign <number> to type int` = 十六进制太大，用 long 接

## 最后提醒

* 代码里**少写类型声明**，BSH 里 `double x = 1.5` 和 `x = 1.5` 都能跑但后者在某些块作用域会出问题——看第 3 条陷阱
* API 调用失败都是软返回，**不抛异常**；脚本作者需要自己判空
* 每次改完脚本文件后 `.scripts reload <name>` 就生效，**不用重启游戏**
* 写之前建议先看目标 API 详情页，上面有更完整的签名 / 参数说明
